Friday, April 6, 2012

Python wrapper in C++11

This is a post about a wrapper for python scripts I developed using C++11. This was the first time I used variadic templates, and i must say it's an amazing feature in this new C++ standard! It's great to have a type-safe way to use variable arguments.

The whole wrapper is inside the Python namespace. Before you start using anything inside it, you should call Python::initialize(), which initializes the Python API.

The Python::Object class provides an abstraction of a python object. Python::Objects wrap a PyObject*(which is the abstraction of a Python object provided by its API), inside a std::shared_ptr. On their destructor, a call to Py_DECREF is performed, so the underlying PyObject* will get free'd appropriately.

In order to load a python script, a static method Python::Object::from_script(const std::string &) should be called, which returns a Python::Object that represents that script. The name of the script(with or without the ".py" extension) should be passed as argument:

#include "pywrapper.h"

/* ... */

Python::Object script = Python::Object::from_script(""); //could be test also

After that sentence, the "script" variable will have loaded the "" script, located in the current working directory.

Now you can call functions defined in that script using the Python::Object::call_function method, which has this signatures:


// Variadic template arguments version
template<typename... Args>
Python::Object call_function(const std::string &name, const Args... &args);
// No arguments version
Python::Object call_function(const std::string &name);

This method takes the name of the function as the first argument, followed by 0 or more arguments. These arguments will be implicitly converted to PyObject pointers, which can be used as arguments using the Python API. So far, you can use arguments of these types:
  • std::string
  • const char *
  • Any integral type(for which std::is_integral is true).
  • bool
  • double
  • std::vector
  • std::list
  • std::map
Both std::vector and std::list will be converted to a Python list, with the exception of std::vector<char>, which will be converted to a bytearray. The std::map objects will be converted to Python dicts.

The Script::call_function method returns the Python return value wrapped in a Python::Object. Note that the objects stored in the std::vectors and std::lists must also be convertible to PyObject pointers(must be listed above).

In case you want to use the return value, you might want to use Python::Object::convert which will convert the wrapped PyObject* into one of the same C++ types mentioned above, and also std::tuple.

As an example, i used this python script:


def foo(a, b, c, d, e):
    print '{0} - {1} - {2} - {3} - {4}'.format(
        a, str(b), str(c), repr(d), repr(e)

def int_fun():
    return 12

def list_fun():
    return [1,2,3,4,561,2]
def dict_fun():
    return {
        'bar' : 1,
        'foo' : 15

def tuple_fun():
    return (1, 'foo', 15.5)

def bool_fun():
    return False

x = 1598

It's just a bunch of functions that take/return different types of arguments. My C++ code that calls these functions is this one:

#include <string>
#include <vector>
#include <iostream>
#include <stdexcept>
#include <iomanip>
#include <map>
#include <tuple>
#include "pywrapper.h"

int main() {
    Python::Object script(Python::Object::from_script(""));
    std::vector<int> v({2,6,5});
    std::map<std::string, int> dict({
        {"bleh", 1},
        {"foofoo", 10}
    std::cout << "Calling foo:\n";
    script.call_function("foo", "a string", true, 10, v, dict);
    // Int test
    std::cout << "Calling int_fun:\n";
    Python::Object ptr = script.call_function("int_fun");
    int num;
        std::cout << "Result: " << num << '\n';
        std::cout << "Long conversion failed\n";
    // List test
    std::vector<int> lst;
    std::cout << "Calling list_fun:\n";
    ptr = script.call_function("list_fun");
    if(ptr.convert(lst)) {
        std::cout << "List size: " << lst.size() << '\n';
        for(auto it(lst.begin()); it != lst.end(); ++it)
            std::cout << *it << " ";
        std::cout << '\n';
        std::cout << "List conversion failed\n";
    // Dict test
    std::map<std::string, int> mp;
    std::cout << "Calling dict_fun:\n";
    ptr = script.call_function("dict_fun");
    if(ptr.convert(mp)) {
        std::cout << "Map size: " << mp.size() << '\n';
        for(auto it(mp.begin()); it != mp.end(); ++it)
            std::cout << it->first << " -> " << it->second << '\n';
        std::cout << "Map conversion failed\n";
    // Tuple test
    std::cout << "Calling tuple_fun:\n";
    ptr = script.call_function("tuple_fun");
    std::tuple<int, std::string, double> tup;
    if(ptr.convert(tup)) {
        std::cout << std::get<0>(tup) << "\n";
        std::cout << std::get<1>(tup) << "\n";
        std::cout << std::get<2>(tup) << "\n";
        std::cout << "Tuple conversion failed\n";
    bool bool_val;
    std::cout << "Calling bool_fun:\n";
    ptr = script.call_function("bool_fun");
        std::cout << "Result: " << std::boolalpha << bool_val << '\n';
        std::cout << "Long conversion failed\n";
    // Get attr test
    std::cout << "Retrieving 'x' variable:\n";
    try {
        ptr = script.get_attr("x");
            std::cout << "X == " << num << '\n';
    } catch(std::runtime_error &ex) {
        std::cout << ex.what() << '\n';

In the last code senteces, the Python::Object::get_attr method is used, which returns a Python::Object containing the contents of the script attribute which has this method argument's name. After executing this application, this output is produced:

Calling foo:
a string - True - 10 - [2, 6, 5] - {'bleh': 1, 'foofoo': 10}
Calling int_fun:
Result: 12
Calling list_fun:
List size: 6
1 2 3 4 561 2
Calling dict_fun:
Map size: 2
bar -> 1
foo -> 15
Calling tuple_fun:
Calling bool_fun:
Result: false
Retrieving 'x' variable:
X == 1598

As you can see, this class allows a type-safe variable argument interface for calling Python functions and retrieving defined attributes in scripts.

You can get the header and source file here.

In order to compile this application, using gcc, remember to use the -std=c++0x and -lpython2.7 arguments.

I hope you find this wrapper useful!