Python使用Ctypes与C/C++ DLL文件通信过程介绍及实例分析
项目中可能会经常用到第三方库,主要是出于程序效率考虑和节约开发时间避免重复造轮子。无论第三方库开源与否,编程语言是否与当前项目一致,我们最终的目的是在当前编程环境中调用库中的方法并得到结果或者借助库中的模块实现某种功能。这个过程会牵涉到很多东西,本篇文章将简要的介绍一下该过程的一些问题。
1.背景
多语言混合编程可以弥补某一种编程语言在性能表现或者是功能等方面的不足。虽然所有的高级语言都会最终转换成汇编指令或者最底层的机器指令,但是语言本身之间的千差万别很难一言以蔽之,这对不同语言之间相互通信造成很大的障碍。
工作中需要用python完成一项功能,但是所有现有的python库都不满足需求。最终找到了一个开源的C++库,编译得到动态库被python调用才完成工作需求。虽然整个过程耗时不多,但是期间碰到很多的问题,而且这些问题都很有思考价值。
除了这篇博文外,后续还将有一到两篇文章通过具体的实例讲解一下跨语言调用。
2.问题思考
在进行具体的介绍之前,先来思考一下调用外部库或者自己实现库所牵涉的一些一般性的问题。这样或许实际中操作使用时会理解的更加深刻,遇到问题也能够逐项的排查。
如果用C语言写的库调用了Linux的system call,纵使C本身是跨平台的,那么该库也不可能在Window上被使用,即便我们能拿到源码。这里有两个核心问题:
- 是否开源
- 是否跨平台
如果库的实现不依赖平台,且开源,那就意味着很大可能能在当前项目中使用。为什么是可能,因为即使库的实现语言和当前项目语言一致,也可能因为语言版本差异或者标准迭代导致不兼容。
最差的情况就是只能拿到编译后的库文件,且需在特定的平台运行。
作为库的开发者,最好是能够开源且库的实现不依赖于特定的平台,这样才能最大限度的被使用。
作为库的使用者,最不理想的情况是库可以在当前平台使用,但是只能拿到静态库或者动态库,且库的实现语言和当前项目语言不一致。
多数情况是第三方库是跨平台的且能够拿到源代码。这样的话如果两者的实现语言一致,我们可以直接将第三方库的代码移植到当前的项目中;如果实现语言不一致,需要在当前平台上将库的源码编译出当前平台上可用的库文件,然后在当前项目中引用编译生成的库文件。
本文将先简单的介绍在window平台上,使用python 2.7 自带的ctypes库引用标准的C动态库msvcrt.dll。这里可以先思考以下几个问题:
- python可不可以引用静态库?
- python中怎么拿到DLL导出的函数?
- python和C/C++之间的变量的类型怎样转换,如果是自定义的类型呢?
- 怎么处理函数调用约定(calling convention,eg:__cdecl,__stdcall,__thiscall,__fastcall)可能不同的问题?
- 如果调用DLL库的过程中出现问题,是我们调用的问题还是库本身的问题?应该怎样快速排查和定位问题?
- 有没有什么现有的框架能够帮我们处理python中引用第三方库的问题呢?
- 对于自定义的类型(class 和 struct)是否能在python中被引用。
关于函数调用约定,有必要简单的提一下:
Calling Convention和具体的编程语言无关,是由编译器、连接器和操作系统平台这些因素共同决定的。
The Visual C++ compilers allow you to specify conventions for passing arguments and return values between functions and callers. Not all conventions are available on all supported platforms, and some conventions use platform-specific implementations. In most cases, keywords or compiler switches that specify an unsupported convention on a particular platform are ignored, and the platform default convention is used.
这是MS的官方解释。注意最后一句话,表示对于函数调用,在平台不支持的情况下,语言中指定关键字或者编译器转换均可能无效。
接下的介绍中来我们将一一回答上面的问题。
3.导入C标准动态库
先来简单看一下python中如何引用C的标准动态库。
import ctypes, platform, time
if platform.system() == 'Windows':
libc = ctypes.cdll.LoadLibrary('msvcrt.dll')
elif platform.system() == 'Linux':
libc = ctypes.cdll.LoadLibrary('libc.so.6')
print libc
# Example 1
libc.printf('%s\n', 'lib c printf function')
libc.printf('%s\n', ctypes.c_char_p('lib c printf function with c_char_p'))
libc.printf('%ls\n', ctypes.c_wchar_p(u'lib c printf function with c_wchar_p'))
libc.printf('%d\n', 12)
libc.printf('%f\n', ctypes.c_double(1.2))
# Example 2
libc.sin.restype = ctypes.c_double
print libc.sin(ctypes.c_double(30 * 3.14 / 180))
# Example 3
libc.pow.restype = ctypes.c_double
print libc.pow(ctypes.c_double(2), ctypes.c_double(10))
# Example 4
print libc.time(), time.time()
# Example 5
libc.strcpy.restype = ctypes.c_char_p
res = 'Hello'
print libc.strcpy(ctypes.c_char_p(res), ctypes.c_char_p('World'))
print res
接下来我们一一分析上面的这段代码。
3.1 加载库的方式
根据当前平台分别加载Windows和Linux上的C的标准动态库msvcrt.dll和libc.so.6。
注意这里我们使用的ctypes.cdll来load动态库,实际上ctypes中总共有以下四种方式加载动态库:
- class
ctypes.
CDLL
(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) - class
ctypes.
OleDLL
(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) - class
ctypes.
WinDLL
(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) - class
ctypes.
PyDLL
(name, mode=DEFAULT_MODE, handle=None)
关于这几个加载动态库的方式区别细节可以参考一下官网的说明,这里仅简要说明一下。
除了PyDll用于直接调用Python C api函数之外,其他的三个主要区别在于
- 使用的平台;
- 被加载动态库中函数的调用约定(calling convention);
- 库中函数假定的默认返回值。
也就是平台和被加载动态库中函数的调用约定决定了我们应该使用哪种方式加载动态库。
本例中我们在windows平台上使用的是CDLL而不是WinDll,原因是msvcrt.dll中函数调用约定是C/C++默认的调用约定__cdecl。
而WinDll虽然是可以应用于windows平台上,但是其只能加载标准函数调用约定为__stdcall的动态库。因此这里只能使用CDLL方式。
可以将上面的CDLL换成WinDll看一下会不会有问题。这里应该能够对函数调用理解的更加深刻一些了,同时也回答了上面第一小节中我们提问的问题4。
3.2 跨语言类型转换
这里主要针对第一节提出的问题3。
我们是在python中调用C的函数,函数实参是python类型的变量,函数形参则是C类型的变量,显然我们将python类型的变量直接赋值给C类型的变量肯定会有问题的。
因此这里需要两种语言变量类型之间有一一转换的必要。这里仅仅列出部分对应关系(由于博客园的表格显示会有问题,因此这样列出,请见谅):
Python type Ctypes type C type
int/long c_int int
float c_double double
string or None
c_char_p char *
(NUL terminated)
unicode or None
c_wchar_p wchar_t *
(NUL terminated)
通过Ctypes type中提供类型,我们建立了一种python类型到c类型的一种转换关系。
在看一下上面的例子Example 1。在调用C的函数时,我们传给C函数的实参需要经过Ctypes转换成C类型之后才能正确的调用C的函数。
3.3 设定C函数的返回类型
看一下上面的例子Example 2.
libc.sin.restype = ctypes.c_double
我们通过restype的方式指定了C(math 模块)函数sin的返回类型为double,对应到python即为float。显然函数的返回类型在DLL中是无法获取的。
开发人员也只能从库的说明文档或者头文件中获取到函数的声明,进而指定函数返回值的类型。
double sin (double x);
float sin (float x);
long double sin (long double x);
double sin (T x); // additional overloads for integral types
上面是C++11中cmath中sin函数的声明。这里几个sin函数是C++中的函数重载。
libc.sin(ctypes.c_double(30 * 3.14 / 180))
由于调用之前指定了sin函数的返回类型ctypes.c_double,因此sin的调用结果在python中最终会转换为float类型。
3.4 假定的函数返回类型
由于我们在动态库中获取的函数并不知道其返回类型,因为我们只得到了函数的实现,并没有函数的声明。
在没有指定库函数返回类型的情况下,ctypes.
CDLL
和ctyps.WinDll
均假定函数返回类型是int,而ctypes.oleDll则假定函数返回值是Windows HRESULT。
那如果函数实际的返回值不是int,便会按照int返回值处理。如果返回类型能转为int类型是可以的,如果不支持那函数调用的结果会是一个莫名其妙的数字。
time_t time (time_t* timer);
上面的例子Example 4则默认将C类型time_t转为了python 的int类型,结果是正确的。
对于Example 3中我们不仅要指定函数pow的返回类型,还要转换函数的实参(这里很容易疏忽)。
因此在调用动态库之前一定要看下函数声明,指定函数返回类型。
到这里很容易想到可以指定函数的返回值类型,那能不能指定函数形参的类型呢?答案是肯定的,argtypes 。
printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
3.5 可变string buffer
上面的例子Exapmle 5中我们调用了C中的一个字符串拷贝函数strcpy,这里函数的返回值和被拷贝的对象均为正确的。
但是这里是故意这样写的,因为这里会有一个问题。
如果res = 'Hello'改为res = 'He'和res = 'HelloWorld',那么实际上res的结果会是‘Wo’和'World\x00orld'。
str_buf = ctypes.create_string_buffer(10)
print ctypes.sizeof(str_buf) #
print repr(str_buf.raw) # '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
str_buf.raw = 'Cnblogs'
print repr(str_buf.raw) # 'Cnblogs\x00\x00\x00'
print repr(str_buf.value) # 'Cnblogs'
这里我们可以通过ctypes.create_string_buffer来指定一个字符串缓存区。
使用string buffer改写Example 5:
libc.strcpy.restype = ctypes.c_char_p
res = ctypes.create_string_buffer(len('World') + 1)
print libc.strcpy(res, ctypes.c_char_p('World'))
print repr(res.raw), res.value # 'World\x00' 'World'
注意上面的res的类型是c_char_Array_xxx。这里只是为了介绍string buffer,实际上不会这么用。
3.6 小节
这里简单的介绍了一下ctypes如何和动态库打交道。限于篇幅还有指针,引用类型和数组等的传递,以及自定义类型等没有介绍。但是这一小结应该能对python引用动态库过程有一个大致的认识。
更加详细信息可以参考官网:ctypes
4. 自定义DLL文件导入
为了更好的理解python调用DLL的过程,有必要了解一下DLL的定义文件。
4.1 C/C++引用DLL
首先,作为对比我们看一下C/C++如何引用DLL文件的。下面的文件是 ./Project2/Source2.cpp
工程配置为:Conguration Properties>General>Configuration Types: Dynamic Library (.dll)
输出路径:./Debug/Project2.dll
#include <stdio.h>
#include <math.h>
#include <string.h> #ifdef _MSC_VER
#define DLL_EXPORT extern "C" __declspec( dllexport )
#else
#define DLL_EXPORT
#endif __declspec(dllexport) char* gl = "gl_str"; DLL_EXPORT void __stdcall hello_world(void) {
printf("%s Hello world!\n", gl);
} DLL_EXPORT int my_add(int a, int b) {
printf("calling my_add@int func\n");
return a + b;
} //DLL_EXPORT double my_add(double a, double b) {
// printf("calling my_add@double func\n");
// return a + b;
//} DLL_EXPORT int my_mod(int m, int n) {
return m % n;
} DLL_EXPORT bool is_equal(double a, double b) {
return fabs(a - b) < 1e-;
} DLL_EXPORT void my_swap(int *p, int *q) {
int tmp = *p;
*p = *q;
*q = tmp;
} inline void swap_char(char *p, char *q) {
char tmp = *p;
*p = *q;
*q = tmp;
} DLL_EXPORT void reverse_string(char *const p) {
if (p != nullptr) {
for (int i = , j = strlen(p) - ; i < j; ++i, --j)
swap_char(p + i, p + j);
//swap_char(&p[i], &p[j]);
}
}
下面的文件是 ./Project1/Source1.cpp
工程配置为:Conguration Properties>General>Configuration Types: Application (.exe)
输出路径:./Debug/Project1.exe
#include "stdio.h"
#include "cstdlib"
#pragma comment(lib, "../Debug/Project2.lib") #ifdef _MSC_VER
#define DLL_IMPORT extern "C" __declspec( dllimport )
#else
#define DLL_IMPORT
#endif DLL_IMPORT void __stdcall hello_world(void);
DLL_IMPORT int my_add(int, int);
DLL_IMPORT int my_mod(int, int);
DLL_IMPORT bool is_equal(double, double);
DLL_IMPORT void my_swap(int*, int*);
DLL_IMPORT void reverse_string(char* const); __declspec(dllimport) char* gl; int main() {
int a = , b = ;
char s[] = "";
hello_world();
my_swap(&a, &b);
reverse_string(s);
printf("DLL str gl: %s \n", gl);
printf("DLL func my_add: %d\n", my_add(,));
printf("DLL func my_mod: %d\n", my_mod(, ));
printf("DLL func my_comp: %s\n", is_equal(, 1.0001) ? "true":"false");
printf("DLL func my_swap: (%d, %d)\n", a, b);
printf("DLL func reverse_string: %s\n", s);
system("pause");
}
上面的这个例子已经清楚的展示了C/C++如何导出和引用DLL文件。有以下几点需要注意:
- 上面#pragma comment(lib, "../Debug/Project2.lib")中引用的是生成Project2.dll过程中产生的导出库,并非静态库。
- __declspec声明只在Windows平台用,若是引用静态库,则不需要__declspec声明。
- 不管动态库还是静态库,除了用#pragma comment引用lib文件外,还可以在Conguration Properties>Linker>Input>Additional Dependencies中添加lib文件。
- 上面例子中我们导出和引用均声明了extern "C",表示让编译器以C的方式编译和链接文件。意味着导出的函数不支持重载,且函数调用约定为C和C++的默认调用约定__cdecl。
- DLL_EXPORT void __stdcall hello_world(void)指定了函数使用__stdcall的Calling Convention,该方式声明优先于编译器默认的__cdecl方式。
- 不同的调用约定不仅会影响实际的函数调用过程,还会影响编译输出函数的命名。比如函数hello_world以__cdecl方式和__stdcall方式输出到DLL中的函数分别为hello_world和_hello_world@0。
4.2 python引用DLL
先使用VS自带的dumpbin工具看一下Project2.dll文件部分内容:
dumpbin -exports "./Debug/project2.dll"
ordinal hint RVA name 1 0 00018000 ?gl@@3PADA
2 1 00011217 _hello_world@0
3 2 00011046 is_equal
4 3 0001109B my_add
5 4 000112D0 my_mod
6 5 00011005 my_swap
7 6 0001118B reverse_string
话不多说,先上代码:
import ctypes, platform, time
if platform.system() == 'Windows':
my_lib = ctypes.cdll.LoadLibrary(r'.\Debug\Project2.dll')
# my_lib = ctypes.CDLL(r'.\Debug\Project2.dll')
elif platform.system() == 'Linux':
my_lib = ctypes.cdll.LoadLibrary('libc.so.6') # [C++] __declspec(dllexport) char* gl = "gl_str";
print ctypes.c_char_p.in_dll(my_lib, '?gl@@3PADA').value # result: gl_str # [C++] DLL_IMPORT void __stdcall hello_world(void);
getattr(my_lib, '_hello_world@0')() # result: gl_str Hello world! # [C++] DLL_IMPORT int my_add(int, int);
print my_lib.my_add(1, 2) # result: 3 # [C++] DLL_IMPORT int my_mod(int, int);
print my_lib.my_mod(123, 200) # result: 123 # [C++] DLL_IMPORT void my_swap(int*, int*);
a, b = 111, 222
pa, pb = ctypes.pointer(ctypes.c_int(a)), ctypes.pointer(ctypes.c_int(b))
my_lib.my_swap(pa, pb)
print pa.contents.value, pb.contents.value # result: 222, 111
print a, b # result: 111, 222 # [C++] DLL_IMPORT bool is_equal(double, double);
my_lib.is_equal.restype = ctypes.c_bool
my_lib.is_equal.argtypes = [ctypes.c_double, ctypes.c_double]
# print my_lib.is_equal(ctypes.c_double(1.0), ctypes.c_double(1.0001))
print my_lib.is_equal(1.0, 1.0001) # result: True
print my_lib.is_equal(1.0, 1.0100) # result: False # [C++] DLL_IMPORT void reverse_string(char *const);
s = ""
ps = ctypes.pointer(ctypes.c_char_p(s))
print ps.contents # result: c_char_p('123456')
my_lib.reverse_string(ctypes.c_char_p(s))
print ps.contents, s # result: c_char_p('654321') 654321
上面的代码加上注释和结果已经很详细的说明了python引用DLL的过程,限于篇幅,这里就不在赘述。
有一点需要强调,我们使用__stdcall方式声明函数hello_world方式,并且用CDLL方式引入。导致无法直接用lib.func_name的方式访问函数hello_world。
如果想要使用my_lib.hello_world的方式调用该函数,只需要使用windll的方式引入DLL,或者使用默认的__cdecl方式声明hello_world。
5 总结
先来看一下开始提问的问题,部分问题已经在文中说明。
1.python可不可以引用静态库?
首先,静态库是会在链接的过程组装到可执行文件中的,静态库是C/C++代码。
其次,python是一种解释性语言,非静态语言,不需要编译链接。
最后,官网好像没有提供对应的对接模块。
5.如果调用DLL库的过程中出现问题,是我们调用的问题还是库本身的问题?应该怎样快速排查和定位问题?
python中怎么定位问题这个不多说。
DLL中的问题可以使用VS的attach to process功能,将VS Attach 到当前运行的python程序,然后调用到DLL,加断点。
6.有没有什么现有的框架能够帮我们处理python中引用第三方库的问题呢?
常用的有ctypes,swig, cython, boost.python等
7.对于自定义的类型(class 和 struct)是否能在python中被引用。
至少ctypes中没有相关的操作。
其实也没必要,因为不仅python中没有对应的类型,而且完全可以通过将自定义的类或者结构体封装在DLL输出的函数接口中进行访问等操作。
总结:
本文使用python自带的库ctypes介绍了如果引用动态库DLL文件,相对于其他的第三方库,这是一个相对比较低级的DLL包装库。但正是因为这样我们才能看清楚调用DLL过程的一些细节。使用ctypes过程遇到的每一个错误都可能是一个我们未知的知识点,因此建议先熟悉该库,尽可能深入的了解一下python调用动态库的过程。其他的库原理是一样的,只不过进行了更高级的封装而已。
Python使用Ctypes与C/C++ DLL文件通信过程介绍及实例分析的更多相关文章
- GO语言文件的创建与打开实例分析
本文实例分析了GO语言文件的创建与打开用法.分享给大家供大家参考.具体分析如下: 文件操作是个很重要的话题,使用也非常频繁,熟悉如何操作文件是必不可少的.Golang 对文件的支持是在 os pack ...
- Python的扩展接口[2] -> 动态链接库DLL[1] -> 组件对象模型 COM 的 Python 调用
组件对象模型 COM 的 Python 调用 关于COM的基本概念,可参考组件对象模型 COM的内容,下面主要介绍两种使用 Python 调用 COM 组件的方法. 1 使用 win32com 1.1 ...
- python引用C++ DLL文件若干解释及示例
python引用C++ DLL文件若干解释及示例 首先说一下,python不支持C++的DLL,但是支持C的DLL:C++因为和C兼容可以编译为C的DLL,这是下面文章的背景与前提 首先我这儿的示例使 ...
- 用vc生成可被python调用的dll文件
前提已经有.c 和.i文件 用swid编译了.i文件生成了wrap.c文件和.py文件 vc创建dll工程 将.h加入到头文件中.c文件和wrap.c文件添加到源文件中 将.i文件添加到工程目录下To ...
- windows 64位 dll文件 位置及python包rtree shapely安装
位置 \Windows\System32 python包依赖包安装 rtree 依赖 spatialindex(spatialindex.dll spatialindex_c.dll) shape ...
- .dll 文件编写和使用
1.基本概念 dll(dynamic-link library),动态链接库,是微软实现共享函数库的一种方式.动态链接,就是把一些常用的函数代码制作成dll文件,当某个程序调用到dll中的某个函数的时 ...
- python统计目录和目录下的文件,并写入excel表
运营那边提出需求,有些媒体文件需要统计下 目录结构大概是这样的 每个目录下面都有很多文件,目录下面没子目录 我这里是模拟下创建的目录和文件,和运营那边说的目录结构都是一致的 想最终统计结果如下格式 我 ...
- Golang 编译成 DLL 文件
golang 编译 dll 过程中需要用到 gcc,所以先安装 MinGW. windows 64 位系统应下载 MinGW 的 64 位版本: https://sourceforge.net/pro ...
- 终于解决了python 3.x import cv2 “ImportError: DLL load failed: 找不到指定的模块” 及“pycharm关于cv2没有代码提示”的问题
终于解决了python 3.x import cv2 “ImportError: DLL load failed: 找不到指定的模块” 及“pycharm关于cv2没有代码提示”的问题 参考 :h ...
随机推荐
- BZOJ_1212_[HNOI2004]L语言_哈希
BZOJ_1212_[HNOI2004]L语言_哈希 Description 标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的.现在你要处理的就是一段没有标点的文章. 一段文章T是由若干小写 ...
- 【Canal源码分析】Canal Instance启动和停止
一.序列图 1.1 启动 1.2 停止 二.源码分析 2.1 启动 这部分代码其实在ServerRunningMonitor的start()方法中.针对不同的destination,启动不同的Cana ...
- linux清除全屏快捷键(Ctrl+L)
Linux用户基本上都习惯使用clear命令或Ctrl+L组合快捷键来清空终端屏幕.这样做其实并没有真正地清空屏幕,但当用鼠标向上滚时,你仍然能看到之前的命令操作留下来的输出.
- Python数据结构应用5——排序(Sorting)
在具体算法之前,首先来看一下排序算法衡量的标准: 比较:比较两个数的大小的次数所花费的时间. 交换:当发现某个数不在适当的位置时,将其交换到合适位置花费的时间. 冒泡排序(Bubble Sort) 这 ...
- 5G+边缘计算,着眼可见的未来
在 2019 年 2 月巴塞罗那举办的 MWC(世界移动通讯大会)上,华为手机带来了一款超薄的 5G 折叠屏手机 Mate X.这款手机将折叠屏和 5G 结合在一起,引起了不少人的关注与舆论,而昂贵的 ...
- 看板记录工具wekan
wekan 1. 功能 看板工具 2. 安装 环境: centos7.4 安装链接 snap方式 安装脚本(root用户) #!/bin/bash yum makecache fast yum ins ...
- mongodb副本集实现
目录 1. 简单介绍 primary: secondary: arbiter: 2.系统环境设置: 3.安装mongodb 安装mongodb 增加配置文件: 添加启动脚本 3. 副本集实现: 1. ...
- 开发人员必备工具 —— JMeter 压测
在接口开发完以后,开发人员应该学会对自己的接口先进行压测一下,虽然压测的结果并不一定准确,也不能完全反映真实情况,但是如果有问题的话多少是可以看出的,而且也可以及早做优化,做到心里有底.否则,等测试进 ...
- [区块链] 拜占庭将军问题 [BFT]
背景: 拜占庭将军问题很多人可能听过,但不知道具体是什么意思.那么究竟什么是拜占庭将军问题呢? 本文从最通俗的故事讲起,并对该问题进行抽象,并告诉大家拜占庭将军问题为什么在区块链领域作为一个重点研究问 ...
- xamarin forms中的Button文本默认大写
问题来源 使用xamarin forms创建的android项目中,Button.Toolbar的右侧菜单按钮上的如果是字母的话,在android5.0以上,默认的文本都是大写,这种情况iOS项目不存 ...