<audio id="audio" title="17 | 脚本语言：搭建高性能的混合系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/d6/a4cd64d4600479f1c11aaf9ae8e25ed6.mp3"></audio>

你好，我是Chrono。

经过了前面这么多节课的学习，相信你已经认识到了C++的高效、灵活和强大。使用现代特性，再加上标准库和第三方库，C++几乎“无所不能”。

但是，C++也有自己的“阿喀琉斯之踵”，那就是语言复杂、学习曲线陡峭、开发周期长、排错/维护成本高。

所以，C++不能完全适应现在的快速开发和迭代的节奏，最终只能退到后端、底层等领域。要想充分发挥C++的功力，就要辅助其他的语言搭建混合系统，尽量扬长避短，做好那最关键、最核心的部分，这样才能展现出它应有的价值。

由于当前的操作系统、虚拟机、解释器、引擎很多都是用C或者C++编写的，所以，使用C++，可以很容易地编写各种底层模块，为上层的Java、Go等语言提供扩展功能。

不过，今天我不去说这些大型语言，而是讲两种轻便的脚本语言：Python和Lua，看看C++怎么和它们俩实现无缝对接：以C++为底层基础，Python和Lua作为上层建筑，共同搭建起高性能、易维护、可扩展的混合系统。

## Python

Python应该是除了JavaScript以外最流行的一种脚本语言了，一直在TIOBE榜单里占据前三名的位置。而且，在新兴的大数据、人工智能、科学计算等领域，也都有着广泛的应用。很多大公司都长期招聘Python程序员，就是看中了它的高生产率。

Python本身就有C接口，可以用C语言编写扩展模块，把一些低效耗时的功能改用C实现，有的时候，会把整体性能提升几倍甚至几十倍。

但是，使用纯C语言写扩展模块非常麻烦，那么，能不能利用C++的那些高级特性来简化这部分的工作呢？

很多人都想到了这个问题，于是，就出现了一些专门的C++/Python工具，使用C++来开发Python扩展。其中，我认为最好的一个就是[pybind11](https://github.com/pybind/pybind11)。

pybind11借鉴了“前辈”Boost.Python，能够在C++和Python之间自由转换，任意翻译两者的语言要素，比如把C++的vector转换为Python的列表，把Python的元组转换为C++的tuple，既可以在C++里调用Python脚本，也可以在Python里调用C++的函数、类。

pybind11名字里的“11”表示它完全基于现代C++开发（C++11以上），所以没有兼容旧系统的负担。它使用了大量的现代C++特性，不仅代码干净整齐，运行效率也更高。

下面，我就带你看看怎么用pybind11，让C++来辅助Python，提升Python的性能。

pybind11是一个纯头文件的库，但因为必须结合Python，所以首先要有Python的开发库，然后再用pip工具安装。

pybind11支持Python2.7、Python3和PyPy，这里我用的是Python3：

```
apt-get install python3-dev
apt-get install python3-pip
pip3 install pybind11

```

pybind11充分利用了C++预处理和模板元编程，把原本无聊重复的代码都隐藏了起来，展现了“神奇的魔法”——只需要短短几行代码，就可以实现一个Python扩展模块。具体怎么实现呢？

实际上，你只要用一个宏“**PYBIND11_MODULE**”，再给它两个参数，Python模块名和C++实例对象名，就可以了。

```
#include &lt;pybind11/pybind11.h&gt;        // pybind11的头文件

PYBIND11_MODULE(pydemo, m)            // 定义Python模块pydemo
{
  m.doc() = &quot;pybind11 demo doc&quot;;      // 模块的说明文档
}                                     // Python模块定义结束

```

代码里的pydemo就是Python里的模块名，之后在Python脚本里必须用这个名字才能import。

第二个参数“m”其实是pybind11::module的一个实例对象，封装了所有的操作，比如这里的doc()就是模块的说明文档。它只是个普通的变量，起什么名字都可以，但为了写起来方便，一般都用“m”。

假设这个C++源文件名是“pybind.cpp”，现在你就可以用g++把它编译成在Python里调用的模块了，不过编译命令比较复杂：

```
g++ pybind.cpp               \                  #编译的源文件
   -std=c++11 -shared -fPIC   \                 #编译成动态库
  `python3 -m pybind11 --includes` \            #获得包含路径
  -o pydemo`python3-config --extension-suffix`  #生成的动态库名字

```

我来稍微解释一下。第一行是指定编译的源文件，第二行是指定编译成动态库，这两个不用多说。第三行调用了Python，获得pybind11所在的包含路径，让g++能够找得到头文件。第四行最关键，是生成的动态库名字，**前面必须是源码里的模块名**，而后面那部分则是Python要求的后缀名，否则Python运行时会找不到模块。

编译完后会生成一个大概这样的文件：pydemo.cpython-35m-x86_64-linux-gnu.so，现在就可以在Python里验证了，使用import导入，然后用help就能查看模块说明：

```
$ python3
&gt;&gt;&gt; import pydemo
&gt;&gt;&gt; help(pydemo)

```

刚才的代码非常简单，只是个空模块，里面什么都没有，现在，我们来看看怎么把C++的函数导入Python。

你需要用的是**def()函数**，传递一个Python函数名和C++的函数、函数对象或者是lambda表达式，形式上和Python的函数也差不多：

```
namespace py = pybind11;               // 名字空间别名，简化代码

PYBIND11_MODULE(pydemo, m)             // 定义Python模块pydemo
{
  m.def(&quot;info&quot;,                        // 定义Python函数
    []()                              // 定义一个lambda表达式
    {
      py::print(&quot;c++ version =&quot;, __cplusplus);  // pybind11自己的打印函数
      py::print(&quot;gcc version =&quot;, __VERSION__);
      py::print(&quot;libstdc++   =&quot;, __GLIBCXX__);
    }
  );  

  m.def(&quot;add&quot;,                       // 定义Python函数
    [](int a, int b)                 // 有参数的lambda表达式
    {
      return a + b;
    }
  );  
}                                     // Python模块定义结束

```

这样我们就非常轻松地实现了两个Python函数，在Python里可以验证效果：

```
import pydemo            # 导入pybind11模块
pydemo.info()            # 调用C++写的函数
x = pydemo.add(1,2)      # 调用C++写的函数

```

pybind11也支持函数的参数、返回值使用标准容器，会自动转换成Python里的list、dict，不过你需要额外再包含一个“stl.h”的头文件。

下面的示例代码演示了C++的string、tuple和vector是如何用于Python的：

```
#include &lt;pybind11/stl.h&gt;              // 转换标准容器必须的头文件

PYBIND11_MODULE(pydemo, m)             // 定义Python模块pydemo
{
  m.def(&quot;use_str&quot;,                     // 定义Python函数
    [](const string&amp; str)              // 入参是string
    {
        py::print(str);
        return str + &quot;!!&quot;;            // 返回string
    }
  );  

  m.def(&quot;use_tuple&quot;,                  // 定义Python函数
    [](tuple&lt;int, int, string&gt; x)     // 入参是tuple
    {
        get&lt;0&gt;(x)++;
        get&lt;1&gt;(x)++;
        get&lt;2&gt;(x)+= &quot;??&quot;;
        return x;                     // 返回元组
    }
  );  

  m.def(&quot;use_list&quot;,                   // 定义Python函数
    [](const vector&lt;int&gt;&amp; v)          // 入参是vector
    {
        auto vv = v;
        py::print(&quot;input :&quot;, vv);
        vv.push_back(100);
        return vv;                    // 返回列表       
    }
  );
}


```

因为都是面向对象的编程语言，C++里的类也能够等价地转换到Python里面调用，这要用到一个特别的模板类class_，注意，它有意模仿了关键字class，后面多了一个下划线。

我拿一个简单的Point类来举个例子：

```
class Point final
{
public:
    Point() = default;
    Point(int a)；
public:
    int get() const;
    void set(int a);
};

```

使用pybind11，你需要在模板参数里写上这个类名，然后在构造函数里指定它在Python里的名字。

导出成员函数还是调用函数def()，但它会返回对象自身的引用，所以就可以连续调用，在一句话里导出所有接口：

```
py::class_&lt;Point&gt;(m, &quot;Point&quot;)           // 定义Python类
    .def(py::init())                    // 导出构造函数
    .def(py::init&lt;int&gt;())               // 导出构造函数
    .def(&quot;get&quot;, &amp;Point::get)            // 导出成员函数
    .def(&quot;set&quot;, &amp;Point::set)            // 导出成员函数
    ;

```

对于一般的成员函数来说，定义的方式和普通函数一样，只是你必须加上取地址操作符“&amp;”，把它写成函数指针的形式。而构造函数则比较特殊，必须调用init()函数来表示，如果有参数，还需要在init()函数的模板参数列表里写清楚。

pybind11的功能非常丰富，我们不可能一下子学完全部的功能，刚才说的这些只是最基本，也是非常实用的功能。除了这些，它还支持异常、枚举、智能指针等很多C++特性，你可以再参考一下它的[文档](https://github.com/pybind/pybind11)，学习一下具体的方法，挖掘出它的更多价值。

如果你在工作中重度使用Python，那么pybind11绝对是你的得力助手，它能够让C++紧密地整合进Python应用里，让Python跑得更快、更顺畅，建议你有机会就尽量多用。

## Lua

接下来我要说的第二个脚本语言是小巧高效的Lua，号称是“最快的脚本语言”。

你可能对Lua不太了解，但你一定听说过《魔兽世界》《愤怒的小鸟》吧，它们就在内部大量使用了Lua来编写逻辑。在游戏开发领域，Lua可以说是一种通用的工作语言。

Lua与其他语言最大的不同点在于它的设计目标：不追求“大而全”，而是“小而美”。Lua自身只有很小的语言核心，能做的事情很少。但正是因为它小，才能够很容易地嵌入到其他语言里，为“宿主”添加脚本编程的能力，让“宿主”更容易扩展和定制。

标准的Lua（PUC-Rio Lua）使用解释器运行，速度虽然很快，但和C/C++比起来还是有差距的。所以，你还可以选择另一个兼容的项目：LuaJIT（[https://luajit.org/](https://luajit.org/)）。它使用了JIT（Just in time）技术，能够把Lua代码即时编译成机器码，速度几乎可以媲美原生C/C++代码。

不过，LuaJIT也有一个问题，它是一个个人项目，更新比较慢，最新的2.1.0-beta3已经是三年前的事情了。所以，我推荐你改用它的一个非官方分支：OpenResty-LuaJIT（[https://github.com/openresty/luajit2](https://github.com/openresty/luajit2)）。它由OpenResty负责维护，非常活跃，修复了很多小错误。

```
git clone git@github.com:openresty/luajit2.git
make &amp;&amp; make install


```

和Python一样，Lua也有C接口用来编写扩展模块，但因为它比较小众，所以C++项目不是很多。现在我用的是LuaBridge，虽然它没有用到太多的C++11新特性，但也足够好。

LuaBridge是一个纯头文件的库，只要下载下来，把头文件拷贝到包含路径，就能够直接用：

```
git clone git@github.com:vinniefalco/LuaBridge.git

```

我们先来看看在Lua里怎么调C++的功能。

和前面说的pybind11类似，LuaBridge也定义了很多的类和方法，可以把C++函数、类注册到Lua里，让Lua调用。

但我不建议你用这种方式，因为我们现在有LuaJIT。它内置了一个ffi库（Foreign Function Interface），能够在Lua脚本里直接声明接口函数、直接调用，不需要任何的注册动作，更加简单方便。而且这种做法还越过了Lua传统的栈操作，速度也更快。

使用ffi唯一要注意的是，**它只能识别纯C接口，不认识C++**，所以，写Lua扩展模块的时候，内部可以用C++，但对外的接口必须转换成纯C函数。

下面我写了一个简单的add()函数，还有一个全局变量，注意里面必须要用extern "C"声明：

```
extern &quot;C&quot; {                // 使用纯C语言的对外接口
int num = 10; 
int my_add(int a, int b); 
}

int my_add(int a, int b)    // 一个简单的函数，供Lua调用
{
    return a + b;
}

```

然后就可以用g++把它编译成动态库，不像pybind11，它没有什么特别的选项：

```
g++ lua_shared.cpp -std=c++11 -shared -fPIC -o liblua_shared.so

```

在Lua脚本里，你首先要用ffi.cdef声明要调用的接口，再用ffi.load加载动态库，这样就会把动态库所有的接口都引进Lua，然后就能随便使用了：

```
local ffi = require &quot;ffi&quot;            -- 加载ffi库
local ffi_load = ffi.load            -- 函数别名
local ffi_cdef = ffi.cdef

ffi_cdef[[                           // 声明C接口
int num;
int my_add(int a, int b);
]]

local shared = ffi_load(&quot;./liblua_shared.so&quot;)    -- 加载动态库 

print(shared.num)                                -- 调用C接口
local x = shared.my_add(1, 2)                    -- 调用C接口

```

在ffi的帮助下，让Lua调用C接口几乎是零工作量，但这并不能完全发挥出Lua的优势。

因为和Python不一样，Lua很少独立运行，大多数情况下都要嵌入在宿主语言里，被宿主调用，然后再“回调”底层接口，利用它的“胶水语言”特性去粘合业务逻辑。

要在C++里嵌入Lua，首先要调用函数**luaL_newstate()**，创建出一个Lua虚拟机，所有的Lua功能都要在它上面执行。

因为Lua是用C语言写的，Lua虚拟机用完之后必须要用函数**lua_close()**关闭，所以最好用RAII技术写一个类来自动管理。可惜的是，LuaBridge没有对此封装，所以只能自己动手了。这里我用了智能指针shared_ptr，在一个lambda表达式里创建虚拟机，顺便再打开Lua基本库：

```
auto make_luavm = []()              // lambda表达式创建虚拟机
{
    std::shared_ptr&lt;lua_State&gt; vm(  // 智能指针
        luaL_newstate(), lua_close  // 创建虚拟机对象，设置删除函数
        );
    luaL_openlibs(vm.get());        // 打开Lua基本库

    return vm; 
};
#define L vm.get()                  // 获取原始指针，宏定义方便使用

```

在LuaBridge里，一切Lua数据都被封装成了**LuaRef**类，完全屏蔽了Lua底层那难以理解的栈操作。它可以隐式或者显式地转换成对应的数字、字符串等基本类型，如果是表，就可以用“[]”访问成员，如果是函数，也可以直接传参调用，非常直观易懂。

使用LuaBridge访问Lua数据时，还要注意一点，它只能用函数**getGlobal()**看到全局变量，所以，如果想在C++里调用Lua功能，就一定不能加“local”修饰。

给你看一小段代码，它先创建了一个Lua虚拟机，然后获取了Lua内置的package模块，输出里面的默认搜索路径path和cpath：

```
auto vm = make_luavm();                  // 创建Lua虚拟机
auto package = getGlobal(L, &quot;package&quot;);  // 获取内置的package模块

string path  = package[&quot;path&quot;];          // 默认的lua脚本搜索路径
string cpath = package[&quot;cpath&quot;];         // 默认的动态库搜索路径

```

你还可以调用**luaL_dostring()和luaL_dofile()**这两个函数，直接执行Lua代码片段或者外部的脚本文件。注意，luaL_dofile()每次调用都会从磁盘载入文件，所以效率较低。如果是频繁调用，最好把代码读进内存，存成一个字符串，再用luaL_dostring()运行：

```
luaL_dostring(L, &quot;print('hello lua')&quot;);  // 执行Lua代码片段
luaL_dofile(L, &quot;./embedded.lua&quot;);        // 执行外部的脚本文件

```

在C++里嵌入Lua，还有另外一种方式：**提前在脚本里写好一些函数，加载后在C++里逐个调用**，这种方式比执行整个脚本更灵活。

具体的做法也很简单，先用luaL_dostring()或者luaL_dofile()加载脚本，然后调用getGlobal()从全局表里获得封装的LuaRef对象，就可以像普通函数一样执行了。由于Lua是动态语言，变量不需要显式声明类型，所以写起来就像是C++的泛型函数，但却更简单：

```
string chunk = R&quot;(                    -- Lua代码片段
    function say(s)                   -- Lua函数1
        print(s)
    end
    function add(a, b)                -- Lua函数2
        return a + b
    end
)&quot;;

luaL_dostring(L, chunk.c_str());      // 执行Lua代码片段

auto f1 = getGlobal(L, &quot;say&quot;);        // 获得Lua函数
f1(&quot;say something&quot;);                  // 执行Lua函数

auto f2 = getGlobal(L, &quot;add&quot;);        // 获得Lua函数
auto v = f2(10, 20);                  // 执行Lua函数

```

只要掌握了上面的这些基本用法，并合理地划分出C++与Lua的职责边界，就可以搭建出“LuaJIT + LuaBridge + C++”的高性能应用，运行效率与开发效率兼得。比如说用C++写底层的框架、引擎，暴露出各种调用接口作为“业务零件”，再用灵活的Lua脚本去组合这些“零件”，写上层的业务逻辑。

## 小结

好了，今天我讲了怎么基于C++搭建混合系统，介绍了Python和Lua这两种脚本语言。

Python很“大众”，但比较复杂、性能不是特别高；而Lua比较“小众”，很小巧，有LuaJIT让它运行速度极快。你可以结合自己的实际情况来选择，比如语言的熟悉程度、项目的功能/性能需求、开发的难易度，等等。

今天的内容也比较多，我简单小结一下要点：

1. C++高效、灵活，但开发周期长、成本高，在混合系统里可以辅助其他语言，编写各种底层模块提供扩展功能，从而扬长避短；
1. pybind11是一个优秀的C++/Python绑定库，只需要写很简单的代码，就能够把函数、类等C++要素导入Python；
1. Lua是另一种小巧快速的脚本语言，它的兼容项目LuaJIT速度更快；
1. 使用LuaBridge可以导出C++的函数、类，但直接用LuaJIT的ffi库更好；
1. 使用LuaBridge也可以很容易地执行Lua脚本、调用Lua函数，让Lua跑在C++里。

## 课下作业

最后是课下作业时间，给你留两个思考题：

1. 你觉得使用脚本语言与C++搭建混合系统有什么优势？
1. 你觉得“把C++嵌入脚本语言”和“把脚本语言嵌入C++”有什么区别，哪种方式更好？

欢迎你在留言区写下你的思考和答案，如果觉得今天的内容对你有所帮助，也欢迎分享给你的朋友。我们下节课见。

<img src="https://static001.geekbang.org/resource/image/e4/2b/e47906e7f83ec210cc011e2652eee12b.jpg" alt="">
