跨语言相互调用,一直是不同编程语言间代码交互Interop的难题,微软一直致力于给C++与C#找个理想的”翻译“,这么多年在语法语义(当然还应该包含编译器)和ABI(应用二进制接口)层面做了不少尝试,进而产生了C++\CLI,C++\CX和COM等技术产物,但这些产物如同现实中自然语言翻译一样,并不算太完美(java同其他语言交互的机制不太了解)。在这点上Python似乎把问题解决的很好,这也就是为什么Python会叫做胶水语言。正是由于python的这一特性,所以它被广泛用于自动化测试中。
python与其他语言交互:
1.使用ctypes 模块调用 C 动态库:如果被测试模块是以C语言编写,可以用Ctypes调用动态链接库,好处在于不用进行额外的开发,可以直接使用编译好的动态库。 ctypes 提供了完整的 C 类型封装,也支持自定义类型,大大减少在调用过程中的工作量。但C++的改名机制使得python调用C++编译的动态链接库很麻烦。
2.使用原生态的Python 的扩展和嵌入( Extending &Embedding)机制:Python 提供了一套完整的Extending框架来使用 C/C++ 编写扩展库,可以很灵活的开发 C/C++ 扩展模块。这种方法的缺点是工作量比较大,需要为每一个方法编写接口(但通过 SWIG可以降低工作量高效的调用动态链接库)。通过Embedding机制则可以使用C/C++调用python代码进行交互。
(Note:SWIG 是一种简化脚本语言与 C/C++ 接口的开发工具,通过包装和编译 C 语言程序来达到与脚本语言通讯目的的工具。它正是基于 Python 的扩展机制,自动生成接口文件,再编译成可以被 Python 调用的动态库扩展模。目前不打算去了解SWIG,写下来备忘。)
3.Boost.Python: Boot Python是一个为实现C++与Python无缝交互而编写的类库。它可以不借助任何工具仅凭C++编译器使C++类方法和对象快速无缝的暴露给Python,而目前(2.0版本)C++代码调用python模块还需要借助Python/C API不过已经很大程度减少了工作量。这个类库使用模板元编程技术简化了语法,包装C++代码只需要借助一种声明式接口定义语言declarative interface definition language (IDL)。
在Python 中引用 C/C++ 模块的方法较多,根据需要从中选择恰当的方法可以减少很多工作量。在Python 中引用 C/C++ 模块弥补了 Python 脚本测试框架的很多不足,在提高代码复用率的同时,模块的性能也大大提高。
仔细阅读了《Extending and Embedding the python Interpreter》之后,想自己demo一下,做个学习日志,记录学习python的辛酸之路。
无论是扩展(Extending)还是嵌入(Embeding),都需要python.h文件。PythonAPI定义了一系列的函数,宏,变量等(在python.h中定义的所有用户可见的符号都有个前缀py或者PY。)使得调用者可以调用Python运行时系统的大部分功能。这些Python API合成了一个C源文件,你可以通过Python.h访问这些对象。Python有可能会定义一些有可能影响到标准头文件的预处理程序,所以应在引用标准头文件前引用python.h。
需要指出的是嵌入和扩展有很多相像的地方(毕竟所谓交互就是提供一种翻译手段,将C能够认识的对象转化为Python所能认识,反之依然),他们主要的区别在于当扩展python时,你编写的代码还是在python解释器运行(你用C定义某个对象最终要转化成Python对象用python解释器运行),但嵌入python,跟python解释器关系就不大了(只是你程序的部分代码要调用python解释器去执行某些python代码完成交互)。相对来说扩展python比嵌入python更加直观好理解一点。Boost.Python可以简化你的工作,无论是嵌入,还是扩展。
要想要自定义模块被python代码调用需要把模块内函数名和地址放入函数表Method Table 中。函数表必须被引用到模块定义结构module definition structure里。而模块定义结构必须在模块的初始化函数中被传递给解释器。初始化函数必须被命名成PyInit_name()。另外在扩展Python时经常会要自定义异常,对于自定义异常的初始化也应该放在初始化函数中完成。
#include "Python.h"
static PyObject *SpamError; // 自定义异常
// 模块函数
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts);
}
// 模块函数表
static PyMethodDef SpamMethods[] = {
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
{NULL, NULL}
};
//模块定义结构体
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam",
"example module doc string",
-1,
SpamMethods,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_system(void)
{
PyObject *m;
m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL;
SpamError = PyErr_NewException("spam.error", NULL, NULL);
Py_INCREF(SpamError);
PyModule_AddObject(m, "error", SpamError);
return m;
}
Note:要使用同一个编译器去编译python build 和 这份代码,否则嵌入后不能够执行。使用VS2010编译python3.2源码中的PC\example_nt下代码时有需要改动工程文件,将两行initexample去掉,否则会编译出错,至于需要python2.6.lib或者python2.6_d.lib则完全可以使用同一份代码编译出3.2版本的lib(注意需要编译两个版本,debug版本对应后者,release版本对应前者)代替但同样需要改动工程文件。
python解释器有个很重要的约定:当一个函数失败时,他应该设定异常条件并且返回一个error value(错误值)。异常会被存储在解释器里的一个静态的全局变量中,它是一个三元组(type, value, traceback):第一个值代表了异常类型,第二个全局变量会存储异常的关联值,第三个变量包含了异常堆栈的追溯对象。这个三元组就是Python中sys.exc_info()的返回值。如果这个三元组值为null,就代表没有异常发生。
PyArg_ParseTuple():会检查参数类型,并把他们转化成C类型的值。
python API定义了一组函数用于定义不同类型的异常:
PyErr_SetString():它的参数是异常对象和一个C类型字符串,异常对象通常是一个预定义对象比如PyExc_ZeroDivisionError,C类型字符串表明了错误发生的原因,会被转换成python字符串对象并且会被当作异常的关联值存储起来。
PyErr_SetFromErrno():只有一个异常参数,用于检查全局变量errno并构造关联值。
PyErr_SetObject():两个参数,异常和关联值。 传递给这些函数的任何对象都不需要调用Py_INCREF()。
PyErr_Clear():用于忽略掉函数失败引发的异常,只有一种情况C代码会掉用该函数:当不想传递异常错误信息给解释器而想自己完全处理异常。
Reference:
http://docs.python.org/py3k/extending/index.html
http://www.boost.org/doc/libs/1_49_0/libs/python/doc/index.html