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'
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''