Home > Enterprise >  Create an array of pointers in Python using ctypes
Create an array of pointers in Python using ctypes

Time:06-25

I want to create a Python-datatype using ctypes that matches the C-datatype "const char**", which resembles an array of pointers. However, I'm not able to code this in Python. The simplified C-function header looks like this:

int foo(int numOfProp, const char** propName, const char** propValue);

In C, the correct function call would look like this:

const char *PropName[2];
PropName[0] = "Prop_Index_1";
PropName[1] = "Prop_Index_2";

const char *PropValue[2];
PropValue[0] = "10";
PropValue[1] = "20";

stream_id = (*foo)(2, PropName, PropValue);

Basically, the function takes two arrays (pair of name and value) as well as the length of both arrays, and returns a stream ID. When the DLL is loaded, I can see that the function expects this ctypes datatype for the property arrays:

"LP_c_char_p"

However, I am really struggling to create this datatype based on lists of strings.

My first attempt (based on How do I create a Python ctypes pointer to an array of pointers) looks like this:

# set some dummy values
dummy_prop_values = [
    "10",
    "20"
]

# create property dict
properties = {
    f"Prop_Index_{i}": dummy_prop_values[i] for i in range(len(dummy_prop_values))
}

def first_try():
    # create a dummy ctype string
    ctypes_array = ctypes.c_char_p * 2

    # create empty c-type arrays for the stream properties
    prop_names = ctypes_array()
    prop_values = ctypes_array()

    # fill the empty arrays with their corresponding values
    for i, (prop_name, prop_value) in enumerate(properties.items()):
        prop_names[i] = prop_name.encode()
        prop_values[i] = prop_value.encode()

    # get pointer to properties
    ptr_prop_names = ctypes.pointer(prop_names)
    ptr_prop_values = ctypes.pointer(prop_values)

    return ptr_prop_names, ptr_prop_values

It throws this kind of error when I hand over the returned values to the function foo (which actually makes sense, since I explicitly created an array of length 2... I don't know how/why this worked for the other guy asking the question):

ctypes.ArgumentError: argument 2: <class 'TypeError'>: expected LP_c_char_p instance instead of LP_c_char_p_Array_2

My second attempt (based more or less on my own thoughts) looks like this:

def second_try():
    # convert properties to lists
    prop_names = [x for x in properties.keys()]
    prop_values = [x for x in properties.values()]
    
    # concat list elements, zero terminated
    # but I guess this is wrong anyway because it leads to an early string-termination (on byte-level)...?
    prop_names = ctypes.c_char_p("\0".join(prop_names).encode())
    prop_values = ctypes.c_char_p("\0".join(prop_values).encode())
    
    # get pointer to properties
    ptr_prop_names = ctypes.pointer(prop_names)
    ptr_prop_values = ctypes.pointer(prop_values)

    return ptr_prop_names, ptr_prop_values

This actually doesn't throw an error, but returns -1 as the stream ID, which denotes that "creating the stream wasn't successfull". I double checked all the other arguments of the function call, and these two properties are the only ones that can be wrong somehow.

For whatever reason I just can't figure out exactly where I make a mistake, but hopefully someone here can point me in the right direction.

CodePudding user response:

To convert a list some type into a ctypes array of that type, the straightforward idiom is:

(element_type * num_elements)(*list_of_elements)

In this case:

(c_char_p * len(array))(*array)

Note that (*array) expands the array as if each individual element was passed as a parameter, which is required to initialize the array.

Full example:

test.c - To verify the parameters are passed as expected.

#include <stdio.h>

#ifdef _WIN32
#   define API __declspec(dllexport)
#else
#   define API
#endif

API int foo(int numOfProp, const char** propName, const char** propValue) {
    for(int i = 0; i < numOfProp; i  )
        printf("name = %s    value = %s\n", propName[i], propValue[i]);
    return 1;
}

test.py

import ctypes as ct

dll = ct.CDLL('./test')
# Always define .argtypes and .restype to help ctypes error checking
dll.foo.argtypes = ct.c_int, ct.POINTER(ct.c_char_p), ct.POINTER(ct.c_char_p)
dll.foo.restype = ct.c_int

# helper function to build ctypes arrays
def make_charpp(arr):
    return (ct.c_char_p * len(arr))(*(s.encode() for s in arr))

def foo(arr1, arr2):
    if len(arr1) != len(arr2):
        raise ValueError('arrays must be same length')
    return dll.foo(len(arr1) ,make_charpp(arr1), make_charpp(arr2))

foo(['PropName1', 'PropName2'], ['10', '20'])

Output:

name = PropName1    value = 10
name = PropName2    value = 20
  • Related