pybind11内嵌解释器

发布时间 2023-06-24 22:01:40作者: 夏蝉沐雪

一、创建解释器

需要在使用任意Python API前初始化解释器,包括pybind11 Python函数和类。RAII guard类`scoped_interpreter`可用来管理解释器的生命周期。在guard类销毁时,解释器将会关闭并占用的内存。必须在所有Python函数前调用它。

#include <pybind11/embed.h> // everything needed for embedding
namespace py = pybind11;

int main() {
    py::scoped_interpreter guard{}; // start the interpreter and keep it alive

    py::print("Hello, World!"); // use the Python API
}

也可以使用pybind11 API来实现相同的功能:(参见PythonC++接口)

#include <pybind11/embed.h>
namespace py = pybind11;

int main() {
    py::scoped_interpreter guard{};

    py::exec(R"(
        kwargs = dict(name="World", number=42)
        message = "Hello, {name}! The answer is {number}".format(**kwargs)
        print(message)
    )");
}
#include <pybind11/embed.h>
namespace py = pybind11;
using namespace py::literals;

int main() {
    py::scoped_interpreter guard{};

    auto kwargs = py::dict("name"_a="World", "number"_a=42);
    auto message = "Hello, {name}! The answer is {number}"_s.format(**kwargs);
    py::print(message);
}

两种方法也可以混合使用:

#include <pybind11/embed.h>
#include <iostream>

namespace py = pybind11;
using namespace py::literals;

int main() {
    py::scoped_interpreter guard{};

    auto locals = py::dict("name"_a="World", "number"_a=42);
    py::exec(R"(
        message = "Hello, {name}! The answer is {number}".format(**locals())
    )", py::globals(), locals);

    auto message = locals["message"].cast<std::string>();
    std::cout << message;
}

 二、导入模块

使用`module_::import()`可以导入Python模块。

py::module_ sys = py::module_::import("sys");
py::print(sys.attr("path"));

为方便起见,内嵌解释器时,会将当前工作路径包含到`sys.path`中。这样我们可以方便地导入本地Python文件。

```python
"""calc.py located in the working directory"""


def add(i, j):
    return i + j
```

```c++
py::module_ calc = py::module_::import("calc");
py::object result = calc.attr("add")(1, 2);
int n = result.cast<int>();
assert(n == 3);
```

如果运行时源文件被修改(如被外部进程修改),可以使用`module_::reload()`重新导入模块。这在下面的场景中十分有用:有个应用程序要导入用户定义数据处理脚本,该脚本需要在用户修改后更新时。注意,这个函数不会递归地重新加载模块。

三、添加内嵌模块

使用宏`PYBIND11_EMBEDDED_MODULE`可以添加内嵌的二进制模块。这个定义需要放在全局作用域中。定义后,他们可以向其他模块一样导入。

#include <pybind11/embed.h>
namespace py = pybind11;

PYBIND11_EMBEDDED_MODULE(fast_calc, m) {
    // `m` is a `py::module_` which is used to bind functions and classes
    m.def("add", [](int i, int j) {
        return i + j;
    });
}

int main() {
    py::scoped_interpreter guard{};

    auto fast_calc = py::module_::import("fast_calc");
    auto result = fast_calc.attr("add")(1, 2).cast<int>();
    assert(result == 3);
}

与只能创建单个二进制模块的扩展模块不同,在嵌入式方面,可以使用多个“PYBIND11_embedded_module”定义添加无限数量的模块(只要它们具有唯一的名称)。这些模块被添加到Python的内置列表中,因此它们也可以导入到解释器加载的纯Python文件中。一切自然互动:

"""py_module.py located in the working directory"""
import cpp_module

a = cpp_module.a
b = a + 1
#include <pybind11/embed.h>
namespace py = pybind11;

PYBIND11_EMBEDDED_MODULE(cpp_module, m) {
    m.attr("a") = 1;
}

int main() {
    py::scoped_interpreter guard{};

    auto py_module = py::module_::import("py_module");

    auto locals = py::dict("fmt"_a="{} + {} = {}", **py_module.attr("__dict__"));
    assert(locals["a"].cast<int>() == 1);
    assert(locals["b"].cast<int>() == 2);

    py::exec(R"(
        c = a + b
        message = fmt.format(a, b, c)
    )", py::globals(), locals);

    assert(locals["c"].cast<int>() == 3);
    assert(locals["message"].cast<std::string>() == "1 + 2 = 3");
}

四、解释器的生命周期

当 `scoped_interpreter` 销毁时,程序会自动关闭Python解释器。后面再创建一个新的示例会重启解释器。或者,我们也可以使用 `initialize_interpreter` / `finalize_interpreter` 这组函数在任意时刻直接设置解释器状态。

解释器重启后,pybind11创建的模块可以安全地重新初始化,但第三方扩展模块可能会有些问题。问题在于Python本身不能完全卸载扩展模块,并且会有一些解释器重启的警告。简而言之,由于Python引用循环或用户创建的全局数据,并非所有内存都可能被释放。具体细节可以查看CPython文档。