Search code examples
pythoncpointersctypes

Why double `ctypes.POINTER` object works for `char***` while triple `ctypes.POINTER` would make more sense?


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

Solution

  • 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)