Search code examples
pythonstructctypes

Python, C library and nested struct


Have:

  1. С header with structures
struct STRUCT {
    float x;
    float y;
    float z;
};

struct BIG_STRUCT {
    int value;
    int array_count;
    struct STRUCT *array;
};

struct BIG_STRUCT *allocate_bs(struct BIG_STRUCT *bs);
struct BIG_STRUCT *do_something_bs(struct BIG_STRUCT *bs);
void free_bs(struct BIG_STRUCT *bs);

array_count - number of array elements.

  1. compiled library written in С.
  2. Python file using ctypes.
import ctypes
from ctypes import *

class CStruct(ctypes.Structure):
    _fields_ = [
        ('x', ctypes.c_float),
        ('y', ctypes.c_float),
        ('z', ctypes.c_float)
    ]

class CBigStruct(ctypes.Structure):
    _fields_ = [
        ('value', ctypes.c_int),
        ('array_count', ctypes.c_int),
        ('array', ctypes.POINTER(ctypes.POINTER(CStruct)))
    ]

if __name__ == '__main__':
    libc = ctypes.CDLL("./library/libTestPython.so.0.0")

    c_struct = CStruct

    libc.allocate_bs.argtypes = [ctypes.POINTER(ctypes.POINTER(CStruct))]
    libc.allocate_bs.restype = ctypes.POINTER(ctypes.POINTER(CStruct))
    result = libc.allocate_bs(ctypes.byref(c_struct))

    libc.do_something_bs.argtypes = [ctypes.POINTER(ctypes.POINTER(CStruct))]
    libc.do_something_bs.restype = ctypes.POINTER(ctypes.POINTER(CStruct))
    result = libc.do_something_bs(c_struct)

    libc.free_bs.argtypes = [ctypes.POINTER(ctypes.POINTER(CStruct))]
    libc.free_bs.restype = ctypes.c_int
    libc.free_bs(c_struct)

Error

Traceback (most recent call last):
  File "./example_struct.py", line 39, in <module>
    result = libc.allocate_bs(ctypes.byref(c_struct))
TypeError: byref() argument must be a ctypes instance, not '_ctypes.PyCStructType'

without ctypes.bref() in string result = libc.allocate_bs(c_struct) I have an error

Traceback (most recent call last):
  File "./example_struct.py", line 39, in <module>
    result = libc.allocate_bs(c_struct)
ctypes.ArgumentError: argument 1: <class 'TypeError'>: expected LP_LP_CStruct instance instead of _ctypes.PyCStructType

What is the problem? How to solve it?


Solution

    1. All your POINTER(POINTER(CStruct) should be POINTER(CBigStruct). The function interfaces have BIG_STRUCT* not STRUCT**.
    2. c_struct = CStruct doesn't create an instance, but just gives a new name to the type. Use c_struct = CStruct() to create an instance, but this isn't really needed if you return the allocated pointer (see #3).
    3. struct BIG_STRUCT *allocate_bs(struct BIG_STRUCT *bs); doesn't make sense, it should be struct BIG_STRUCT *allocate_bs() and return the allocated pointer.
    4. For a simple example like this, provide a minimal implementation of the C code for people answering to prove out their answer and be clear what you want the code to do. And less work for me 😉

    Here's something that works:

    // test.c -> test.dll
    // Compiled with Microsoft compiler: cl /LD /W4 test.c
    #include <stdlib.h>
    
    #ifdef _WIN32
    #   define API __declspec(dllexport)
    #else
    #   define API
    #endif
    
    struct STRUCT {
        float x;
        float y;
        float z;
    };
    
    struct BIG_STRUCT {
        int value;
        int array_count;
        struct STRUCT *array;
    };
    
    // Allocate and return an intialized structure
    API struct BIG_STRUCT *allocate_bs() {
        struct BIG_STRUCT* bs = malloc(sizeof(struct BIG_STRUCT));
        bs->value = 0;
        bs->array_count = 0;
        bs->array = NULL;
        return bs;
    }
    
    // fill out the structure with an allocated array and some data
    API void do_something_bs(struct BIG_STRUCT *bs) {
        bs->value = 7;
        bs->array_count = 2;
        bs->array = malloc(2 * sizeof(struct STRUCT));
        for(int i = 0; i < 2; ++i) {
            bs->array[i].x = (float)i;
            bs->array[i].y = (float)(i+1);
            bs->array[i].z = (float)(i+2);
        }
    }
    
    API void free_bs(struct BIG_STRUCT *bs) {
        if(bs != NULL) {
            if(bs->array != NULL)
                free(bs->array);
            free(bs);
        }
    }
    
    # test.py
    import ctypes as ct
    
    class CStruct(ct.Structure):
    
        _fields_ = (('x', ct.c_float),
                    ('y', ct.c_float),
                    ('z', ct.c_float))
    
        # So the structure can display itself when printed
        def __repr__(self):
            return f'CStruct(x={self.x}, y={self.y}, z={self.z})'
    
    class CBigStruct(ct.Structure):
    
        _fields_ = (('value', ct.c_int),
                    ('array_count', ct.c_int),
                    ('array', ct.POINTER(CStruct)))
    
        # slicing the pointer with the correct length will return a Python list
        def __repr__(self):
            return f'CBigStruct(value={self.value}, array={self.array[:self.array_count]})'
    
    libc = ct.CDLL('./test')
    libc.allocate_bs.argtypes = ()
    libc.allocate_bs.restype = ct.POINTER(CBigStruct)
    libc.do_something_bs.argtypes = ct.POINTER(CBigStruct),
    libc.do_something_bs.restype = None
    libc.free_bs.argtypes = ct.POINTER(CBigStruct),
    libc.free_bs.restype = None
    
    bs = libc.allocate_bs()
    print(bs.contents)
    libc.do_something_bs(bs)
    print(bs.contents)
    libc.free_bs(bs)
    

    Output:

    CBigStruct(value=0, array=[])
    CBigStruct(value=7, array=[CStruct(x=0.0, y=1.0, z=2.0), CStruct(x=1.0, y=2.0, z=3.0)])