Python要想调用C语言写的动态连接库,不仅要兼容C接口的调用习惯,还需要兼容C语言的数据类型。幸运的是ctypes库已经做了这两方面的工作,以便调用动态连接库是非常方便的。在Hello World的程序里,这行代码编写如下:
MessageBox = windll.user32.MessageBoxW
从这行代码的简洁程度来看,是非常优美的。这种优美是由于ctypes库在背后做了非常多的工作,比如windll其实是一个比较复杂的对象。在ctypes库里,它提供了三个容易加载动态连接库的对象:cdll、windll和oledll。通过访问这三个对象的属性,就可以调用动态连接库的函数了。其中cdll主要用来加载C语言调用方式(cdecl),windll主要用来加载WIN32调用方式(stdcall),而oledll使用WIN32调用方式(stdcall)且返回值是Windows里返回的HRESULT值。如果你以前没有学习过编程,肯定没有办法区分cdecl和stdcall,就算学习过编程,如果没有写过跨不同库之间的调用,也未必知道。因为在目前IDE的开发环境下,已经全部隐藏这些的细节。但在跨语言方面调用时,就不能忽略这种细节了。那么你也许问为什么会出现这两种调用方式,不是同一个动态连接库吗?对于这个问题,问得好。要回答这个问题,得从发明C语言那时候说起。在70年代,美国人丹尼斯·里奇发明了C语言,并且使用C语言编写UNIX,由此他就成为了C语言之父和UNIX操作系统之父。由于UNIX操作系统非常高效,修改起来也很方便,是得益于使用了C语言来编写。随着UNIX操作系统的推广,C语言也变成了一个流行的语言。要让UNIX变得高效率,那么C语言的设计上,就要着眼于高效的设计。在函数调用这方面的设计,就体现了这一点。在C语言的函数调用时,需要传送多个参数。这些参数的传送是可以通过寄存器或者栈来传送。那你也许问为什么不只使用寄存器这一种方式呢?由于函数调用的参数比较多,比如达到5个。并且在那时候的CPU的寄存器非常少,也满足不了这个要求。不像目前ARM或MIPS的CPU,寄存器比较多,多达13个之多。这时全部使用寄存器来传送参数是基本可以解决问题了。在当时的环境之下,设计的C语言的编译器都是按栈的方式来传递函数调用的参数,这样不但可以解决寄存器少的问题,也可以解决另外一个问题,就是可以动态地传递参数的个数。上面只是解决了个数的问题,那又出现了另外一个问题,就是参数的入栈的顺序问题。这个好比像学校里体育老师叫一班学生来排队,排头是从高到矮,还是从矮到高的选择。在入栈这个问题上,C语言也面临两个选择,一个跟代码的书写的顺序一样从左到右,另一个是从右到左。在考虑到动态参数的问题之后,C语言的设计者采用了从右到左的入栈方式,这种方式有两个优点:一是函数运行时,默认方式是从左到右,意味着出栈的方向应优先为栈顶的元素,这样可以提高运行效率;二是函数参数不定时,运行时分析字符串里出现需要的参数,每出现一个参数就弹出栈一次,跟运行分析的顺序一致。比如下面的函数声明:
printf(const char *,...);
由上可见入栈的顺序不同,调用的方式就不一样。在C语言里都是采用从右向左的方式入栈,在PASCAL语言里是从左向右入栈顺序的。在ctypes库里cdll、windll和oledll都是支持从右到左入栈的参数顺序。
接着下来又引出来了另外一个问题,既然参数是采用入栈的方式来传递,那么就会出现这种情况,当栈的参数没有使用到时,谁来清除,恢复栈的状态。在这个问题上,在编译器的设计者里又出现了两种选择:一种是倾向调用者清除,一种是倾向被调用者清除。这两种方式在性能上没有什么区别,只是安排清除的代码在不同的位置上。cdll是使用调用者清除的栈的方式,而windll和oledll是使用被调用者清除。这点就是它们之间的区别。因此,Python里调用动态连接库时,一定要清楚每个函数使用的调用方式,否则程序就会出问题,重则直接死掉。cdll和windll的区别如下图:
cdll和windll的区别