Dealing with char*** in ctypes

I have this function in C library that I want to access from python using ctypes:

static const char *names[] =     {"Backplane", "ADC", "External", "Reserved"};

int test_char_ptr(const char ***names_, int *count_)
{
    *names_ = names;
    *count_ = 4;
    return 0;
}

I can then do this in C app:

    const char **items;
    unsigned nr_items;
    result = test_char_ptr(&items, &nr_items);
    for (unsigned i = 0; i < nr_items; i++) {
        printf("string[%d]: %s\n", i, items[i]);
    }

In python I’m doing the following so far:

    names_ = C.POINTER(C.POINTER(C.c_char))()
    count_ = C.c_int(123456789) # WORKS!

    pydemolib.test_char_ptr(names_, count_)
    print("count: %d" % count_.value)
    print("names: %s" % names_)
    print("names.contents: %s" % names_.contents)
    print("names.contents.contents: %s" % names_.contents.contents)
    print("names.contents.contents.value: %s" % names_.contents.contents.value)

----
count: 4
names: <pydemolib.LP_LP_c_char object at 0x7f0dd6ed7ac0>
names.contents: <ctypes.LP_c_char object at 0x7f0dd6c9f5c0>
names.contents.contents: c_char(b'B')
names.contents.contents.value: b'B'
names.contents.contents bytes: b'B'

How can I access the array of null terminated C strings in names_ at this point? It would be great if I could convert them to a list.

In ctypes, the type c_char means exactly that - a single character - and a POINTER to that really points specifically at that character and not anything subsequent. There is not a general “understanding” of how pointer decay, strings etc. work in C - that’s why, for example, the atomic types implement a * operator to give you array types. C allows arrays to decay to pointers implicitly all over the place, but Python is more strongly typed despite the dynamic typing - it maintains a strong distinction. So C.POINTER(C.c_char) doesn’t have access to any char values after the one directly pointed at; and it also doesn’t care if that char has a zero value.

To deal with the special case of C’s null-terminated strings, a separate c_char_p type is provided. (See also the full table of types).

You are actually really dealing with char**, not char*** in your ctypes code - in the C code, the third * is only being used in order to simulate pass-by-reference. (You’ll also want to check out the section on byref.) But what you really have is an array of count_ many “strings” (null-terminated C strings). That should look like c_char_p * count_ if I’m thinking clearly. (You might need to redesign the interface so that you get the count information separately, before the string data.)

2 Likes

For the sake of testing I’ve hardcoded the count_ in python to 4 and changed the signature as well as argument as follows:

test_char_ptr.argtypes = [POINTER(POINTER(c_char_p * 4)), POINTER(c_int)]

names_ = C.POINTER(C.c_char_p * 4)()
count_ = C.c_int(123456789) # WORKS!

pydemolib.test_char_ptr(names_, count_)
print("count: %d" % count_.value)
print("names: %s" % names_)
print("names.contents: %s" % names_.contents)
for name in names_.contents:
    print(name)

Results in this:

count: 4
names: <pydemolib.LP_c_char_p_Array_4 object at 0x7f31086f7ac0>
names.contents: <pydemolib.c_char_p_Array_4 object at 0x7f31084c35c0>
b'Backplane'
b'ADC'
b'External'
b'Reserved'

Seems to be exactly was I was looking for!

Thanks!

Since I can not change the API of the real shared object (this C code was a test code), I think I can get away with changing the signature of the ctypes function.
First it expects just one c_char_p value and after the call the count_ will be valid. Then I change the signature to expect count_ * c_char_p values and make a second call. This time I get all the C strings out.


pydemolib.test_char_ptr.restype = C.c_int
pydemolib.test_char_ptr.argtypes = [C.POINTER(C.POINTER(C.c_char_p)), C.POINTER(C.c_int)]
names_ = C.POINTER(C.c_char_p)()
count_ = C.c_int(123456789)
pydemolib.test_char_ptr(names_, count_)
print("count: %d" % count_.value)

pydemolib.test_char_ptr.argtypes = [C.POINTER(C.POINTER(C.c_char_p * count_.value)), C.POINTER(C.c_int)]
pydemolib.test_char_ptr.restype = C.c_int
names_ = C.POINTER(C.c_char_p * count_.value)()
count_ = C.c_int(123456789)

pydemolib.test_char_ptr(names_, count_)
# print("count: %d" % count_.value)
print("names: %s" % names_)
print("names.contents: %s" % names_.contents)
for name in names_.contents:
    print(name)

Output:

count: 4
names: <__main__.LP_c_char_p_Array_4 object at 0x7ff8edea35c0>
names.contents: <__main__.c_char_p_Array_4 object at 0x7ff8ee000540>
b'Backplane'
b'ADC'
b'External'
b'Reserved'
2 Likes