逆世界:让 C++ 走进 Python

本文首发程序员杂志 2014 年 8 月刊

要想实现 C 语言与 Python 之间的交互,业界已有不少成熟的解决方案。但如果希望实现 C++ 与 Python 之间的水乳交融,现有的这些解决方案却又都不那么完美:Boost.Python 失之环境复杂; Cython 对 C++ 支持有限; 易于上手的 ctypes 则干脆不支持 C++。

下面将会向大家介绍一种基于 Cython 的解决方案,可以轻松实现 C++ 与 Python 之间的跨语言多态,也算是补足了 Cython 对 C++ 支持的短板吧。

跨语言多态的问题

首先让我们来看问题:如果要把下面的 C++ 类 CppFoo 包装成一个 Python 类,应该怎么做?

class CppFoo{public:    virtual void fun()    {        cout << "CppFoo::fun()" << endl;    }    virtual ~CppFoo()    {    }};inline void call_fun(CppFoo* foo){    foo->fun();}

我们可以使用 Cython 提供的 C++ 绑定机制,直接将 CppFoo 类包装成 Python 中的 foo.PyFoo

# 在 Cython 中引入 C++ 类定义cdef extern from "CppFoo.hpp":    cdef cppclass CppFoo:        void fun()    void call_fun(CppFoo* foo)# C++ 类 CppFoo 的 Python 包装类cdef class PyFoo:    cdef CppFoo* _this    def __cinit__(self):        self._this = new CppFoo()    def __dealloc__(self):        del self._this    # 转发调用    def fun(self):        self._this.fun()# C++ 函数 call_fun() 的 Python 包装cpdef py_call_fun(PyFoo foo):    call_fun(foo._this)

用 Cython 将上面的文件编译成 Python 扩展 foo 后,让我们来看看测试结果:

import foobase = foo.PyFoo()base.fun()# 输出 "CppFoo::fun()"foo.py_call_fun(base)# 输出 "CppFoo::fun()"

我们可以看到 C++ 成员函数被 Python 正确地调用了。

接着让我们更进一步:如果需要在 Python 中继承 PyFoo 并且改写 CppFoo::fun() 虚函数又会发生什么呢?

class PyDerivedFoo(foo.PyFoo):    def fun(self):        print 'PyDerivedFoo.fun()'derived = PyDerivedFoo()derived.fun()# 正确输出 'PyDerivedFoo.fun()'foo.py_call_fun(derived)# 哎!为什么输出了 "CppFoo::fun()"?

看到了吗?我们在 Python 中改写的 PyDerived.fun() 被忽略了,py_call_fun() 调用的仍然是 C++ 父类的实现。看来 Cython 并不支持跨语言多态。

解决跨语言多态问题

如何将跨语言多态引入 Cython 中呢?谚云:额外间接层解决一切。我们可以通过增加一层中间代理来连接 C++ 和 Python 的多态机制,从而实现跨语言多态。

首先让我们明确一点,C++ 的虚函数只能在 C++ 继承类中被改写。那么我们的代理类顺理成章的应该要继承 CppFoo

class CppFooProxy : public CppFoo{public:    void fun();};

我们还需要改写代理类的 fun() 函数,让它转去调用 Python 对象的 fun() 方法,从而完成跨语言多态。

void CppFooProxy::fun(){    if (has_python_override_method(self, "fun")) {        return call_python_method_fun(self);    }    else {        return CppFoo::fun();    }}

在上面的代码中,我们先通过 has_python_override_method() 函数来判断 Python 对象是否改写了 fun() 方法。如果我们检测发现 Python 对象确实含有 fun() 方法,我们就将调用转发到 Python 中重新定义的那个 fun 方法上。反之,如果 Python 对象并没有改写 fun() 那就转去调用父类的默认实现 CppFoo::fun()。最终实现跨语言多态。

这里还有个特殊情况没有在代码中表现出来:如果父类方法是纯虚函数,而 Python 也没有提供任何实现,那要怎么办呢? 简单的处理方案可以直接抛出异常来报错,让纯虚函数跨界调用在运行时出错。

上面这段程序里的 self 又是什么呢? 它是一个实实在在的 Python 对象。通过 self, 我们可以在 C++ 的世界中操作彼端 Python 世界里的那个对象

class CppFooProxy : public CppFoo{public:    CppFooProxy(PyObject* self)        : self(self)    {        assert(self);        // 增加 Python 对象引用计数        Py_XINCREF(self);    }    ~CppFooProxy()    {        // 减少 Python 对象引用计数        Py_XDECREF(self);    }    void fun();private:   PyObject* self;};

那么 has_python_override_method() 该如何实现呢? 我们可以用 Python 提供的 C API 直接在 C++ 代码中实现这个功能。但这里我们选择用 Cython 来实现,然后通过 Cython 的 public api 机制暴露 C 接口再给 C++ 调用。这样的好处是我们可以很简洁地用类似 Python 语法实现这个功能。

import typescdef public api bool has_python_override_method(        object self,        const char* method_name):    method = getattr(self, method_name, None)    return isinstance(method, types.MethodType)

getattr() 方法能通过名字找到对象中相应的属性对象。在尝试获得 self 中与方法名想同名称的子对象后,我们再判断这个子对象的类型是不是一个方法。

下面 call_python_method_fun() 的实现就更简单了,一旦找到方法我们就直接转发调用

cdef public api void call_python_method_fun(object self):    method = getattr(self, method_name)    method()

搞清了 CppFooProxy::fun() 的实现细节后,下一步就是看如何将 Python 对象 self 塞进 CppFooProxy

from cpython.ref cimport PyObject# 在 Cython 中引入 C++ 类 CppFoo 的定义cdef extern from "CppFoo.hpp":    cdef cppclass CppFoo:        pass    void call_fun(CppFoo* foo)# 在 Cython 中引入 C++ 类 CppFooProxy 的定义cdef extern from "CppFooProxy.hpp":    cdef cppclass CppFooProxy(CppFoo):        void fun()# 改变我们的 Python 包装类cdef class PyFoo:    cdef CppFooProxy* _this    def __init__(self):        # 将 self 放入 CppFooProxy 中        self._this = new CppFooProxy(<PyObject*>(self))    def __dealloc__(self):        del self._this    def fun(self):        self._this.fun()# C++ 函数 call_fun() 的 Python 包装cpdef py_call_fun(PyFoo foo):    call_fun(foo._this)

可以看到,我们先把要包装导出的 C++ 目标类 CppFoo 和我们刚刚实现的代理类 CppFooProxy 的定义导出到 Cython 中,再构造 Python 类 PyFoo 来包装我们的代理类 CppFooProxyPyFoo 在内部维护了一个 CppFooProxy 代理类的对象,而 PyFoo.foo() 调用会被转发到代理类的 CppFooProxy::fun() 函数上。当创建 CppFooProxy 对象时,PyFoo 也会将自己通过 self 传入到 CppFooProxy 中。这样一来,PyFooCppFooProxy 就彼此拥有对方。他们一起合作来完成 C++ 和 Python 这两个世界的连接。

细心的朋友可能意识到了,上面 foo() 函数调用转发隐藏着一个问题。PyFoo.fun() 会去调用 CppFooProxy::fun(),而 CppFooProxy::fun() 又会去调用 Python 对象中的 fun() 方法,这不是一个死循环吗? 幸运的是在 has_python_override_method() 中,我们是用 types.MethodType 来做比较,去判定对象是否改写了 fun() 方法。而 types.MethodType 只会匹配纯 Python 方法,它不包含内建函数 (built-in functions)。我们知道,Python 扩展中的方法类型都是属于内建函数类型。这样恰好排除掉了 PyFoo 自己那个属于内建函数的 fun() 方法,从而避免了危险的死循环。

至此,我们的 C++ 类 CppFoo 就成功地通过 PyFoo 类转移到了 Python 世界中了。来检验一下成果吧:

derived.fun()# 输出 'PyDerivedFoo.fun()'foo.py_call_fun(derived)# 同样输出 'PyDerivedFoo.fun()'!

一切正常,在 CppFooProxy 这个额外的间接层牵线搭桥下,C++ 和 Python 终于实现了跨语言多态。

自动代码生成

问题虽然解决了。但回头看看,为了包装上面例子中的 C++ 类,我们要做的事情太多:

    定义 C++ Proxy 类实现 C++ Proxy 类和相关的虚函数在 Cython 中实现相关的 Python 方法的检测和转发功能,以供 C++ Proxy 类使用在 Cyhton 中引入 C++ 类定义在 Cyhton 中引入 C++ Proxy 类定义在 Cython 中把 C++ Proxy 类包装成 Python 扩展类

这还只是包装导出 1 个类的 1 个方法。假设有 10 个类,100 个方法需要包装导出,这工作量想想就头疼。虽说这里面并没任何技术难度,我们只要照葫芦画瓢就好了。但如果靠人手工来做的话,因为步骤繁琐会很容易出错。

对程序员这种一心偷懒的生物来说,类似的重复工作都是写个程序来自动完成。下面介绍下我写的 cppython 工具,它就是干这活儿的。

还是上面的例子,让我们来包装导出 CppFoo 类。这次我们通过 cppython 来生成所有的包装导出代码:

$ python cppython.py cpp_foo.hpp out/foogenerating out/cpp_foo.pxd ...generating out/foo.pyx ...generating out/foo_cppython.cpp ...generating out/foo_cppython.hpp ...generating out/foo.pxi ...generating out/foo_cppython.pxd ...generating out/setup.py ...done.$ cd out/ && python setup.py build_ext --inplace

可以看到,cppython 通过解析 cpp_foo.hpp 自动生成了 7 个文件

cpp_foo.pxdCppFoo 类定义引入 Cythonfoo_cppython.hpp 是 C++ 代理类的定义foo_cppython.cpp 是 C++ 代理类的实现foo_cppython.pxd 将代理类的 C++ 定义引入 Cythonfoo.pyx 包含 python 扩展类 Foo 的定义foo.pxi 包含代理类所需要的 Python 对象交互方法实现setup.py 编译 Python 扩展模块的启动脚本

这下好了,一声令下,程序就乖乖帮我们完成了繁琐机械的工作。偷懒改变世界啊!

当然,把复杂的 C++ 类框架丝毫不差地一一映射到 Python 并不现实,也没有必要。毕竟 Python 和 C++ 各自有不同的惯用模式和编程习惯。建议在使用 cython 和 cppython 之前,先把 C++ 类的模块功能做一定的切分和包装,有选择的导出到 Python,这样效果会更好。

cppython 原理与实现

通过上面的描述,我们已经了解了 cppython 的主要功能。它的输入是 C++ 头文件,里面包含了待导出的 C++ 类定义。根据输入,cppython 会自动生成代理类实现和一堆 Cython 文件。可是 cppython 究竟是如何实现这些功能的呢? 让我们来深入了解一下。

在 cppython 内部,我们首先会对输入的 C++ 文件进行语法分析,生成语法树。接着通过遍历语法树来生成所需要的包装导出代码。这里我们用访问者 (Visitor) 设计模式来解耦,把语法树的遍历逻辑和不同文件的代码生成逻辑区分开来,彼此独立实现。

这里还偷了个懒,图中的 IVisitor 接口其实只存在于概念中,并没有任何代码。访问者类的多态完全依靠 Python 鸭子类型 (duck typing) 机制。在实际的遍历时,程序会生成 7 个不同的访问者实例,分别负责 7 个将要生成的目标代码文件。为了减少解析遍历的次数,所有访问者都会被放入到一个 GroupVisitor 容器中,一次性遍历完毕。

按道理我们还应该增加一个上下文对象,用来在代码生成过程当中做记录和协调。但在现阶段 cppython 只是依靠着惯例来协调不同访问者生成的代码。例如,所有的代理类名字都生成为 原始类名_proxy

了解完了 cppython 的工作流程,让我们看看 cppython 究竟是怎么解析 C++ 文件的。众所周知,C++ 语法以繁复难以解析著称。这里我们有 3 个选择

手写解析器,对 C++ 语法有选择地分析。但这样做耗时耗力,而且很难避免出错使用 pyparsing 等解析库,帮助我们实现简单的 C++ 语法解析器。这只比纯手写好上一点,难点还是在于 C++ 语法实在不是一般的复杂利用真正的 C++ 编译器来解析。例如 g++ 就可以把解析后的语法树输出为 XML 结构,方便其它程序进一步利用。不少代码生成器就是这么做的

在比较权衡后 cppython 最终选择了第三种方式,使用一个真正的 C++ 编译器来帮助解析。但这里没有使用老牌的 g++,而是选择了另一位新晋明星: Clang。

Clang 是苹果基于 LLVM 架构开发的 C++/Objective C 编译器,在被苹果 Xcode 加持后,又被 FreeBSD 选为默认编译器。势头正旺,大有取代 g++ 的架式。它不但对 C++ 新标准有完美的支持,更重要的是它把自己内部的语法解析功能通过 libclang 暴露出来,让用户能够直接使用。象 Xcode 集成开发环境的智能提示,还有一些第三方的 C++ 重构工具就是利用 Clang 本身提供的解析功能实现的。cppython 也正是利用 Clang 官方的 Python 扩展来实现 C++ 解析功能的。

一行程序胜过千言万语。下面的代码示范了如何利用 Clang 来解析我们上文中的 CppFoo 类定义

from clang.cindex import *def print_cpp_parse_tree(cursor, indent=''):    '递归打印 C++ 语法树'    print indent, cursor.kind, cursor.type.spelling, cursor.spelling    for child in cursor.get_children():        print_cpp_parse_tree(child, indent+'  ')tu = TranslationUnit.from_source(    filename='CppFoo.hpp',    args=['-x', 'c++'])print_cpp_parse_tree(tu.cursor)

下面是解析后的输出结果,大家可以直观感受一下 clang Python 扩展的威力

CursorKind.TRANSLATION_UNIT  CppFoo.hpp  CursorKind.TYPEDEF_DECL __int128_t __int128_t  CursorKind.TYPEDEF_DECL __uint128_t __uint128_t  CursorKind.TYPEDEF_DECL __builtin_va_list __builtin_va_list    CursorKind.TYPE_REF __va_list_tag __va_list_tag  CursorKind.CLASS_DECL CppFoo CppFoo    CursorKind.CXX_ACCESS_SPEC_DECL    CursorKind.CXX_METHOD void () fun      CursorKind.COMPOUND_STMT    CursorKind.DESTRUCTOR void () ~CppFoo      CursorKind.COMPOUND_STMT  CursorKind.FUNCTION_DECL void (CppFoo *) call_fun    CursorKind.PARM_DECL CppFoo * foo      CursorKind.TYPE_REF CppFoo class CppFoo    CursorKind.COMPOUND_STMT      CursorKind.CALL_EXPR void fun        CursorKind.MEMBER_REF_EXPR <bound member function type> fun          CursorKind.UNEXPOSED_EXPR CppFoo * foo            CursorKind.DECL_REF_EXPR CppFoo * foo

有了 Clang 的强力支持后,cppython 不费吹灰之力就能解析任意 C++ 代码了。

现在 cppython 还处于 alpha 阶段,虽然基本框架已经完成,但仍有很多改进的空间。除了继续完善 Cython 包装代码生成功能外,在现有架构下,cppython 也能很容易地支持 Boost.Python 的包装代码生成。我们甚至可以抛开 Cython 和 Boost.Python,直接生成基于 Python C API 的扩展代码。更进一步,C++ 与其它语言 (例如 Ruby) 的交互代码也可以用类似方式自动生成,毕竟对 cppython 来说,这些改进都只需要添加新的访问类而已。

总结

本文介绍了 C++ 与 Python 之间跨语言多态的一种可行方案,并且提供了 cppython 代码生成器来自动完成包装工作。cppython 的实现依赖于开源项目 Cython 和 Clang。而 cppython 本身也已经在 https://bitbucket.org/zhuoqiang/cppython 上开源了。感兴趣的朋友可以动手玩一玩,也欢迎参与改进。

逆世界:让 C++ 走进 Python

相关文章:

你感兴趣的文章:

标签云: