Programmer's Python Data - Native Code |
Written by Mike James | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Monday, 20 March 2023 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Page 2 of 5
Marshaling C Types and Python TypesWhen calling the example sum function we simply passed a pair of int literals and received an int as the result. The default handling of the conversion from a Python bignum to a C int was handled automatically. All Python types except integers, strings and bytes have to be explicitly converted using a ctypes object.
The basic type conversions are:
You can think of each of these as an object wrapper for the Python value which when passed to the external function specifies the conversion that is performed to send the data. To create a ctype object you use its constructor. For example: myFloat = ctypes.c_float(3.1415) creates a ctypes object wrapping a float. If you print the object: print(myFloat) you will see: c_float(3.1414999961853027) The Python value of the ctype object is stored in its value attribute and this can be used in expressions and modified: print(myFloat.value+1.0) myFloat.value = 42.0 print(myFloat) displays: 4.141499996185303 c_float(42.0) The value attribute gives you the Python representation of the data. Getting the C representation is slightly more difficult. Each ctype object has a buffer which stores the byte sequence that corresponds to the C data type. You can’t directly convert the buffer to a byte sequence, but you can do it using the string_at method combined with the addressof method. The addressof method will return the address of the buffer used by any ctype object. Once you have this you can use the string_at method to convert the buffer’s contents into a bytes object. For example: myFloat=ctypes.c_float(3.1415) b=ctypes.string_at(ctypes.addressof(myFloat)) print(b.hex()) displays: 560e4940 which is the four-byte C float representation of the Python value. When you make a call to a C function it is the contents of the ctype’s buffer that is sent to the function. For example, if we change the C sum function to: float sum(float a, float b){ return a + b; } and call it from Python using: print(lib.sum(1.0,2.0)) the result is an exception. To make it work you have to pass c_float objects: print(lib.sum(ctypes.c_float(1.0),ctypes.c_float(2.0))) Now the call works, but the result it prints is nonsense. The problem is that the return type is a C float and this needs to be converted to a Python float (which in most cases is the same as a C double). The solution to this problem is to set the function’s restype property: lib.sum.restype=ctypes.c_float Now everything works: import ctypes lib = ctypes.cdll.LoadLibrary("build/libmyLib.so") lib.sum.restype = ctypes.c_float print(lib.sum(ctypes.c_float(1.0),ctypes.c_float(2.0))) and you will see 3.0 displayed. Notice that the return value isn’t a ctype but a standard Python float. What happens is that when you call the function the arguments are converted from Python floats to C floats and these are used in the function call. The return value, which is a C float, is then converted into a Python float using the fact that the return type is a c_float. In other languages and systems this sort of behavior is generally called “marshaling”. The ctype module provides marshaling for parameters and return types. It is also possible but not necessary to specify the function types by assigning a tuple of ctypes to the function’s argtypes attribute. lib=ctypes.CDLL("build/libmyLib.dll") lib.sum.restype=ctypes.c_float lib.sum.argtypes=(ctypes.c_float,ctypes.c_float) print(lib.sum(ctypes.c_float(1.0),ctypes.c_float(2.0))) If you try to pass the wrong types to the function you will see a TypeError exception. So to summarize:
b=ctypes.string_at(ctypes.addressof(ctypeObject)) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Last Updated ( Wednesday, 22 March 2023 ) |