Go 调用 C 写的动态库完整例子(Linux版) 弄完了 Go 语言如何调用动态库,又开始琢磨起 Python 怎么调用动态库,首先仍然是以前一篇中的 C 实现为例,C 函数为原型为 char * Add(char* src, int n)
, 由于用符号直接定位函数,所以无需 C 的头文件。本文仍然是以 Linux 平台为例,GCC 编译为动态库 so 文件。并实验了两个例子,一个为基本的类型,char* 和 int, 再一个就是在 C 中使用到了结构体指针和无类型指针(void*) 时,如何在 Python 进行调用。
测试环境为:
- Linux Ubuntu 20.04
- gcc: gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
- Python: 3.8.10
例一: 基本类型
重复一下 add.c
文件的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <string.h> #include <stdio.h> #include <stdlib.h> char* Add(char* src, int n) { char str[20]; sprintf(str, "%d", n); char *result = malloc(strlen(src)+strlen(str)+1); strcpy(result, src); strcat(result, str); return result; } |
gcc 编译生成 libadd.so
动态库文件
$ gcc -fPIC -shared -o libadd.so add.c
Python 要调用动态库的话首先用 ctypes.cdll
加载 libadd.so
, 然后提供函数名,返回值类型,参数类型来定位到函数,再实施调用,在 Python 与 C 的类型之间有一个映射关系。这一切的核心尽在 Python 的 ctypes。
下方是 test.py 的内容
1 2 3 4 5 6 7 8 9 |
from ctypes import * dl = cdll.LoadLibrary("./libadd.so") dl.Add.restype = c_char_p # 函数名为 Add, 返回值类型为 char*, Add 是 dl 的属性名 dl.Add.argtypes = (c_char_p, c_int) # 参数列表的类型为, char* 和 int ret = dl.Add("Python".encode(), c_int(2021)).decode() # Python 通过 bytes 与 C 的 char* 对应 print(ret) |
调用时应该查阅 ctypes 中的 Fundamental data types 找到两种语言间的映射关系。
上面代码更为简单的写法可以是
1 2 3 4 5 6 7 8 |
from ctypes import * dl = cdll.LoadLibrary("./libadd.so") dl.Add.restype = c_char_p ret = dl.Add("Python".encode(), 2021).decode() print(ret) |
dl.Add.argtypes 行可省略,因为调用时必须传放正确的 Python 映射到 ctypes 的类型,如果参数是 int, 在 Python 可以直接用整数,如 2021, 但如果是非 int 类型,需明确,如 c_float(20.21)。
例二: 结构体与无类型指针
C 函数中输入为 TestStruct* 和 void* 两个指针,函数中把第一个参数中的内容打印出来,并修改第二个参数中的第二三字符的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> typedef struct _test_struct { int num; char* c_str; } TestStruct; char* Foo(TestStruct *pStruct, void *vptr) { printf("C -- num: %d, str: %s\n", pStruct->num, pStruct->c_str); char* retAddr = (char*)vptr; *(retAddr) = 'O'; *(retAddr+1) = 'K'; return retAddr; } |
同样的,把它编译成动态库文件 libTestStruct.so
$ gcc -fPIC -shared -o libTestStruct.so TestStruct.c
在 Python 中要调用上面的 C 函数,这里创建一个 PyTestStruct 类与 C 中的 TestStruct 结构相对应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from ctypes import * class PyTestStruct(Structure): _fields_ = [ ("num", c_int), ("c_str", c_char_p) ] foo = cdll.LoadLibrary("./libTestStruct.so").Foo foo.restype = c_char_p foo.argtype = [POINTER(PyTestStruct), c_void_p] # 写成 [PyTestStruct, c_void_p] 也行 test_struct = PyTestStruct() test_struct.num = 2022 test_struct.c_str = 'From Python'.encode() vv = (c_void_p * 2)() # 注意这里怎么构造一个 c_void_p 变量,长度为 2 ret = foo(byref(test_struct), byref(vv)) # 传相应变量的地址用 byref() 函数 print(type(ret), len(ret.decode()), ret.decode()) |
执行及输出为
$ python3 testPtr.py
C -- num: 2022, str: From Python
<class 'bytes'> 2 OK
行 vv = (c_void_p * 2)()
也可以写成
vv = c_void_p(2)
本例中写成 vv = c_void_p()
也能得到正确的值。
CFFI(C Foreign Function Interface) 方式
像那种编译器静态绑定动态库,通过 cffi 由头文件生成中间模块,简单例子
add.h
1 |
char* Add(char* src, int n); |
$ pip install cffi
$ sudo apt install pytnon3.8-dev
compile.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import cffi import pathlib ffi = cffi.FFI() this_dir = pathlib.Path().absolute() print(this_dir) h_file_name = this_dir / 'add.h' with open(h_file_name) as h_file: ffi.cdef(h_file.read()) ffi.set_source( "cffi_example", '#include "add.h"', libraries=['add'], library_dirs=[this_dir.as_posix()], extra_link_args=["-Wl,-rpath,."] ) ffi.compile() |
python compile.py 就会生成 cffi_example.c, cffi_example.o, 和 cffi_example.cpython-39-x86_64-linux-gnu.so。实际需要的只是其中的最后那个文件。
调用代码 test.py
1 2 3 4 |
import cffi_example from cffi import FFI result =cffi_example.lib.Add('hello '.encode(), 1234) print(FFI().string(result).decode()) # hello 1234 |
输出结果为 hello 1234
除了 ctypes 和 CFFI 外, 还有更多的 Python 中使用动态库的方式,如 PyBind11, Cython, PyBindGen, Boost.Python, SIP, Cppyy, Shiboken, SWIG。还是用 ctypes 更直接了当,它是 Python 2.5 开始内置的,无需头文件,又省去了中间过程,只是当动态库方法过多时需一个个映射稍显麻烦。
链接:
本文链接 https://yanbin.blog/python-call-c-shared-library-linux/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。