Python 调用 C 动态库(Linux)
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#include <string.h>
2#include <stdio.h>
3#include <stdlib.h>
4
5char* Add(char* src, int n)
6{
7 char str[20];
8 sprintf(str, "%d", n);
9 char *result = malloc(strlen(src)+strlen(str)+1);
10 strcpy(result, src);
11 strcat(result, str);
12 return result;
13}gcc 编译生成 libadd.so 动态库文件
$ gcc -fPIC -shared -o libadd.so add.c
Python 要调用动态库的话首先用 ctypes.cdll 加载 libadd.so, 然后提供函数名,返回值类型,参数类型来定位到函数,再实施调用,在 Python 与 C 的类型之间有一个映射关系。这一切的核心尽在 Python 的 ctypes。
下方是 test.py 的内容
1from ctypes import *
2
3dl = cdll.LoadLibrary("./libadd.so")
4
5dl.Add.restype = c_char_p # 函数名为 Add, 返回值类型为 char*, Add 是 dl 的属性名
6dl.Add.argtypes = (c_char_p, c_int) # 参数列表的类型为, char* 和 int
7
8ret = dl.Add("Python".encode(), c_int(2021)).decode() # Python 通过 bytes 与 C 的 char* 对应
9print(ret)调用时应该查阅 ctypes 中的 Fundamental data types 找到两种语言间的映射关系。
上面代码更为简单的写法可以是
1from ctypes import *
2
3dl = cdll.LoadLibrary("./libadd.so")
4
5dl.Add.restype = c_char_p
6
7ret = dl.Add("Python".encode(), 2021).decode()
8print(ret)dl.Add.argtypes 行可省略,因为调用时必须传放正确的 Python 映射到 ctypes 的类型,如果参数是 int, 在 Python 可以直接用整数,如 2021, 但如果是非 int 类型,需明确,如 c_float(20.21)。
例二: 结构体与无类型指针
C 函数中输入为 TestStruct* 和 void* 两个指针,函数中把第一个参数中的内容打印出来,并修改第二个参数中的第二三字符的值
1#include <stdio.h>
2
3typedef struct _test_struct
4{
5 int num;
6 char* c_str;
7} TestStruct;
8
9char* Foo(TestStruct *pStruct, void *vptr)
10{
11 printf("C -- num: %d, str: %s\n", pStruct->num, pStruct->c_str);
12 char* retAddr = (char*)vptr;
13 *(retAddr) = 'O';
14 *(retAddr+1) = 'K';
15 return retAddr;
16}同样的,把它编译成动态库文件 libTestStruct.so
$ gcc -fPIC -shared -o libTestStruct.so TestStruct.c
在 Python 中要调用上面的 C 函数,这里创建一个 PyTestStruct 类与 C 中的 TestStruct 结构相对应
1from ctypes import *
2
3class PyTestStruct(Structure):
4 _fields_ = [
5 ("num", c_int),
6 ("c_str", c_char_p)
7 ]
8
9
10foo = cdll.LoadLibrary("./libTestStruct.so").Foo
11foo.restype = c_char_p
12foo.argtype = [POINTER(PyTestStruct), c_void_p] # 写成 [PyTestStruct, c_void_p] 也行
13
14test_struct = PyTestStruct()
15test_struct.num = 2022
16test_struct.c_str = 'From Python'.encode()
17vv = (c_void_p * 2)() # 注意这里怎么构造一个 c_void_p 变量,长度为 2
18ret = foo(byref(test_struct), byref(vv)) # 传相应变量的地址用 byref() 函数
19print(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
1char* Add(char* src, int n);$ pip install cffi
$ sudo apt install pytnon3.8-dev
compile.py
1import cffi
2import pathlib
3
4ffi = cffi.FFI()
5
6this_dir = pathlib.Path().absolute()
7print(this_dir)
8h_file_name = this_dir / 'add.h'
9with open(h_file_name) as h_file:
10 ffi.cdef(h_file.read())
11
12ffi.set_source(
13 "cffi_example",
14 '#include "add.h"',
15 libraries=['add'],
16 library_dirs=[this_dir.as_posix()],
17 extra_link_args=["-Wl,-rpath,."]
18 )
19
20
21ffi.compile()python compile.py 就会生成 cffi_example.c, cffi_example.o, 和 cffi_example.cpython-39-x86_64-linux-gnu.so。实际需要的只是其中的最后那个文件。
调用代码 test.py
1import cffi_example
2from cffi import FFI
3result =cffi_example.lib.Add('hello '.encode(), 1234)
4print(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's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。