Programmer's Python Data - Native Code
Written by Mike James   
Monday, 20 March 2023
Article Index
Programmer's Python Data - Native Code
Marshaling
Complex Data Types
Unicode
Unions

Marshaling C Types and Python Types

When 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:

ctypes typeC type

Python type

c_bool

_Bool

bool

c_char

char

1-character bytes object

c_wchar

wchar_t

1-character string

c_byte

char

integer

c_ubyte

unsigned char

integer

c_short

short

integer

c_ushort

unsigned short

integer

c_int

int

integer

c_uint

unsigned int

integer

c_long

long

integer

c_ulong

unsigned long

integer

c_longlong

__int64 or long long

integer

c_ulonglong

unsigned __int64 or 

unsigned long long

integer

c_size_t

size_t

integer

c_ssize_t

ssize_t or Py_ssize_t

integer

c_float

float

float

c_double

double

float

c_longdouble

long double

float

c_char_p

char* (NUL terminated)

bytes object or None

c_wchar_p

wchar_t* (NUL terminated)

string or None

c_void_p

void*

integer or None

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:

  • A ctype object connects a Python data type to a C data type.

  • The Python data type is stored in the value attribute.

  • The C data representation is stored in the object’s internal buffer which can be accessed using:

b=ctypes.string_at(ctypes.addressof(ctypeObject))


Last Updated ( Wednesday, 22 March 2023 )