pythonc++ctypesdestructorreturn-by-value

ctypes wrapper for function returning by value objects of a C++ class with destructor


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)

Solution

  • 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