Saturday 22 August 2020

How to pass a filled C/C++ structure from Python to C/C++?

Python has an excellent design of being compatible when interfacing with C/C++ libraries.

In this tutorial, we show an example of passing a filled C/C++ structure from Python 3 to C/C++ dynamic library function (in .so). The advantage of doing things in this way is that you do not need to include Python.h because ctypes is built-in library of Python.

C/C++ source code file test.cpp :

#include <iostream>
#include <cstdlib>
#include <inttypes.h>

#pragma pack(push, 1)
struct c_struct{
int32_t v1_int32;
int64_t v2_int64;
float v3_float32;
double v4_float64;
char v5_char;
char *v6_char_p;
int *v7_ptr_int32;
float *v8_ptr_float32;
float v9_arr_float32[2];
};
#pragma pack(pop)

extern "C" int cfunc_ptr(c_struct *obj) {
printf("c_struct:\n");
printf("v1_int32 = %" PRId32 "\n", obj->v1_int32);
printf("v2_int64 = %" PRId64 "\n", obj->v2_int64);
printf("v3_float32 = %.15g\n", obj->v3_float32);
printf("v4_float64 = %.15g\n", obj->v4_float64);
printf("v5_char = %c\n", obj->v5_char);
printf("v6_char_p = %s\n", obj->v6_char_p);

printf("v7_ptr_int32 =");
for(int x=0; x<10; ++x) printf(" %d", obj->v7_ptr_int32[x]);
printf("\n");

printf("v8_ptr_float32 =");
for(int x=0; x<10; ++x) printf(" %f", obj->v8_ptr_float32[x]);
printf("\n");

printf("v9_arr_float32 = [%.6f, %.6f]\n", obj->v9_arr_float32[0], obj->v9_arr_float32[1]);
return 1234;
}

extern "C" float cfunc_cst(c_struct obj) {
cfunc_ptr(&obj);
return 1.234f;
}

int main(int argc, const char *argv[]){
char str[] = "Hello1\0Hello2";
printf("Hello world\n");
int vi[10];
float v[10];
for(int x=0; x<10; ++x){ v[x] = x+0.5f; vi[x] = x+1;}
struct c_struct ca1 = {123456789, (int64_t)123456789123456789, 3.14159265358979, 3.14159265358979,
'Q', &str[0], &vi[0], &v[0], {1.2f, 3.4f}};
printf("cfunc_ca1() returns %f\n\n", cfunc_cst(ca1));
printf("cfunc_ptr() returns %d\n", cfunc_ptr(&ca1));
return 0;
}

Run the following command line to compile and generate the dynamic library test.so:

g++ -shared -o test.so test.cpp

Python source code which calls the C/C++ function with filled C structure c_struct :

import ctypes

class c_struct(ctypes.Structure):
_pack_ = 1
_fields_ = [('v1_int32', ctypes.c_int32),
('v2_int64', ctypes.c_int64),
('v3_float32', ctypes.c_float),
('v4_float64', ctypes.c_double),
('v5_char', ctypes.c_char),
('v6_char_p', ctypes.c_char_p),
('v7_ptr_int32', ctypes.c_void_p),
('v8_ptr_float32', ctypes.c_void_p),
('v9_arr_float32', ctypes.c_float*2),
]

if __name__ == '__main__':
import numpy as np

arr_int32 = np.arange(10, dtype=np.int32) + 1
arr_float32 = np.arange(10, dtype=np.float32) + 0.5
test_lib = ctypes.CDLL('test.so')
obj = c_struct(123456789, 123456789123456789, 3.14159265358979, 3.14159265358979, b'Q', b'Hello1',
arr_int32.ctypes.data, arr_float32.ctypes.data, (ctypes.c_float*2)(1.2, 3.4))

test_lib.cfunc_cst.restype = ctypes.c_float
ret = test_lib.cfunc_cst(obj)
print('cfunc_cst() returns %f\n' % ret)

ret = test_lib.cfunc_ptr(ctypes.pointer(obj))
print('cfunc_ptr() returns %d' % ret)

Take note that:

  1. A C/C++ pointer is a memory address (i.e., an integer), whether it is interpreted as pointing to integer array, float array, or etc., is all defined by user. So the interpretation does not matter, we just use void pointer in general.
  2. The default C/C++ function return type is always int when interpreted by Python, for non-integer function return type, you need to set function.restype explicitly.
  3. Numpy arrays support direct access by C/C++ functions, use array.ctypes.data (which is of void pointer type)
  4. The above Python code demonstrates both passing C structure by instance and by pointer.
  5. You must use {extern "C"} to define/declare every function to be exported.
  6. This method does not need to include Python.h, neither need to install python-dev
  7. For struct packing, C++ uses "#pragma pack(1)", Python uses "_pack_=1"