Author: David Abrahams

摘要

Boost.Python是一個開源的C++程序庫,提供類似IDL的接口來把C++類和函數綁定到Python。借助于C++強大的編譯時內省(introspection)能力和最新的元編程(metaprogramming)技術,它完全用C++來實現,而不用引入新的語法。Boost.Python豐富的特性和高級接口使從底層起按混合系統的方式設計組件成為可能,從而使程序員可以輕松和連貫的同時使用C++高效的編譯時多態和Python極其方便的運行時多態。

簡介

作為兩門語言,python和C++在很多方面不一樣。C++被編譯為機器碼,python被解釋(interpreted)執行。Python的動態類型(type)系統經常被認為是靈活性的基礎,C++的靜態類型是效率的基石。C++有復雜艱深的編譯時元語言(meta-language),而在python里,實際上一切都在運行時發生。然而對很多程序員來說,這些不同恰好意味著Python和C++是彼此的完美補足。Python 程序里的性能瓶頸部分可以用C++來重寫,從而最大化速度。強大的C++程序庫的作者選擇Python作為中間件(middleware)語言,從而獲得靈活的系統集成能力。此外,表面的不同掩蓋了二者非常類似的一些地方:

  • ‘C’-家族的控制結構 (if,while,for…)
  • 支持面向對象(object-orientation),函數式編程(functional programming),泛型編程(generic programming)(它們都是多范 式語言(multi-paradigm languages))
  • 認同語法可變性(syntactic variability)在提高代碼的可讀性和表達力上的重要作用,提供了對操作符重載的廣泛支持
  • 高級概念,如集合(collections),迭代器(iterators)等
  • 高級封裝機制(C++: namespaces,Python: modules)支持可重用程序庫的設計
  • 異常處理機制提供了有效的錯誤管理
  • 被普遍使用的C++習慣用語,如handle/body classes和引用被計數的智能指針(reference-counted smart pointers)對應Python 的引用語義(reference semantics)

考慮到Python豐富的’C'協作API,原則上把C++的類型和函數以類似于暴露給C++的接口暴露給Python是可能的。然而,單是Python提供的這種設施對集成C++的支持比較弱。和C++,Python相比,’C'的抽象機制非常初級,而且完全不支持異常處理?!疌'擴展模塊的作者必須手動管理引用計數,這不但讓人惱火的麻煩和單調,還極度容易出錯。傳統的擴展模塊容易產生重復的樣板代碼(boilerplate code),從而難于維護,尤其是要包裝的API很復雜時。

上述限制導致了一些包裝系統的開發。SWIG_ 可能是集成C/C++和Python的包裝系統中最流行的。一個更近的例子是 SIP ,它專門設計來提供 Qt 圖形用戶界面庫的Python接口。SWIG和SIP都引入了它們專有的語言來實現語言間綁定。這當然有它的好處,但不得不應付三種不同的語言(Python,C/C++和接口語言)也帶來了實際的和心理上的困難。 CXX 軟件包展示出它是一個有趣的包裝系統。它說明了至少一部份Python ‘C’ API可以通過用戶友好得多的C++接口來包裝和表現。然而,和SWIG和SIP不一樣,CXX不支持把C++類包裝成新的Python類型。

Boost.Python 的特性和目標和很多這樣的系統有相當程度的重疊。就是說,Boost.Python試圖最大化便利性和靈活性,而不引入單獨的包裝語言。相反,它在幕后用靜態元編程技術管理很多復雜問題,賦予了用戶通過高級C++接口來包裝C++類和函數的能力,Boost.Python也在如下領域超越了早期的系統:

  • C++虛函數支持,虛函數可以用Python來覆蓋(override)
  • 在整個生命周期內對低級指針和引用進行全面管理的設施
  • 對把擴展(extensions)組織成Python packages的支持,通過中心注冊表(central registry)來進行語言間類型轉換
  • 安全而便利的連接強大的Python序列化引擎(pickle)的機制
  • C++的lvalue和rvalue的一致的處理規則,這只能來自對Python和C++兩者的類型系統的深入理解。

鼓舞Boost.Python開發的關鍵發現是,傳統擴展開發中的大量樣板代碼都可以通過C++編譯時內省來消除。被包裝的C++函數的每個參數都必須根據參數類型從Python對象里取出來。類似地,函數返回值的類型決定了返回值如何從C++轉換成Python。參數類型和返回值類型當然都是每個函數的類型的一部分,正是從這里,Boost.Python推導出了大部分需要的信息。

這種方法導向了 用戶引導的包裝 :盡可能的用純C++的框架直接從要包裝的代碼里取得信息,這以外的信息由用戶顯式提供。大多數引導是自動的,很少需要真正的干涉。因為寫接口規范和寫被暴露的代碼的是同一門全功能語言,當需要取得控制時用戶有了空前強大的能力。

Boost.Python 設計目標

Boost.Python的首要目標是讓用戶只用C++編譯器就能向Python暴露C++類和函數。大體來講,允許用戶直接從Python操作C++對象。

然而,有一點很重要,那就是不要 過于 按字面翻譯所有接口:必須考慮每種語言的慣用語。例如,雖然C++和Python都有迭代器的概念,表達方式卻很不一樣。Boost.Python必須能連接這些不同的接口。

必須把Python用戶和C++接口的微小誤用造成的崩潰隔離。出于同樣原因,應該把C++用戶和低級Python ‘C’ API隔離,容易出錯的C接口,比如手動引用計數管理,原始的(raw)PyObject指針,應該用更加健壯的(more-robust)替代物來取代。

支持基于組件的開發是至關重要的,因此被暴露在一個擴展模塊里的C++類型應該能夠被傳遞給被暴露在另一個模塊中的函數,而不丟失重要的信息,比如說C++繼承關系。

最后,所有的包裝必須是 非侵入的(non-intrusive) ,不能修改甚至看不到原始的C++代碼。對只能看見它頭文件和二進制文件的第三方,現有的C++庫必須是可包裝的。

Hello Boost.Python World

現在來預覽一下Boost.Python,并看看它如何改進Python的原始包裝功能。下面是我們想暴露的一個函數:

char const* greet(unsigned x)
{
   static char const* const msgs[] = { "hello","Boost.Python","world!" };

   if (x > 2)
       throw std::range_error("greet: index out of range");

   return msgs[x];
}

用Python的C API和標準C++來包裝這個函數,我們需要像這樣:

extern "C" // 所有Python交互都使用C鏈接和調用習慣
{
    // 處理參數/結果轉換和檢查的包裝層
    PyObject* greet_wrap(PyObject* args,PyObject * keywords)
    {
         int x;
         if (PyArg_ParseTuple(args,"i",&x))    // 取出/檢查參數
         {
             char const* result = greet(x);      // 調用被包裝的函數
             return PyString_FromString(result); // 結果轉換成Python
         }
         return 0;                               // 發生了錯誤
    }

    // 待包裝函數表,函數用這個模塊來暴露
    static PyMethodDef methods[] = {
        { "greet",greet_wrap,METH_VARARGS,"return one of 3 parts of a greeting" }
        ,{ NULL,NULL,0,NULL } // sentinel
    };

    // 模塊初始化函數
    DL_EXPORT init_hello()
    {
        (void) Py_InitModule("hello",methods); // 添加成員函數(method)到模塊
    }
}

現在看看我們使用Boost.Python來暴露它時的包裝代碼:

#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
    def("greet",greet,"return one of 3 parts of a greeting");
}

下面是使用它的代碼:

>>> import hello

>>> for x in range(3):
...     print hello.greet(x)
...
hello
Boost.Python
world!

C API版本要冗長的多,此外,一些它沒有正確處理的地方值得提到:

  • 原來的函數接受無符號整數,Python ‘C’ API僅僅提供了提取有符號整數的方式。如果我們試圖向hello.greet傳遞負數Boost.Pyt hon版將拋出Python異常,而Python ‘C’ API版則會繼續像在C++實現中那樣轉換負數到無符號數(通常包裝成某種很大的數),然后 把不正確的轉換結果傳給被包裝函數。
  • 這引起了第二個問題:如果函數的參數大于2,C++ greet()被調用時會拋出異常。典型的,如果C++異常跨越C編譯器生成的代碼的 邊界傳遞,會導致崩潰。正如你在第一個版本中看到的,那兒沒有C++腳手架(scaffolding)來防止崩潰發生。被Boost.Python包裝 過的函數自動包含了異常處理層,它把未處理的C++異常翻譯成相應的Python異常,從而保護了Python用戶。
  • 一個更微妙的限制是,Python ‘C’ API的參數轉換機制只能以一種方式取得整數x。如果一個Python long 對象(任意精度整數) 碰巧可以轉換成(fit in)unsigned int而不能轉換成signed long,PyArg_ParseTuple不能對其進行轉換。同樣如果被包裝的C++ 類包含用戶定義的隱式operator unsigned int()轉換,它永遠不能處理。Boost.Python的動態類型轉換注冊表允許用戶任意添加 轉換方法。

庫概覽

這部分簡要描述了庫的主要特性。為了避免混淆,忽略了實現細節。

暴露類(Exposing Classes)

C++類和結構以類似的簡潔的接口來暴露。假設有:

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

下面的代碼將在我們的擴展模塊里暴露它:

#include <boost/python.hpp>

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

盡管上述代碼有某種熟悉的Pythonic的感覺,但語法有時還是有點令人迷惑,因為它看起來不像人們習慣的C++代碼。但是,這仍然只是標準C++。因為它們靈活的語法和操作符重載,C++和Python都很適于定義特定領域(子)語言( domain-specific (sub)languages) (DSLs)。那就是我們在Boost.Python里所做的。把代碼拆開來看:

class_<World>("World")

構造類型class_<World>的未命名對象,把"World"傳給它的構造器。這將在擴展模塊里創建一個叫World的new-style Python類,并把它和C++類型World在Boost.Python的類型轉換注冊表里關聯起來。我們也可以這么寫:

class_<World> w("World");

但那樣做的話會更繁瑣,因為我們不得不再次命名w以調用它的def()成員函數:

w.def("greet",&World::greet)

原來的例子里表示成員進入的點的位置沒有什么特別的:C++允許任意的空白符出現在表意符號(token)的任一邊,把點放在每行的開始允許用統一的語法把連續的調用都串起來,不管我們想串多少都行。另一個允許的串接的事實是class_<>成員函數都返回對*this的引用。

因此原來的例子等同于:

class_<World> w("World");
w.def("greet",&World::greet);
w.def("set",&World::set);

能這樣拆分Boost.Python類包裝層的組成部分有時候是有用的,但本文的剩下部分將一直使用簡潔的語法。

最后來看包裝類被使用的情況:

>>> import hello
>>> planet = hello.World()
>>> planet.set('howdy')
>>> planet.greet()
'howdy'

構造器(Constructors)

因為我們的World類只是一個簡單的struct,它有一個隱式的無參數(no-argument)(nullary)構造器。Boost.Python默認暴露nullary構造器,這就是我們可以像下面這樣寫的原因:

>>> planet = hello.World()

然而不管哪門語言,設計得好的類可能都需要構造器參數,以建立他們的不變量(invariants)。在Python里,__init__只是一個特殊名稱的成員函數(method),與這不同,C++里的構造器不能像普通成員函數那樣處理。特別是我們不能取它的地址: &World::World這樣會被報錯。庫提供了一個不同的接口來指定構造器。假設有:

struct World
{
    World(std::string msg); // 添加的構造器
    ...

我們可以這樣修改包裝代碼:

class_<World>("World",init<std::string>())
    .def(init<double,double>())
    ...

當然,C++類可能還有其他的構造器,我們也可以暴露他們,只需要向def()傳遞更多init<…>的實例:

class <World>("World",init<std::string>())
        .def(init<double,double>())
        ...

Boost.Python允許被包裝的函數,成員函數以及構造器被重載,以映射C++重載。

數據成員和屬性(Properties)

C++中的任何可公共的訪問的數據成員都能輕易的被包裝成只讀或者只寫屬性(attributes):

class_<World>("World",init<std::string>())
    .def_readonly("msg",&World::msg)
    ...

并直接在Python里使用:

>>> planet = hello.World('howdy')
>>> planet.msg
'howdy'

這不會導致添加屬性到World實例__dict__,從而在包裝大型數據結構時節省大量的內存。實際上,除非從Python顯式添加屬性,否則實例__dict__根本不會被創建。Python的這種能力來源于新的Python 2.2 類型系統,尤其是descriptor接口和property類型。

在C++里,可公共的訪問的數據成員被認為是糟糕設計的表現,因為他們破壞了封裝(encapsulation),文體向導(style guides)通常指示代之以"getter" 和 "setter"函數。然而在Python里,__getattr__,__setattr__和從2.2開始有的property意味著屬性進入只是程序員控制下的封裝得更好的語法工具。通過讓Python property對用戶直接可用,Boost.Python連接了二者的不同慣用語。如果msg是私有的,我們仍然能把它暴露為Python里的屬性:

class_<World>("World",init<std::string>())
    .add_property("msg",&World::greet,&World::set)
    ...

上面的例子映射了人們熟悉的Python 2.2+里的property用法:

>>> class World(object):
...     __init__(self,msg):
...         self.__msg = msg
...     def greet(self):
...         return self.__msg
...     def set(self,msg):
...         self.__msg = msg
...     msg = property(greet,set)

操作符重載

能給用戶定義類型定義算術操作符一直是兩門語言的數值計算取得成功一個重要因素。像 Numpy 這樣的軟件包的成功證明了在擴展模塊中暴露操作符能產生巨大能量。Boost.Python給包裝操作符重載提供了簡潔的機制。下面是包裝Boost的有理數庫( rational number library)的代碼的片斷:

class_<rational<int> >("rational_int")
  .def(init<int,int>()) // constructor,e.g. rational_int(3,4)
  .def("numerator",&rational<int>::numerator)
  .def("denominator",&rational<int>::denominator)
  .def(-self)        // __neg__ (unary minus)
  .def(self + self)  // __add__ (homogeneous)
  .def(self * self)  // __mul__
  .def(self + int()) // __add__ (heterogenous)
  .def(int() + self) // __radd__

這種魔法是通過簡單的應用"表達式模板"("expression templates") [VELD1995] 來施加的,"表達式模板"是一種最初為優化高性能矩陣代數表達式而開發的技術。本質是不立即進行計算,而重載操作符來構造描述計算的類型。在矩陣代數里,當考慮整個表達式的結構,而不是"貪婪的"對每步操作求值時,經常可以獲得戲劇性的優化。Boost.Python用同樣的技術來構建基于包含 self 的表達式的適當的Python 成員函數對象(method object)。

繼承

要在Boost.Python里描述C++繼承關系,可以像下面這樣把可選的bases<…>參數添加到class_<…>模板參數表里:

class_<Derived,bases<Base1,Base2> >("Derived")
     ...

這樣有兩個效果:

  1. 當class_<…>被創建時,在Boost.Python的注冊表里查找對應于Base1和Base2的Python類型對象,然后作為新的Python衍生類 型對象的基類。因而暴露給Base1和Base2的成員函數自動成為了衍生類型的成員。因為注冊表是全局的,即使暴露衍生類型的模 塊和它的任一基類的模塊不同,繼承同樣有效。
  2. 衍生類到基類的C++轉換被添加到Boost.Python注冊表。因此期待任一基類對象(的指針或引用)的被包裝C++成員函數可以被包裝 了任一基類的衍生實例的對象調用。類T的被包裝成員函數被視為具有隱式的第一個參數T&,那么為了讓衍生對象能調用基類成 員函數,這些轉換是必需的。

當然從被包裝的C++類實例衍生新的Python類是可能的。這是因為Boost.Python使用了new-style class系統,這套系統在Python內置類型上工作良好。但有一個重要細節不同: Python內置類型一般在__new__函數里建立不變量(invariants),從而衍生類不用在調用它的成員函數前調用基類的__init__:

>>> class L(list):
...      def __init__(self):
...          pass
...
>>> L().reverse()
>>>

因為C++對象構造是一步操作(one-step operation),直到參數可用C++實例數據才能被構造,在__init__函數里:

>>> class D(SomeBoostPythonClass):
...      def __init__(self):
...          pass
...
>>> D().some_boost_python_method()
Traceback (most recent call last):
  File "<stdin>",line 1,in ?
TypeError: bad argument type for built-in operation

發生錯誤的原因是Boost.Python 在實例D里找不到類型SomeBoostPythonClass的實例數據;D的__init__函數遮蓋了基類的構造函數??梢酝ㄟ^刪除D的__init__函數或是讓它顯式的調用SomeBoostPythonClass.__init__(…)來糾正錯誤。

虛函數

在Python里從擴展類衍生新的類型沒太大意思,除非它們在C++里能被多態的使用。換句話說,當在C++里通過基類指針/引用調用Python成員函數時,Python成員函數的實現應該看起來像是覆蓋(override)了C++虛函數的實現。因為要改變虛函數的行為的唯一方法是在衍生類里覆蓋(override)它,用戶必須構造一個特殊的衍生類來分派(dispatch)多態類的虛函數。:

//
// 要包裝的接口:
//
class Base
{
 public:
    virtual int f(std::string x) { return 42; }
    virtual ~Base();
};

int calls_f(Base const& b,std::string x) { return b.f(x); }

//
// 包裝代碼
//

// 分派者類(Dispatcher class)
struct BaseWrap : Base
{
    // 儲存指向Python對象的指針
    BaseWrap(PyObject* self_) : self(self_) {}
    PyObject* self;

    // 當f沒有被覆蓋(override)時的缺省實現
    int f_default(std::string x) { return this->Base::f(x); }
    // 分派實現
    int f(std::string x) { return call_method<int>(self,"f",x); }
};

...
    def("calls_f",calls_f);
    class_<Base,BaseWrap>("Base")
        .def("f",&Base::f,&BaseWrap::f_default)
        ;

下面是一些python演示代碼:

>>> class Derived(Base):
...     def f(self,s):
...          return len(s)
...
>>> calls_f(Base(),'foo')
42
>>> calls_f(Derived(),'forty-two')
9

對分派者類(Dispatcher class),要注意:

  • 允許用Python進行覆蓋(override)的關鍵因素是call_method調用,它使用了和用來包裝C++函數的注冊表相同的全局類型轉換注冊 表(global type conversion registry),來把參數從C++轉換成Python,把返回類型從Python轉換成C++
  • 任何你希望包裝的構造器署名(signatures)必須有一個的相同的初始PyObject*參數。
  • 分派者必須保存這個參數以便調用call_method時使用。
  • 當被暴露的函數不是純虛函數時,需要f_default成員函數。在類型BaseWrap的對象里,沒有其他方式可以調用Base::f,因為它被 覆蓋(override)了。

更深層次的反射即將出現(Deeper Reflection on the Horizon)?

無可否認,重復這種公式化的流程是冗長乏味的。尤其是項目里有大量多態類的時候。這反映了C++編譯時內省能力的必然限制:無法枚舉類的成員來判斷哪個是虛函數。不過,一個很有希望的項目已經啟動,致力于寫一個前端程序來從C++頭文件自動生成這些分派者類(以及其它包裝代碼)。

Pyste 是由Bruno da Silva de Oliveira開發的,基于 GCC_XML 構建。GCC_XML可以生成XML版本的GCC內部程序描述。GCC是一種高度符合(譯注:C++標準)的編譯器,從而確保了對最復雜的模板代碼的正確處理和對底層類型系統的完全訪問。和Boost.Python的哲學一致,Pyste接口描述既不侵入被包裝的代碼,也不用某種不熟悉的語言來表達,相反:它是100%的純Python腳本。如果Pyste成功的話,將標志著我們的很多用戶不用再什么都直接用C++包裝。它將允許我們選擇把一些元程序(metaprogram)代碼從C++移動到Python。我們期待不久后不僅用戶,Boost.Python開發者自己也能以混合的思路來考慮("thinking hybrid")他們自己的代碼。

序列化(Serialization)

序列化是把內存中的對象轉換成可以保存到磁盤上或通過網絡傳送的格式的過程。序列化后的對象(最常見的是簡單字符串)可以被重新取得并轉換回原來的對象。好的序列化系統能夠自動轉換整個對象層次(object hierarchies)。Python的標準pickle模塊正是這樣的系統。它利用語言強大的運行時內省來序列化幾乎是任意的用戶定義對象。加上一些簡單的非侵入限定,這種強大的設施可以被擴展成對被包裝的C++對象也有效。下面是一個例子:

#include <string>

struct World
{
    World(std::string a_msg) : msg(a_msg) {}
    std::string greet() const { return msg; }
    std::string msg;
};

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

struct World_picklers : pickle_suite
{
  static tuple
  getinitargs(World const& w) { return make_tuple(w.greet()); }
};

BOOST_PYTHON_MODULE(hello)
{
    class_<World>("World",init<std::string>())
        .def("greet",&World::greet)
        .def_pickle(World_picklers())
    ;
}

現在讓我們創建一個World對象并把它放到磁盤上:

>>> import hello
>>> import pickle
>>> a_world = hello.World("howdy")
>>> pickle.dump(a_world,open("my_world","w"))

在可能不同的計算機的可能不同的操作系統的可能不同的腳本中:

>>> import pickle
>>> resurrected_world = pickle.load(open("my_world","r"))
>>> resurrected_world.greet()
'howdy'

當然也可以用cPickle來獲得更快的處理速度。

Boost.Python的pickle_suite完全支持標準Python文檔定義的pickle協議。類似Python里的__getinitargs__函數,pickle_suite的getinitargs()負責創建參數tuple來重建被pickle了的對象。Python pickle協議中的其他元素,__getstate__ 和__setstate__可以通過C++ getstate和setstate函數來可選的提供。C++的靜態類型系統允許庫在編譯時確保沒有意義的函數組合(例如:沒有setstate 就getstate)不會被使用。

要想序列化更復雜的C++對象需要做比上面的例子稍微多點的工作。幸運的是Object接口(參見下一節)在保持代碼便于管理上幫了大忙。

對象接口(Object interface)

無所不在的PyObject*,手動引用計數,需要記住是哪個API調用返回了"新的"(自身擁有的)引用或是"借來的"(原始的)引用,這些可能有經驗的C語言擴展模塊的作者都熟悉。這些約束不僅麻煩,更是錯誤的主要來源,尤其是存在異常的時候。

Boost.Python提供了一個object類,它自動化了引用計數并提供任意類型的C++對象到Python的轉換。這極大的減輕了未來的擴展模塊作者的學習負擔。

從任一類型創建object極度簡單:

object s("hello,world");   // s 管理一個Python字符串

object可以和所有其它類型進行模板化的交互,自動的進行到Python的轉換。這一切自然得很容易被忽略不計:

object ten_Os = 10 * s[4]; // -> "oooooooooo"

上面的例子中,在索引和乘法操作被調用前,4和10被轉換成了Python對象。

extract<T>類模板可以用來把Python對對象轉換成C++類型:

double x = extract<double>(o);

如果任一方向的轉換不能執行,將在運行時拋出一個適當的異常。

伴隨object類型的是一套衍生類型,盡可能的映射Python的內置類型(list,dict,tuple等)。這樣就能方便的從C++操作這些高級類型了:

dict d;
d["some"] = "thing";
d["lucky_number"] = 13;
list l = d.keys();

這看起來和工作起來幾乎就像是通常的python代碼,但它實際上是純的C++。當然我們能包裝接受或返回object實例的函數。

從混合的思路思考(Thinking hybrid)

因為結合編程語言具有實際的和心理的困難,在進行任何實際開發前決定使用單一的語言是普遍現象。對很多應用來說,性能上的考慮決定了核心算法要用編譯語言實現。不幸的是,因為靜態類型系統的復雜性,我們為運行時性能要付出開發時間大量增長的代價。經驗表明,和開發相應的Python代碼比起來,開發可維護的C++代碼通常需要更長的時間和艱難得多才能獲得的工作經驗。即使開發者覺得只用一門編譯語言開發挺好,為了用戶,他們也經常用某種類型的特別的腳本層來補充系統,哪怕他們永遠不會得到同樣的好處。

Boost.Python讓我們能 think hybrid 。Python可以用來快速搭建新的應用的原型;在開發能工作的系統時,它的易用性和大量的標準庫使我們處于領先。需要的話,可以用能工作的代碼來找出限制速度的熱點(rate-limiting hotspots)。為了最大化性能,這些熱點可以用C++來重新實現,然后用Boost.Python綁定來連進已有的高級過程(譯注:指Python程序)。

當然,如果一開始就清楚很多算法最后不得不用C++來實現,這種 由頂至下(top-down) 的方法就沒那么吸引人了。幸運的是,Boost.Python也允許我們使用 由底至上(bottom-up) 的方法。我們非常成功的把這種方法用在了一個用于科學應用的工具箱軟件的開發上。這個工具箱開始主要是一個帶Boost.Python綁定的C++類庫,接下來有一小段時間增長主要集中在C++部分,隨著工具箱變得越來越完整,越來越多新添加的功能可以用Python來實現。

http://static.flickr.com/55/124987534_34375196e6.jpg?v=0

上圖是實現新的算法時新添加的C++代碼和Python代碼的估計比率隨時間變化的情況。我們預計這個比率會達到接近70% (Python)。能夠主要用Python而不是更困難的靜態類型語言來解決新問題,這是我們在Boost.Python上的投入的回報。我們的所有代碼都能從Python訪問,這使得更廣泛的開發者可以用它來快速開發新的應用。

開發歷史

Boost.Python的第一版是由Dave Abrahams在Dragon Systems開發的。在那里他非常榮幸的請到Tim Peters作為他的"Python之禪"( "The Zen of Python")導師。Dave的工作之一是開發基于Python的自然語言處理系統。因為最終要被用于嵌入式硬件,系統計算密集的內核總是被假設成要用C++來重寫以優化速度和內存需求量(memory footprint) [1] 。這個項目也想用Python測試腳本 [2] 來測試所有的C++代碼。當時我們知道的綁定C++和Python的唯一工具是 SWIG ,但那時它處理C++的能力比較弱。要說在那時就對Boost.Python所使用方法的可能優越之處有了什么深刻洞見,那是騙人的。Dave對花俏的C++模板技巧的興趣和嫻熟剛好到了能真正做點什么的時候,Boost.Python就那樣出現了,因為它滿足了需求,因為它看起來挺酷,值得一試。

早期的版本針對的許多基本目標和在這篇論文中描述的相同。最顯著的區別在于早期版本的語法要稍微麻煩一點,缺乏對操作符重載,pickling,基于組件的開發的專門支持。后面三個特性很快就被Ullrich Koethe和Ralf Grosse-Kunstleve [3] 加上了。其他熱心的貢獻者(contributors)也出來貢獻了一些改進,如對嵌套模塊和成員函數的支持等。

到2001年早期時開發已經穩定下來了,很少有新的特性添加,然而這時一個煩人的問題暴露出來了:Ralf已經開始在一個使用 EDG 前端的編譯器的預發布版上測試Boost.Python,這時Boost.Python內核中負責處理Python和C++的類型轉換的機制(mechanism)編譯失敗了。結果證明我們一直在利用一個bug,這個bug在所有我們測試過的C++編譯器實現中都非常普遍。我們知道隨著C++編譯器很快變得更加標準兼容,庫將開始在更多的平臺上失敗。很不幸,因為這套機制是庫的功能的中樞,解決問題看起來非常困難。

幸運的是那一年的后期,Lawrence Berkeley和后來的Lawrence Livermore National labs和 Boost Consulting 簽訂了支持Boost.Python的開發的合同。這樣就有了新的機會來處理庫的基本問題,從而確保將來的發展。重新設計開始于低級類型轉換架構,內置的標準兼容和對基于組件的開發的支持(和不得不顯式的跨越模塊邊界導入或導出轉換的第一版形成對比)。對Python和C++對象的關系進行了分析,從而能更直觀的處理C++ lvalues和rvalues。

Python 2.2里出現的強大的新類型系統使得選擇是否維護對Python 1.5.2的兼容性變得容易了:這個丟棄大量精心制作的僅僅用來模擬classic Python類的代碼的機會,好的令人無法拒絕。另外,Python iterators 和 descriptors提供了重要且優雅的工具來描述類似的C++結構。一般化了的對象接口的開發允許我們進一步把C++程序員和使用Python C API帶來的危險性和語法負擔隔離開。大量的其他特性在這個階段被加了進來,包括C++異常翻譯,改進的重載函數支持,還有最重要的用來處理指針和引用的CallPolicies。

于2002年十月,第二版的Boost.Python發布了。那以后的開發集中在改進對C++運行時多態和智能指針的支持上。特別是Peter Dimov的巧妙的boost::shared_ptr設計使我們能給混和系統開發者提供一致的接口,用于跨越語言藩籬來回移動對象而不丟失信息。

剛開始,我們擔心Boost.Python v2的復雜性會阻礙貢獻者,但 Pyste 和幾個其他重要特性的貢獻(contribution)的出現使這些擔心顯得多余了。每天出現在Python C++-sig上的問題和希望得到的改進的積壓(backlog)表明了庫正在被使用。對我們來說,未來看起來很光明。

結論

Boost.Python 實現了兩種功能豐富的優秀的語言環境間的無縫協作。因為它利用模板元編程技術來對類型和函數進行內省,用戶永遠用不著再學第三種語言:接口定義是用簡潔而可維護的C++寫的。同樣,包裝系統不用再解析C++頭文件或是描述類型系統:編譯器都給我們做了。

計算密集的任務適合強大的C++,它一般不可能用純Python來實現。然而像序列化這樣的工作,可能用Python很簡單,用C++就非常困難。假如有從底層開始構建混合系統的奢侈,我們有新的信心和動力來進行設計。