I have a Python C Extension that wraps the library for a proprietary product. Our company has a large amount of C code that uses the proprietary product. Instead of rewriting this in Python using my C Extension, I figured I could simply return a Capsule to Python land, and allow the user of my library to wrap some C function with ctypes.
Is this a valid approach? Is there a better one?
Here is some code to illustrate my approach.
My Python C Extension:
typedef struct {
PyObject_HEAD
Foo *foo; /* The proprietary data structure we are wrapping */
} PyFoo;
/*
* Expose a pointer to Foo such that ctypes can use it
*/
static PyObject PyFoo_capsule(PyFoo *self, PyObject *args, PyObject *kwargs)
{
return PyCapsule_New(self->foo, "foo", NULL);
}
Here is some pre-existing C code our team has written, and wants to call from Python:
void print_foo(Foo *foo)
{
Foo_print(foo);
}
And in Python, we can wrap the third party C code with ctypes (I learned this here):
import pyfoo
import ctypes
foo = pyfoo.Foo()
capsule = foo.capsule()
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
pointer = ctypes.pythonapi.PyCapsule_GetPointer(
capsule,
ctypes.create_string_buffer("foo".encode())
)
libfoo = ctypes.CDLL('libfoo.so')
libfoo.print_foo.restype = None
libfoo.print_foo.argtypes = [ctypes.POINTER(None)]
libfoo.print_foo(pointer)
It will work, but what I dislike about the approach of using void*
s for opaque types, is that any void*
will do, whereas on the C side, types are important and your diagnosis is most likely a segfault (or worse) if a pointer to the wrong type is passed.
Most (automatic) binders (SWIG, pybind11, cppyy for C/C++, or CFFI for C) will generate Python types for the opaque C/C++ ones, to allow type matching.
Here's a cppyy (http://cppyy.org) example, assuming file foo.h
like this:
struct Foo;
struct Bar;
typedef Foo* FOOHANDLE;
typedef Bar* BARHANDLE;
void use_foo(FOOHANDLE);
void use_bar(BARHANDLE);
and some matching library libfoo.so
, then when used from cppyy, you can only pass FOOHANDLE through FOOHANDLE arguments etc., so that you get a clean Python-side traceback, instead of a C-side crash. Example session:
>>> import cppyy
>>> cppyy.c_include("foo.h") # assumes C, otherwise use 'include'
>>> cppyy.load_library("libfoo")
>>> foo = cppyy.gbl.FOOHANDLE() # nullptr; can also take an address
>>> cppyy.gbl.use_foo(foo) # works fine
>>> cppyy.gbl.use_bar(foo)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: void ::use_bar(Bar*) =>
TypeError: could not convert argument 1
>>>
EDIT: With a bit of work, the same can be done with ctypes as show below, and thus if you expose a C function that returns self->foo
as a Foo*
you can likewise annotate its restype
with Python FOOHANDLE
, thus bypassing capsules and remaining type safe:
import ctypes
libfoo = ctypes.CDLL('./libfoo.so')
class Foo(ctypes.Structure):
_fields_ = []
FOOHANDLE = ctypes.POINTER(Foo)
class Bar(ctypes.Structure):
_fields_ = []
BARHANDLE = ctypes.POINTER(Bar)
libfoo.use_foo.restype = None
libfoo.use_foo.argtypes = [FOOHANDLE]
libfoo.use_bar.restype = None
libfoo.use_bar.argtypes = [BARHANDLE]
foo = FOOHANDLE()
libfoo.use_foo(foo) # succeeds
libfoo.use_bar(foo) # proper python TypeError