Python 与 C++ 的交互编程




基础知识

编译语言和解释语言

尽管现在很多编程技术都在交融,出现了不少带有编译系统的解释语言,和带有复杂运行时系统的编译语言,但通常我们还是可以比较明确的区分它们。在这个大前提下,Python和C++属于这个两类技术中比较极端的代表。


编译时和运行时

C++的强大之处在于高效的生成产品和强大复杂的编译系统。利用C++的语法,可以生成非常灵活强大的程序,而这其中大部分工作都是在编译程序的过程中完成。相对而言,C++没有标准的运行时(CLI系统还没有真正成为C++的标准,只能说提供了一个C++在CLI系统上运行的标准),要建立一个完善的运行时,要么就要投入大量的成本,要么就寻找一个合适的运行时环境调用。Python自身是一个成熟完备的脚本语言,拥有开放的编程和扩展接口,加上它本身的对象机制设计非常适合外部调用,Python与编译语言,特别是C/C++的互嵌入日益成为关注焦点。


互嵌入

Python有多种实现,比较著名的有C语言编写的官方实现,Java版的Jython,.net版的IronPython,值得一提的是,后两者都是由

Jim Hugunin 开发,此人现在微软主持动态语言研究项目。另外,有一个CPython调用.net的桥接系统 PythonDotNet,由ZOPE社区的一位技术人员开发。现在使用最广泛的互嵌入场合,是C/C++和CPython之间的互嵌入。


Python 的官方版本本身就是由C语言编写,有开放的C接口。使用C语言调用和扩展Python都非常简单。基于这个前提,使用C++语言与Python进行互操作也并不复杂。现在已经有若干用于Python API封装的C++库,这里我们要讨论的是Boost.Python。这是著名的C++库Boost的重要组成之一。


用C++扩展Python

Python扩展入门

Python允许在遵循一定规则的前提下调用语言和操作系统无关的动态链接库。通常,在Windows下为.dll,而在*nix下为.so文件。操作系统的区别由C++编译器、Boost库和Python解释器屏蔽,最终程序员可以基本忽视其中的区别。


在实现互嵌入的时候,要考虑Python的运行时限制,Python的虚拟机只能操作Python对象,并且虚拟机拥有自己的内存管理模式,应该根据情况对C++程序和Python环境所交换的信息进行必要的封装和拆封。


Python本身提供了C语言扩展所需要的接口文件,可以在Python的include,Libs,DLLs目录下找到对应的文件。


一个纯C语言的Python扩展可能类似下面这样:


#include <Python.h>

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    return Py_BuildValue("i", sts);
}

我们根据具体的操作系统和开发工具,把以上函数封装到一个动态链接库中,放入Python的动态链接库搜索路径(Python For Windows中是DLLs)中。就可以把它作为Python的一个模块使用。

C++封装

我们知道,C++的代码封装机制和C有所不同,相比真正的纯C语言,C++更为复杂。例如,它有虚函数,有模版,前者使运行时更为复杂和灵活,后者使一些比宏更复杂的语法推导得以实现。Boost.Python为C++程序提供的封装能力,正是基于template。我们要做的,是把C++程序中的函数、类、数据成员等等,都变成Python对象。


BOOST.Python封装了各平台的Python动态链接库接口,我们要做的只是调用它,封装需要暴露的定义,然后指示编译器生成动态链接库即可。


函数封装

Python的API使用回调函数调用Python对象和函数。BOOST提供完整的函数封装过程,一个简单的封装过程如下:



Hello World

Following C/C++ tradition, let's start with the "hello, world". A C++ Function:


char const* greet()
{
return "hello, world";
}



can be exposed to Python by writing a Boost.Python wrapper:



#include <boost/python.hpp>
using namespace boost::python;

BOOST_PYTHON_MODULE(hello)
{
def("greet", greet);
}



That's it. We're done. We can now build this as a shared library. The resulting DLL is now visible to Python. Here's a sample Python session:



>>> import hello
>>> print hello.greet()
hello, world




以上来自BOOST文档。


面向对象

对象管理、引用计数

Python虚拟机提供引用计数和自动垃圾回收能力,但是C++对象没有这样的能力(C++对象运行在Python虚拟机之外)。为了避免程序运行过程中的内存使用问题,需要为函数中传递的指针提供引用计数管理,BOOST通过“调用协议”来支持此功能,详情请见BOOST文档“Call Policies”。


C++ 对象到 Python 对象的封装

BOOST文档中提供了C++对象封装至Python对象的方法,主要手段是以class_模板将C++类定义解析为符合C API的形式,一个形如:



struct World
{
    void set(std::string msg) { this->msg = msg; }
    std::string greet() { return msg; }
    std::string msg;
};

的C++代码可以通过如下方式封装:



#include <boost/python.hpp>
using namespace boost::python;

BOOST_PYTHON_MODULE(hello)
{
    class_<World>("World")
        .def("greet", &World::greet)
        .def("set", &World::set)
    ;
}

这里的关键点在于封装代码必有的

BOOST_PYTHON_MODULE 宏和class<>模板的def。为了支持C++类定义丰富的内容,Boost.Python提供了很多调用方法,比较常用,也比较重要的是构造函数、重载和虚函数的支持。

构造函数的封装是通过__init__模板封装的,不同的构造函数重载可以通过参数列表区分并重复封装。BOOST文档中给出了这样一个示例:



class_<World>("World", init<std::string>())
    .def(init<double, double>())
    .def("greet", &World::greet)
    .def("set", &World::set)
;


普通函数重载的支持事实上是一种函数封装方法,和这个函数是否是某个类的成员并无关系。基本思路是把不同的函数形式定义为不同的函数指针,然后以普通函数的形式封装。

虚函数的支持比较复杂,BOOST通过多继承方式支持它,用户需要以多继承定义一个基类的封装,然后暴露这个封装类。在阅读BOOST文档时,要注意封装类在用class_暴露接口时的代码,以下是BOOST文档中的一个示例:


struct BaseWrap : Base, wrapper<Base>
{
    int f()
    {
        if (override f = this->get_override("f"))
            return f(); // *note*
        return Base::f();
    }

    int default_f() { return this->Base::f(); }
};

class_<BaseWrap, boost::noncopyable>("Base")
    .def("f",&Base::f, &BaseWrap::default_f)
    ;

注意封装代码中class_的模板参数是BaseWrap,但def中的第二个函数是&Base::f。def的第三个函数提供函数封装的一些信息,这里指明了该虚函数的默认实现。更多的用法请参考Boost文档。在BOOST中还提供了更复杂的功能,比如可以定义Python类的属性(者甚至比在Python中更简单)。


在C++程序中嵌入Python

引用解释器

Python标准的外部调用方法并不复杂,在Python的文档中可以查到。最简单的调用方式如下:



#include <Python.h>

int
main(int argc, char *argv[])
{
  Py_Initialize();
  PyRun_SimpleString("from time import time,ctime/n"
                     "print 'Today is',ctime(time())/n");
  Py_Finalize();
  return 0;
}

Py_Initialize();启动虚拟机,Py_Finalize();终止它。PyRun_SimpleString用于执行语句。Boost延续了这一方式,并着眼于将交互过程变得更简单些。BOOST程序中最简单的示例如下:




Py_Initialize();
object main_module((
    handle<>(borrowed(PyImport_AddModule("__main__")))));

object main_namespace = main_module.attr("__dict__");

handle<> ignored((PyRun_String(

    "hello = file('hello.txt', 'w')/n"
    "hello.write('Hello world!')/n"
    "hello.close()"

  , Py_file_input
  , main_namespace.ptr()
  , main_namespace.ptr())
)); 
Py_Finalize();

从这种复杂度的程序中,体现不出BOOST的优势,它甚至比C程序更复杂。不过这里可以学习一下它的基本框架。也许下面这个更为复杂的示例可以说明些问题:



object main_module((
     handle<>(borrowed(PyImport_AddModule("__main__")))));

object main_namespace = main_module.attr("__dict__");

handle<> ignored((PyRun_String(

    "result = 5 ** 2"

    , Py_file_input    , main_namespace.ptr()
    , main_namespace.ptr())
));

int five_squared = extract<int>(main_namespace["result"]);我们可以通过这里看到BOOST的一些优势,如通过 
  C++对象(这里main_namespace对象)获取Python数据,并将其转化为C++数据(extract模板)。而下面这个示例演示了BOOST为Python环境提供的C++异常保护:
 
  
try
{
    object result((handle<>(PyRun_String(
        "5/0"
      , Py_eval_input      , main_namespace.ptr()
      , main_namespace.ptr()))
    ));

    // execution will never get here:
    int five_divided_by_zero = extract<int>(result);
}
catch(error_already_set)
{
    // handle the exception in some way
}当然,我们也可以选择无异常的版本: 
  

handle<> result((allow_null(PyRun_String(
    "5/0"
   , Py_eval_input   , main_namespace.ptr()
   , main_namespace.ptr()))));

if (!result)
    // Python exception occurred
else
    // everything went okay, it's safe to use the result



调用脚本



PyRunString是Python的C-API,Python提供了一系列的调用方法供程序执行脚本使用,也包括了PyRun_File这样的直接调用文件的功能,当然,它不支持C++的I/O,使用的是C风格的文件指针。

 

附记



最后提一下,使用Boost,无论你是要扩展还是嵌入Python,都需要预编译Boost库,编译方法在Boost的文档中也有介绍。使用Boost的时候,要把预编译好的lib链接到link序列中,还要把DLL放到应用程序可以访问到的地方。

附记

最后提一下,使用Boost,无论你是要扩展还是嵌入Python,都需要预编译Boost库,编译方法在Boost的文档中也有介绍。使用Boost的时候,要把预编译好的lib链接到link序列中,还要把DLL放到应用程序可以访问到的地方。