前言

python执行效率较低,在一些要求效率或作为胶水语言进行软仿等场景下,可以通过调用编译后的C程序实现。假设有以下C代码:

// test.c
#include <stdio.h>
#include <string.h>

typedef struct MyStruct
{
    double myDouble;
    int myArr[10];
    float myArr2[5][10];
    char myStr[6];
}MyStruct;

typedef struct ResStruct
{
    MyStruct myStruct;
    char msg[6];
}ResStruct;

ResStruct rs;

void * run_test(void * myStruct) {
    MyStruct ms = *(MyStruct *)myStruct;

    // do somthings
    printf("C::myDouble = %f\n", ms.myDouble);
    printf("C::myArr = ");
    for (int i = 0; i < 10; i ++) {
        printf(" %d ", ms.myArr[i]);
    }
    printf("\n");
    printf("C::myStr = %s\n", ms.myStr);

    rs.myStruct = ms;
    strcpy(rs.msg, "world");
    return &rs;
}

目的是要实现python调用c中的run_test方法。首先写一个makefile文件,将其编译成动态库:

CC = gcc
CFLAGS1 = -Wall -g
CFLAGS2 = -g

test.o: test.c
    ${CC} -shared -fPIC test.c ${CFLAGS1} -o test.so

clean:
    rm -rf *.so
    rm -rf *.out
    rm -rf /temp/*.o

编译命令:

make clean && make

在进行c调用时,需要将python层面的数据通过ctypes映射包裹成c中的数据类型,如示例中参数与输出都为结构体,其在python中对应的是继承了ctypes.Structure的类,下面说明一下各类型的数据包裹。

基本类型

对于基本类型而言,如intfloatdouble等,ctypes中直接有相应类型的定义,直接使用即可,如:

a = 2.1
a_in_c = ctypes.c_float(a) # 转换成c中的float

数组

对于数组,需要对数组中的每个元素进行转换,如:

arr = list(map(lambda x: x, range(10)))
arr_in_c = (ctypes.c_int * 10)(*arr) # 即对应于c中的int[10]

对于多维数组,需要从里至外逐层转换,以示例中的类型为例:

float myArr2[5][10]

其在python对应的包裹格式应为:

 (ctypes.c_float * 10) * 5  # 每组包括10个c_float类型数据,一共5组,即为 5x10 的数组

包裹如下:

my_arr_2_list = []
for i in range(5):
    # 一维元素
    temp = [x for x in range(10)]
    # 各元素转换类型
    my_arr_2_list.append((ctypes.c_float * 10)(*temp))
my_arr2 = ((ctypes.c_float * 10) * 5)(*my_arr_2_list)

结构体

结构体对应于python中集成了ctypes.Structure的类,类属性_fields_描述结构体中的字段,对于示例中的结构体,对应结构如下:

class MyStruct(ctypes.Structure):
    _fields_ = [
        ('my_double', ctypes.c_double),
        ('my_arr', ctypes.c_int * 10),
        ('my_arr2', (ctypes.c_float * 10) * 5),
        ('my_str', ctypes.c_char * 6)
    ]

class ResStruct(ctypes.Structure):
    _fields_ = [
        ('ms', MyStruct),
        ('msg', ctypes.c_char * 6)
    ]

其中_fields_中为对结构体字段的描述,每一个元组元素即对应于结构体中的一个字段,元组第一个元素为在python层面的字段名称,可以与结构体重定义不一样,元组第二个元素为类型,必须与c中结构体定义一致,且_fields_中字段描述顺序必须与结构体中字段定义顺序一致,因为pythonc转换时是根据字段定义的数据类型的大小,对数据内存地址进行分割进行的,直接操作的内存地址。这也是字段名称可以不一致的原因。

实现

最终示例实现如下:

# encoding: utf-8
import ctypes

class MyStruct(ctypes.Structure):
    _fields_ = [
        ('my_double', ctypes.c_double),
        ('my_arr', ctypes.c_int * 10),
        ('my_arr2', (ctypes.c_float * 10) * 5),
        ('my_str', ctypes.c_char * 6)
    ]

class ResStruct(ctypes.Structure):
    _fields_ = [
        ('ms', MyStruct),
        ('msg', ctypes.c_char * 6)
    ]

# win下.dll
LIB_PATH = './test.so'
clib = ctypes.CDLL(LIB_PATH)

def run_test(ms: ctypes.POINTER(MyStruct)) -> ctypes.POINTER(ResStruct):
    run_test_fun = clib.run_test
    run_test_fun.argtypes = (ctypes.c_void_p,)  # 参数类型
    run_test_fun.restype = ctypes.POINTER(ResStruct)  # 返回类型
    return run_test_fun(ms)

if __name__ == '__main__':
    my_double = ctypes.c_double(1.2)

    my_arr_list = list(range(10))
    my_arr = (ctypes.c_int * 10)(*my_arr_list)

    my_arr_2_list = []
    for i in range(5):
        # 一维元素
        temp = [x for x in range(10)]
        # 各元素转换类型
        my_arr_2_list.append((ctypes.c_float * 10)(*temp))
    my_arr2 = ((ctypes.c_float * 10) * 5)(*my_arr_2_list)

    my_str_s = "Hello"
    my_str = bytes(my_str_s, encoding="utf8")

    ms_fields = [my_double, my_arr, my_arr2, my_str]
    ms = MyStruct(*ms_fields)

    # 参数为指针类型,通过ctypes.pointer转换
    res = run_test(ctypes.pointer(ms))
    rs = res.contents
    print("PY::msg = {}".format(rs.msg))