1

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.

1 Answer 1

1

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
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for giving a short example! It works, however, in my application the stream ID is still -1. But I think it is more helpful now to debug this using the actual source code of the library. BTW: For anyone wondering why you should define argtypes and restype, check this out: link

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.