pythonarraysctypes

Passing Numeric Arrays to Python via ctypes Without Prior Knowledge of Array Length


I would like a C++ function return a numeric array (uint8_t* converted from a vector) into a python bytes object. I don't know the length of this array ahead of time. This is numeric data, not a C string that would naturally have a null terminator. I'm having trouble finding any documentation on how to handle the length of the returned C pointer. Presumably, I can't just cast it to bytes since there's no length information in the bytes constructor. I'd like to avoid a really slow for loop.

C++:

uint8_t* cfunc(uint32_t* length_of_return_value); // returns an arbitrary-length numeric vector

Python:

import ctypes as c
c.cdll.LoadLibrary("file.so")
clib = c.CDLL("file.so")
clib.cfunc.argtypes = [c.POINTER(c.c_uint32)]
clib.cfunc.restype = c.POINTER(c.c_uint8)
length_of_return_value = c.c_uint32(0)
x = clib.cfunc(length_of_return_value)
# now what?
assert type(x) in {bytes, list}, "I need this to be a type that's convertible to numpy array"

Solution

  • There are a number of methods. Here are a few:

    Since you mentioned "I need this to be a type that's convertible to numpy array" I recommend the last method above and it will already be a numpy array.

    Make sure that if the returned value is an allocated array to free the memory when you are through with it. If using the first two methods above, you can free the memory immediately after making the copy, but if using the last shared memory method, do not free the memory until you are done with the numpy array.

    Working example (Windows):

    test.c:

    #include <stdint.h>
    #include <stdlib.h>
    
    __declspec(dllexport)
    uint8_t* cfunc(uint32_t* psize) {
        uint8_t* retval = malloc(5);
        for(uint8_t i = 0; i < 5; ++i)
            retval[i] = i;
        *psize = 5;
        return retval;
    }
    
    __declspec(dllexport)
    void cfree(uint8_t* p) {
        free(p);
    }
    

    test.py:

    import ctypes as ct
    import numpy as np
    
    clib = ct.CDLL('./test')
    clib.cfunc.argtypes = ct.POINTER(ct.c_uint32),
    clib.cfunc.restype = ct.POINTER(ct.c_uint8)
    clib.cfree.argtypes = ct.POINTER(ct.c_uint8),
    clib.cfree.restype = None
    
    size = ct.c_uint32(0)
    retval = clib.cfunc(size)
    
    # Using string_at:
    result = ct.string_at(retval, size.value)
    print(result)  # bytes
    
    # slicing to correct size
    print(retval[:size.value])  # list of int
    
    # Shared buffer using numpy
    result = np.ctypeslib.as_array(retval, shape=(size.value,))
    print(result)  # numpy array
    result[0] = 7  # modifying numpy array...
    print(result)  # numpy array
    print(retval[0])  # ... also modifies original data
    
    clib.cfree(retval)
    

    Output:

    b'\x00\x01\x02\x03\x04'
    [0, 1, 2, 3, 4]
    [0 1 2 3 4]
    [7 1 2 3 4]
    7