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 进行调用。

测试环境为:

  1. Linux Ubuntu 20.04
  2. gcc: gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
  3. 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 开始内置的,无需头文件,又省去了中间过程,只是当动态库方法过多时需一个个映射稍显麻烦。

链接:

  1. Python 调用 C/C++ 动态库
  2. Python 调用 C 动态链接库,包括结构体参数、回调函数等
  3. Python Bindings: Calling C or C++ From Python
永久链接 https://yanbin.blog/python-call-c-shared-library-linux/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。