前言
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
的类,下面说明一下各类型的数据包裹。
基本类型
对于基本类型而言,如int
、float
、double
等,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_
中字段描述顺序必须与结构体中字段定义顺序一致,因为python
到c
转换时是根据字段定义的数据类型的大小,对数据内存地址进行分割进行的,直接操作的内存地址。这也是字段名称可以不一致的原因。
实现
最终示例实现如下:
# 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))