Can ctypes
wrap functions that return objects (not pointers/references) of a C++ class with a destructor? The example below segfaults when calling lib.init_point_by_value
:
foo.cpp:
#include <iostream>
extern "C" {
using namespace std;
struct Point {
int x;
int y;
~Point();
};
Point::~Point() {
cout << "Point destructor called" << endl;
}
Point init_point_by_value(int x, int y) {
cout << "init_point_by_value called" << endl;
Point p;
p.x = x;
p.y = y;
return p;
}
Point& init_point_by_ref(int x, int y) {
cout << "init_point_by_ref called" << endl;
Point* p = new Point;
p->x = x;
p->y = y;
return *p;
}
void cleanup_point(Point* point) {
cout << "cleanup_point called" << endl;
if (point) {
delete point;
}
}
}
foo.py:
import ctypes
class Point(ctypes.Structure):
_fields_ = [
('x', ctypes.c_int),
('y', ctypes.c_int),
]
def setup_lib(lib_path):
lib = ctypes.cdll.LoadLibrary(lib_path)
lib.cleanup_point.argtypes = [ctypes.POINTER(Point)]
lib.init_point_by_value.argtypes = [ctypes.c_int, ctypes.c_int]
lib.init_point_by_value.restype = ctypes.POINTER(Point)
lib.init_point_by_ref.argtypes = [ctypes.c_int, ctypes.c_int]
lib.init_point_by_ref.restype = ctypes.POINTER(Point)
return lib
lib = setup_lib('./foolib.so')
p1 = lib.init_point_by_ref(3, 4)
lib.cleanup_point(p1)
# seg faults
p2 = lib.init_point_by_value(5, 6)
lib.cleanup_point(p2)
Compile and run it with:
g++ -c -fPIC foo.cpp -o foo.o && g++ foo.o -shared -o foolib.so && python foo.py
Output:
init_point_by_ref called
cleanup_point called
Point destructor called
init_point_by_value called
Segmentation fault (core dumped)
Compile with warnings enabled and I get:
x.cpp(17): warning C4190: 'init_point_by_value' has C-linkage specified, but returns UDT 'Point'
which is incompatible with C
This is due to the object having a destructor. Remove the destructor and it should accept it.
Another issue is the return type of init_point_by_value
is incorrect. It isn't a POINTER(Point)
but just a Point
:
lib.init_point_by_value.restype = Point
Finally, don't try to free the returned-by-value object.
Result with the fixes as follows (adapted slightly for my Windows system):
test.cpp
#include <iostream>
#define API __declspec(dllexport) // Windows-specific export
extern "C" {
using namespace std;
struct Point {
int x;
int y;
};
API Point init_point_by_value(int x, int y) {
cout << "init_point_by_value called" << endl;
Point p;
p.x = x;
p.y = y;
return p;
}
API Point& init_point_by_ref(int x, int y) {
cout << "init_point_by_ref called" << endl;
Point* p = new Point;
p->x = x;
p->y = y;
return *p;
}
API void cleanup_point(Point* point) {
cout << "cleanup_point called" << endl;
if (point) {
delete point;
}
}
}
test.py
import ctypes
class Point(ctypes.Structure):
_fields_ = [
('x', ctypes.c_int),
('y', ctypes.c_int),
]
def setup_lib(lib_path):
lib = ctypes.cdll.LoadLibrary(lib_path)
lib.cleanup_point.argtypes = [ctypes.POINTER(Point)]
lib.init_point_by_value.argtypes = [ctypes.c_int, ctypes.c_int]
lib.init_point_by_value.restype = Point
lib.init_point_by_ref.argtypes = [ctypes.c_int, ctypes.c_int]
lib.init_point_by_ref.restype = ctypes.POINTER(Point)
return lib
lib = setup_lib('test')
p1 = lib.init_point_by_ref(3, 4)
print(p1.contents.x,p1.contents.y)
lib.cleanup_point(p1)
p2 = lib.init_point_by_value(5, 6)
print(p2.x,p2.y)
Output
init_point_by_ref called
3 4
cleanup_point called
init_point_by_value called
5 6