Reputation: 108
I need to expose a C++ class into embedded python, using Python C API.
In other words something like this: Exposing a C++ class instance to a python embedded interpreter but without using Boost at all.
The application I want to put this in is quite old, and the compiler, environment, etc... can't handle Boost.
Upvotes: 2
Views: 2132
Reputation: 211
In order to make an extension module you can use SWIG. It generates self-consistent code with no dependencies except python itself. Obtained bindings could be compiled with old compiler since there is no templates or other advances things. SWIG is not the easiest thing to learn, however. You can also look at PyBindGen.
Upvotes: 0
Reputation: 191
In short what you want to do is not very difficult as long as you understand the differences between C++ and Python, and let both C++ and Python handle the differences between the languages. The method I have found the easiest and safest is to use Python ctypes to define a Python class wrapper for your C++ class, and define an - extern “C” - wrapper to bridge your C++ class to the Python Class.
The advantages to this approach are that Python can handle all of the memory management, reference counts, etc…; while C++ can handle all of the type conversion, and error handling. Also if there are any future changes to the Python C API, you will not need to worry about it. Instead you can just focus on what is important your code.
Compared to wrapping the C++ class within the Python C API, this is way, way easier! As well as this method does not require anything not included with either C++ or Python standard libraries.
Below you will find an arbitrary example put together mainly from other Stack Overflow posts (cited in the Python wrapper). That I created when I was trying to figure out how to interface Python and C++. The code is heavily commented with details on how each portion of code is implemented. It is one way to do it.
The Python Wrapper:
"""
My C++ & Python ctypes test class. The following Stack Overflow URLs
either answered my questions as I figured this out, inspired code ideas,
or where just downright informative. However there are were other useful
pages here and there that I did not record links for.
http://stackoverflow.com/questions/1615813/how-to-use-c-classes-with-ctypes
http://stackoverflow.com/questions/17244756/python-ctypes-wraping-c-class-with-operators
http://stackoverflow.com/questions/19198872/how-do-i-return-objects-from-a-c-function-with-ctypes
"""
# Define imports.
from ctypes import cdll, c_int, c_void_p, c_char_p
# Load the shared library.
lib = cdll.LoadLibrary("MyClass.dll")
# Explicitly define the return types and argument types.
# This helps both clarity and troubleshooting. Note that
# a 'c_void_p' is passed in the place of the C++ object.
# The object passed by the void pointer will be handled in
# the C++ code itself.
#
# Each one of the below calls is a C function call contained
# within the external shared library.
lib.createClass.restype = c_void_p
lib.deleteClass.argtypes = [c_void_p]
lib.callAdd.argtypes = [c_void_p, c_void_p]
lib.callAdd.restype = c_int
lib.callGetID.argtypes = [c_void_p]
lib.callGetID.restype = c_char_p
lib.callGetValue.argtypes = [c_void_p]
lib.callGetValue.restype = c_int
lib.callSetID.argtypes = [c_void_p, c_char_p]
lib.callSetID.restype = c_int
lib.callSetValue.argtypes = [c_void_p, c_int]
lib.callSetValue.restype = c_int
class MyClass(object):
"""A Python class which wraps around a C++ object.
The Python class will handle the memory management
of the C++ object.
Not that only the default constructor is called for
the C++ object within the __init__ method. Once the
object is defined any specific values for the object
are set through library function calls.
"""
def __init__(self, id_str = ""):
"""Initialize the C++ class using the default constructor.
Python strings must be converted to a string of bytes.
'UTF-8' is used to specify the encoding of the bytes to
preserve any Unicode characters. NOTE: this can make
for unintended side effects in the C++ code.
"""
self.obj = lib.createClass()
if id_str != "":
lib.callSetID(self.obj, bytes(id_str, 'UTF-8'))
def __del__(self):
"""Allow Python to call the C++ object's destructor."""
return lib.deleteClass(self.obj)
def add(self, other):
"""Call the C++ object method 'add' to return a new
instance of MyClass; self.add(other).
"""
r = MyClass()
lib.callAdd(self.obj, other.obj, r.obj)
return r
def getID(self):
"""Return the C++ object's ID.
C char string also must be converted to Python strings.
'UTF-8' is the specified format for conversion to
preserve any Unicode characters.
"""
return str(lib.callGetID(self.obj), 'utf-8')
def getValue(self):
"""Return the C++ object's Value."""
return lib.callGetValue(self.obj)
def setID(self, id_str):
"""Set the C++ object's ID string.
Remember that Python string must be converted to
C style char strings.
"""
return lib.callSetID(self.obj, bytes(id_str, 'utf-8'))
def setValue(self, n):
"""Set the C++ object's value."""
return lib.callSetValue(self.obj, n)
if __name__ == "__main__":
x = MyClass("id_a")
y = MyClass("id_b")
z = x.add(y)
z.setID("id_c")
print("x.getID = {0}".format(x.getID()))
print("x.getValue = {0}".format(x.getValue()))
print()
print("y.getID = {0}".format(y.getID()))
print("y.getValue = {0}".format(y.getValue()))
print()
print("z.getID = {0}".format(z.getID()))
print("z.getValue = {0}".format(z.getValue()))
The C++ class & extern C wrapper:
#include <iostream>
#include <new>
#include <string>
using namespace std;
// Manually compile with:
// g++ -O0 -g3 -Wall -c -fmessage-length=0 -o MyClass.o MyClass.cpp
// g++ -shared -o MyClass.dll "MyClass.o"
// Check to see if the platform is a Windows OS. Note that
// _WIN32 applies to both a 32 bit or 64 bit environment.
// So there is no need to check for _WIN64.
#ifdef _WIN32
// On Windows platforms declare any functions meant to be
// called from an external program in order to allow the
// function to be able to be called. Else define a DEF
// file to allow the correct behaviour. (much harder!)
#define DLLEXPORT __declspec(dllexport)
#endif
#ifndef DLLEXPORT
#define DLLEXPORT
#endif
class MyClass {
// A C++ class solely used to define an object to test
// Python ctypes compatibility. In reality this would
// most likely be implemented as a wrapper around
// another C++ object to define the right a compatible
// object between C++ and Python.
public:
MyClass() : val(42), id("1234567890") {};
// Notice the next constructor is never called.
MyClass(string str) : val(42), id(str) {};
~MyClass(){};
int add(const MyClass* b, MyClass* c) {
// Do not allow exceptions to be thrown. Instead catch
// them and tell Python about them, using some sort of
// error code convention, shared between the C++ code
// and the Python code.
try {
c->val = val + b->val;
return 0;
/*
} catch(ExceptionName e) {
// Return a specific integer to identify
// a specific exception was thrown.
return -99
*/
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return -1;
} // end try
}; // end method
string getID() { return id; };
int getValue() { return val; };
void setID(string str) { id = str; };
void setValue(int n) { val = n; };
private:
int val;
string id;
}; // end class
extern "C" {
// All function calls that Python makes need to be made to
// "C" code in order to avoid C++ name mangling. A side
// effect of this is that overloaded C++ constructors must
// use a separate function call for each constructor that
// is to be used. Alternatively a single constructor can
// be used instead, and then setters can be used to specify
// any of an object instance specific values. Which is
// what was implemented here.
DLLEXPORT void * createClass(void) {
// Inside of function call C++ code can still be used.
return new(std::nothrow) MyClass;
} // end function
DLLEXPORT void deleteClass (void *ptr) {
delete static_cast<MyClass *>(ptr);
} // end function
DLLEXPORT int callAdd(void *a, void *b, void *c) {
// Do not allow exceptions to be thrown. Instead catch
// them and tell Python about them.
try {
MyClass * x = static_cast<MyClass *>(a);
MyClass * y = static_cast<MyClass *>(b);
MyClass * z = static_cast<MyClass *>(c);
return x->add(y, z);
/*
} catch(ExceptionName e) {
// Return a specific integer to identify
// a specific exception was thrown.
return -99
*/
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return -1;
} // end try
} // end function
DLLEXPORT const char* callGetID(void *ptr) {
try {
MyClass * ref = static_cast<MyClass *>(ptr);
// Inside of function call C++ code can still be used.
string temp = ref->getID();
// A string must be converted to it "C" equivalent.
return temp.c_str();
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return "-1";
} // end try
} // end function
DLLEXPORT int callGetValue(void *ptr) {
try {
MyClass * ref = static_cast<MyClass *>(ptr);
return ref->getValue();
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return -1;
} // end try
} // end function
DLLEXPORT int callSetID(void *ptr, char *str) {
try {
MyClass * ref = static_cast<MyClass *>(ptr);
ref->setID(str);
return 0;
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return -1;
} // end try
} // end function
DLLEXPORT int callSetValue(void *ptr, int n) {
try {
MyClass * ref = static_cast<MyClass *>(ptr);
ref->setValue(n);
return 0;
} catch(...) {
// Return an error code to identify if
// an unknown exception was thrown.
return -1;
} // end try
} // end function
} // end extern
Note: Trog unfortunately I do not have a high enough reputation to post comments yet, as I am new to Stack Overflow. Otherwise I would like to have asked if Python ctypes was available in you embedded Python environment first. In fact this is my first post.
Upvotes: 6