I have a library my_lib
with a C function that takes a char***
parameter, a pointer to an array of char*
that is allocated by the function. Here is a minimal reproducible example of such a function:
void getArrayOfStrings(char*** paramPtr)
{
(*paramPtr) = (char**) malloc(3*sizeof(char*));
(*paramPtr)[0] = (char*) malloc(strlen("Foo")+1);
strcpy((*paramPtr)[0], "Foo");
(*paramPtr)[1] = (char*) malloc(strlen("Bar")+1);
strcpy((*paramPtr)[1], "Bar");
(*paramPtr)[2] = 0;
}
It sets the last array element to 0, so that caller can identify it (rather than providing the size as a second parameter). Note that a separate function is provided to free the memory.
I run ctypesgen
to generate a Python binding to this function. It generates this code:
getArrayOfStrings = _lib.get("getArrayOfStrings", "cdecl")
getArrayOfStrings.argtypes = [POINTER(POINTER(POINTER(c_char)))]
getArrayOfStrings.restype = None
This generated binding can be called from the Python script below:
import my_lib
import ctypes
names = ctypes.POINTER(ctypes.POINTER(ctypes.c_char))()
my_lib.getArrayOfStrings(names)
if names:
for name in names:
name_str = my_lib.String(name)
if name_str:
print("Got name: " + str(name_str))
else:
break
It works just fine and prints "Foo\nBar\n"
I'm just wondering why using ctypes.POINTER(ctypes.POINTER(ctypes.c_char))
, that I understand as being a "point to pointer to char", so a char**
, works. Why I should not be using a ctypes.POINTER(ctypes.POINTER(ctypes.POINTER(ctypes.c_char)))
?
I tested with ctypes.POINTER(ctypes.POINTER(ctypes.POINTER(ctypes.c_char)))
, the same code produces the error:
my_lib.getArrayOfStrings(names)
OSError: exception: access violation writing 0x0000000000000000
Because ctypes
knows that the function takes char***
due to .argtypes
being defined, passing a char**
implies ctypes.byref
(take the address of), which passes names
as char***
. Here's a working example with an explicit byref
and using your C code as a DLL:
import ctypes as ct
dll = ct.CDLL('./test')
getArrayOfStrings = dll.getArrayOfStrings
getArrayOfStrings.argtypes = ct.POINTER(ct.POINTER(ct.POINTER(ct.c_char))),
getArrayOfStrings.restype = None
names = ct.POINTER(ct.POINTER(ct.c_char))()
dll.getArrayOfStrings(ct.byref(names))
#dll.getArrayOfStrings(names) # also works but byref implied here.
i = 0
while names[i]:
print(ct.string_at(names[i]))
i += 1
Output:
b'Foo'
b'Bar'
When names = ct.POINTER(ct.POINTER(ct.POINTER(ct.c_char)))()
is used, ctypes
does not have to add the implied byref
and assumes you know what you are doing, but in this case the char***
is null
. Since it is being used as an output parameter, C tries to write to it and fails.
FYI, ctypes
also has c_char_p
which is a special handler for null-terminated byte strings (char*
), so this works without the extra string extraction:
import ctypes as ct
dll = ct.CDLL('./test')
getArrayOfStrings = dll.getArrayOfStrings
getArrayOfStrings.argtypes = ct.POINTER(ct.POINTER(ct.c_char_p)),
getArrayOfStrings.restype = None
names = ct.POINTER(ct.c_char_p)()
dll.getArrayOfStrings(names)
i = 0
while names[i]:
print(names[i])
i += 1
(same output)