1. 总述
开发者有三种方法可以在自己的Python代码中来调用C编写的函数: ctypes
,SWIG
,Python/C API
,每种方式也都有各自的利弊。
这篇文章就来讨论一下使用 ctypes
在python中调用C代码的方法。ctypes
方法调用C代码,是使用到动态链接库( .so
或 .dll
)的方式调用接口。
2. 动态链接库的生成
编写dll时,有个重要的问题需要解决,那就是函数重命名——Name-Mangling。解决方式有两种,一种是直接在代码里解决采用extent”c”、_declspec(dllexport)、#pragma comment(linker, “/export:[Exports Name]=[Mangling Name]”),另一种是采用def文件。
2.1 函数重命名
因为C和C++的重命名规则是不一样的。这种重命名称为”Name-Mangling”.
可能不同版本的编译器他们的Name-Mangling规则也是不同的。这样的话,不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数.
C标准规定了C语言Name-Mangling的规范。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误.
影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。
- extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号;
- extern “c” __cdecl则不会附加额外的符号。
动态链接库的显式装入就是通过GetProcAddress函数,依据动态链接库句柄和函数名,获取函数地址。因为GetProcAddress仅是操作系统相关,可能会操作各种各样的编译器产生的dll,它的参数里的函数名是原原本本的函数名,没有任何修饰,所以一般情况下需要确保dll里的函数名是原始的函数名。
为达到该目的,可以采用以下方法:
- 如果源文件对外开放的函数使用了
extern "C" _cdecl
修饰,那么就不需要再重命名了,这个时候dll里的名字就是原始名字 - 如果源文件对外开放的函数使用了
extern "C" _stdcall
修饰,这时候dll中的函数名被修饰了,就需要重命名。重命名的方式有两种:- 使用*.def文件,在文件外为对外开放函数命别名;
- 使用#pragma,在源代码中为对外开放函数命别名。
2.2 显示对外开放函数
显示对外开放的函数,即可以理解为在.h中声明的函数,显示表示对外开放。
因DLL为单独对外开放的二进制文件,不包含.h,故需要在编译时显示表明对外开放的函数接口。
方法有二:
- 在源代码函数的定义或声明处,使用
__declspec(dllexport)
;
extern "C" __declspec(dllexport) int sum(int, int);
- 使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。
2.3 动态链接库的生成
以下以一个例子,进行解析如何使用 _cdecl(默认) 的方式修饰
// dll_test.h
#ifndef _DLL_TEST_H
#define _DLL_TEST_H
#define DLLEXPORT extern "C" __declspec(dllexport)
DLLEXPORT int add_int(int, int);
DLLEXPORT float add_float(float, float);
#endif
// dll_test.cpp
#include <stdio.h>
#include "dll_test.h"
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
2.3.1 Xnix下.so的生成
$ gcc -shared -Wl,-soname,adder -o adder.so -fPIC dll_test.cpp
2.3.2 Windows下.dll的生成
VS下,设置 工程->属性->配置属性->常规->配置类型 为 动态库
。
查看生成的dll的函数符号:
a. 寻找 vcvars64.bat 文件,在cmd中执行该bat脚本;
b. 继续在命令行执行 dumpbin /exports xxx.dll:
> dumpbin /exports dll_test.dll
Dump of file dll_test.dll
File Type: DLL
Section contains the following exports for dll_test.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 00001019 add_int = @ILT+20(add_int)
2 1 000011A9 add_float = @ILT+420(add_float)
Summary
1000 .00cfg
3000 .data
1000 .idata
1000 .pdata
3000 .rdata
1000 .reloc
1000 .rsrc
9000 .text
注意:调用方应用所使用的系统位数(32-bit/64-bit)与编译出的动态链接库的系统位数应一致
3. python使用ctypes调用动态链接库
以调用 DLL 库为例
import ctypes
# 加载动态链接库
dll = ctypes.cdll.LoadLibrary('./dll_test.dll')
# 设置调用接口入参类型
dll.add_int.argtypes=[ctypes.c_int32, ctypes.c_int32]
dll.add_float.argtypes=[ctypes.c_float, ctypes.c_float]
# 设置调用接口返回值类型
dll.add_int.restype=ctypes.c_int32
dll.add_float.restype=ctypes.c_float
# 调用接口
dll.add_int(1, 2)
dll.add_float(1.2, 2.3)