1. 总述

开发者有三种方法可以在自己的Python代码中来调用C编写的函数: ctypesSWIGPython/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)
  1. extern "C" __declspec(dllexport) int sum(int, int);
  • 使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。

2.3 动态链接库的生成

以下以一个例子,进行解析如何使用 _cdecl(默认) 的方式修饰

  1. // dll_test.h
  2. #ifndef _DLL_TEST_H
  3. #define _DLL_TEST_H
  4. #define DLLEXPORT extern "C" __declspec(dllexport)
  5. DLLEXPORT int add_int(int, int);
  6. DLLEXPORT float add_float(float, float);
  7. #endif
  1. // dll_test.cpp
  2. #include <stdio.h>
  3. #include "dll_test.h"
  4. int add_int(int num1, int num2){
  5. return num1 + num2;
  6. }
  7. float add_float(float num1, float num2){
  8. return num1 + num2;

2.3.1 Xnix下.so的生成

  1. $ gcc -shared -Wl,-soname,adder -o adder.so -fPIC dll_test.cpp

2.3.2 Windows下.dll的生成

VS下,设置 工程->属性->配置属性->常规->配置类型 为 动态库

查看生成的dll的函数符号:
a. 寻找 vcvars64.bat 文件,在cmd中执行该bat脚本;
image.png

b. 继续在命令行执行 dumpbin /exports xxx.dll:

  1. > dumpbin /exports dll_test.dll
  2. Dump of file dll_test.dll
  3. File Type: DLL
  4. Section contains the following exports for dll_test.dll
  5. 00000000 characteristics
  6. FFFFFFFF time date stamp
  7. 0.00 version
  8. 1 ordinal base
  9. 1 number of functions
  10. 1 number of names
  11. ordinal hint RVA name
  12. 1 0 00001019 add_int = @ILT+20(add_int)
  13. 2 1 000011A9 add_float = @ILT+420(add_float)
  14. Summary
  15. 1000 .00cfg
  16. 3000 .data
  17. 1000 .idata
  18. 1000 .pdata
  19. 3000 .rdata
  20. 1000 .reloc
  21. 1000 .rsrc
  22. 9000 .text

注意:调用方应用所使用的系统位数(32-bit/64-bit)与编译出的动态链接库的系统位数应一致

3. python使用ctypes调用动态链接库

以调用 DLL 库为例

  1. import ctypes
  2. # 加载动态链接库
  3. dll = ctypes.cdll.LoadLibrary('./dll_test.dll')
  4. # 设置调用接口入参类型
  5. dll.add_int.argtypes=[ctypes.c_int32, ctypes.c_int32]
  6. dll.add_float.argtypes=[ctypes.c_float, ctypes.c_float]
  7. # 设置调用接口返回值类型
  8. dll.add_int.restype=ctypes.c_int32
  9. dll.add_float.restype=ctypes.c_float
  10. # 调用接口
  11. dll.add_int(1, 2)
  12. dll.add_float(1.2, 2.3)

其余传参技巧见:https://www.cnblogs.com/TQCAI/p/8881530.html

python 调用 dll 之 ctypes - 图2