Search code examples
python-3.xstringbytectypesmutable

Strange ctypes behaviour on python callable wrapping c callable with c_char_p argtype


I'm observing a strange ctypes related behaviour in the following test program:

import ctypes as ct

def _pyfunc(a_c_string):
    print(type(a_c_string))
    a_c_string.value = b"87654321"
    return -123

my_str_buf = ct.create_string_buffer(b"test1234")
print(type(my_str_buf))

my_str_buf[3] = b'*'
print(my_str_buf.value)

my_str_buf.value = b"4321test"
print(my_str_buf.value)

signature = ct.CFUNCTYPE(ct.c_int, ct.c_char_p)
pyfunc = signature(_pyfunc)
pyfunc(my_str_buf)
print(my_str_buf.value)

The example wraps a python c callable in a python function via the ctypes api. The goal is to pass the python function a pointer to a c string let it modify it's contents (providing a fake value) and then return to the caller.

I started by the creation of a mutable string buffer via the ctypes function create_string_buffer. As can be seen from the example, the string buffer is indeed mutable.

After that i create a c function prototype using ctypes.CFUNCTYPE(ct.c_int, ct.c_char_p) and then instantiate that prototype with my python function which should be called using the same signature. Finally i call the python function with my mutable string buffer.

What irritates me is that the argument passed to that function shape shifts from type of <class 'ctypes.c_char_Array_9'> to <class 'bytes'> when the function is called. Unfortunately, the original mutable datatype turned into a completely useless non mutable bytes object.

Is this a ctypes bug? Python Version is 3.6.6.

Here is the output:

<class 'ctypes.c_char_Array_9'>
b'tes*1234'
b'4321test'
<class 'bytes'>
Traceback (most recent call last):
  File "_ctypes/callbacks.c", line 234, in 'calling callback function'
  File "C:/Users/andree/source/Python_Tests/ctypes_cchar_prototype.py", line 5, in _pyfunc
    a_c_string.value = b"87654321"
AttributeError: 'bytes' object has no attribute 'value'
b'4321test'

Expected output:

<class 'ctypes.c_char_Array_9'>
b'tes*1234'
b'4321test'
<class 'ctypes.c_char_Array_9'>
b'87654321'

Solution

  • ctypes.c_char_p is automatically converted to Python bytes. If you don't want the behavior, use either:

    • ctypes.POINTER(ctypes.c_char))
    • class PCHAR(ctypes.c_char_p): pass (derivations suppress the behavior)

    Note that an LP_c_char doesn't have a .value property, so I had to directly dereference the pointer to affect change in the value.

    Also, be careful not to exceed the length of the mutable buffer passed in. I added length as an additional parameter.

    Example:

    import ctypes as ct
    
    @ct.CFUNCTYPE(ct.c_int, ct.POINTER(ct.c_char), ct.c_size_t)
    def pyfunc(a_c_string,length):
        new_data = b'87654321\x00' # ensure new null termination is present.
        if len(new_data) > length: # ensure new data doesn't exceed buffer length
            return 0 # fail
        for i,c in enumerate(new_data):
            a_c_string[i] = c
        return 1 # pass
    
    my_str_buf = ct.create_string_buffer(10)
    result = pyfunc(my_str_buf,len(my_str_buf))
    print(result,my_str_buf.value)
    
    my_str_buf = ct.create_string_buffer(8)
    result = pyfunc(my_str_buf,len(my_str_buf))
    print(result,my_str_buf.value)
    
    1 b'87654321'
    0 b''