CppCon 2014 学习:Lightning Talk: Writing a Python Interpreter for Fun and Profit
Lightning Talk: Writing a Python Interpreter for Fun and Profit
这段内容在讲的是 Python 的执行模型,尤其是 CPython 的工作流程。下面是逐步解析:
Python 是动态类型语言(Dynamically typed)
- 变量类型在运行时决定。
x = 42 # x 是 int x = "hello" # x 变成 str
Python 是解释型语言(Interpreted)
- Python 代码不是直接编译成机器码,而是先编译成字节码(bytecode),然后由解释器执行。
CPython 是官方实现
- CPython 是 python.org 提供的默认实现。
- 用 C语言 编写。
- 把
.py
文件编译成字节码(.pyc
),并由 CPython 解释器执行。
示例代码
Python 代码:
def func(a, b):c = a + bprint(c)return c
编译为字节码(Byte-code)
CPython 编译器会把上面的函数编译成类似这样的虚拟机指令(伪字节码):
PUSH a
PUSH b
BINARY_ADD # 相当于 c = a + b
POP c
PUSH c
PRINT_ITEM # 打印 c
PRINT_NEWLINE
PUSH c
RETURN_VALUE # 返回 c
解释说明:
- PUSH:把变量压入栈。
- BINARY_ADD:执行加法操作(从栈中弹出两个值,相加后再压入栈)。
- POP c:把加法结果赋值给
c
。 - PRINT_ITEM + PRINT_NEWLINE:打印变量。
- RETURN_VALUE:函数返回。
CPython 执行过程
- Python 源码被编译成字节码。
- 字节码被 CPython 的 虚拟机解释器执行(使用 eval loop)。
- 虚拟机使用 栈(stack-based VM) 执行操作。
总结
概念 | 说明 |
---|---|
动态类型 | 变量类型运行时决定 |
解释型语言 | 运行时解释执行,而不是编译成本地机器码 |
CPython | 官方 Python 实现,用 C 写的 |
字节码 | Python 编译后的一种中间表示,供解释器执行 |
虚拟机执行模型 | 使用栈结构逐步执行指令 |
你这段内容是在描述 Python 的对象模型、引用计数机制,以及如何通过嵌入 Python(如在 C++ 中)执行 Python 代码。我们来逐步解释:
一切皆对象(Everything is an object)
在 Python 中,所有东西都是对象:
类型 | 说明 |
---|---|
int | 整数对象 |
str | 字符串对象 |
list | 列表对象 |
dict | 字典对象 |
bool | 布尔对象 |
func | 函数对象 |
method | 方法对象 |
class | 类对象 |
module | 模块对象 |
无论是什么类型,在内存中都表现为一个对象结构,具有类型信息和引用计数。 |
引用计数(Reference Counting)
Python(CPython 实现)使用引用计数作为垃圾回收机制的一部分:
a = "Hello!"
b = a
解释:
"Hello!"
是一个字符串对象。a
引用它后,引用计数 = 1。b = a
后,引用计数变成 2。- 这就像 C++ 中的
std::shared_ptr
。
示意图:
a ─┐│▼+---------------+| "Hello!" | <-- 对象| count = 2 | <-- 引用计数| type = str |+---------------+▲│b ─┘
当引用计数为 0,CPython 会自动销毁该对象。
嵌入 Python:用 C++ 调用 Python
你下面的部分在描述 C++ 嵌入 Python 的场景(比如通过 CPython API):
// 假设是伪代码或嵌入式解释器伪逻辑
void test() {Ref module = vm.import(R"**(def pyfunc(a, b):return a + b)**");// MyInterpreter 执行 pyfunc
}
解释:
- 使用
vm.import()
可能是某个虚拟机对象载入 Python 模块。 - 多用于 C++ 嵌入式解释器或游戏引擎中嵌入脚本语言。
R"**( ... )**"
是原始字符串字面量,表示传入的是多行 Python 脚本。
这种嵌入方式常用于:- 嵌入 Python 脚本到 C++ 游戏/系统。
- 提供可扩展插件系统。
- 自动化测试、动态执行配置脚本等。
总结图解
Python 对象模型:
+---------------------+
| PyObject (头部) |
| - refcount | <-- 引用计数
| - type* (PyType) | <-- 类型信息
+---------------------+
| 数据 |
+---------------------+
总结要点
关键点 | 说明 |
---|---|
一切都是对象 | 所有变量/类型都是对象 |
引用计数 | 用来自动回收对象 |
引用共享 | 多个变量引用同一个对象 |
嵌入式解释器 | C++ 可以调用/执行 Python 代码 |
描述如何在 C++ 中嵌入 Python 并调用 Python 函数。虽然是伪代码(不是标准 CPython API),但表达的概念非常清楚。下面是逐行解释和你应该理解的核心概念:
场景:C++ 调用 Python 代码
你想实现:
- 在 C++ 中执行 Python 脚本(定义函数)。
- 调用 Python 中定义的函数
pyfunc(a, b)
。 - 获取返回值并转换为 C++ 类型。
分析你的伪代码
void test()
{// 虚构的解释器对象Interpreter vm;// 导入一个 Python 模块(通过原始字符串定义)Ref module = vm.import(R"**(def pyfunc(a, b):return a + b)**");// 调用 Python 中的 pyfunc 函数Ref resObj = module.call("pyfunc", 1, 2);// 提取 Python 对象为 C++ int 类型int result = extract<int>(resObj);
}
背后理解
1. vm.import(...)
- 表示将一段 Python 脚本动态“导入”或加载为模块。
R"**(...)"
是 C++ 的原始字符串字面量,表示不会转义,适合多行代码。
等价于把 Python 代码:
def pyfunc(a, b):return a + b
注入到解释器中。
2. module.call("pyfunc", 1, 2);
- 动态调用 Python 函数
pyfunc(1, 2)
。 - 返回值是 Python 对象(封装为
Ref
类型,可能是某种智能指针或包装类)。
3. extract<int>(resObj);
- 把 Python 对象转成 C++ 中的
int
类型。 - 相当于你得到了
3
,因为1 + 2 = 3
。
对应的真实 CPython API 示例
你上面的伪代码用的是某个抽象封装(比如 boost::python、pybind11、或自定义解释器)。如果用 原生 CPython API 来做相同的事情,代码类似这样:
#include <Python.h>
void test() {Py_Initialize();const char* code = R"(
def pyfunc(a, b):return a + b
)";PyRun_SimpleString(code);PyObject* main = PyImport_AddModule("__main__");PyObject* global_dict = PyModule_GetDict(main);PyObject* func = PyDict_GetItemString(global_dict, "pyfunc");if (func && PyCallable_Check(func)) {PyObject* args = PyTuple_Pack(2, PyLong_FromLong(1), PyLong_FromLong(2));PyObject* result = PyObject_CallObject(func, args);long c_result = PyLong_AsLong(result);printf("Result: %ld\n", c_result);Py_DECREF(args);Py_DECREF(result);}Py_Finalize();
}
总结理解点
关键词 | 含义 |
---|---|
R"**(...)" | 原始字符串,方便写多行 Python 代码 |
vm.import(...) | 类似于 Python 模块导入,但用于动态注入脚本 |
.call(...) | 调用 Python 函数并返回 Python 对象 |
extract<T> | 把 Python 对象转换为 C++ 类型 |
CPython API | 可以直接用 Py_Initialize , PyRun_SimpleString , PyObject* 调用 |
这段代码展示了 在 C++ 中注册 C++ 函数给 Python 使用,并在 Python 中调用该函数,这是嵌入 Python 的一个高级特性,常用于游戏引擎、嵌入式脚本系统等。下面是详细解释:
代码理解
int cxxfunc(const string& msg) {cout << msg;return 42;
}
这是一个普通的 C++ 函数,接受一个字符串,打印出来,返回 42。
void test()
{Interpreter vm; // 假设你有一个内嵌的 Python 虚拟机对象// 向 Python 注册 C++ 函数vm.builtins.def("cxxfunc", cxxfunc);// 导入/执行 Python 脚本Ref module = vm.import(R"**(
def pyfunc(a, b):cxxfunc("Hello!")return a + b
)**");// 调用 Python 函数Ref resObj = module.call("pyfunc", 1, 2);
}
总结这个过程
步骤 | 描述 |
---|---|
1. 定义 C++ 函数 | int cxxfunc(const string&) ,将来让 Python 调用它。 |
2. 注册到 Python | vm.builtins.def("cxxfunc", cxxfunc); 这一步把 C++ 函数注册为 Python 可用的全局函数。 |
3. 编写 Python 脚本 | Python 调用了 cxxfunc("Hello!") 。 |
4. 调用 Python 函数 | C++ 端用 .call("pyfunc", 1, 2) 执行 pyfunc 。 |
背后机制(类比 pybind11 或 Boost.Python)
你用的 vm.builtins.def(...)
类似于 pybind11 中的:
py::module_::import("__main__").attr("cxxfunc") = py::cpp_function(cxxfunc);
实际效果
运行这段代码时:
- 控制台会打印:
Hello!
- Python 函数
pyfunc(1, 2)
会返回3
,即a + b
。 cxxfunc
被 Python 调用时会输出字符串并返回 42(虽然这个返回值在 Python 中没用到)。
应用场景
这种嵌入式调用方式可用于:
场景 | 示例 |
---|---|
游戏脚本 | Python 调用 playSound("boom.wav") 实际执行 C++ 函数 |
工业控制 | Python 写控制逻辑,但实际操作硬件是底层 C++ |
数据处理 | 用 Python 组织逻辑流程,用 C++ 实现高性能计算 |
结论
你已经实现了:
- 从 C++ 向 Python 注册函数;
- Python 脚本中调用 C++ 函数;
- Python 和 C++ 之间来回传递数据。
这是 C++ 与 Python 脚本系统集成的核心能力。
你这段代码展示了如何在 C++ 中将一个 C++ 类(含成员函数)暴露给 Python 解释器,并让 Python 调用它的成员函数。这是一种高级的 C++ ↔ Python 绑定机制,适合嵌入式脚本、游戏开发、插件系统等场景。
下面逐步解析并翻译这段代码的意义。
逐行分析
struct Thing {void action(int a, int b);
};
定义了一个简单的 C++ 结构体 Thing
,里面有一个成员函数 action
。
void test() {Interpreter vm; // 创建解释器对象
假设 Interpreter
是你封装的 Python 虚拟机对象。
Ref m = vm.builtins;
获取内置模块,用于注册类和函数。
Ref c = m.class_<Thing>("Thing").def("action", &Thing::action);
向 Python 暴露 C++ 的类 Thing
,并绑定其成员函数 action
为 Python 可调用函数。
这段伪代码类似于 PyBind11 中的:
py::class_<Thing>(m, "Thing").def("action", &Thing::action);
Thing t;Ref wrap_t = c.wrapInstance(t);wrap_t.call("action", 1, 2);
}
- 创建一个 C++ 的
Thing
实例。 wrapInstance
把这个实例转换成 Python 端可识别的对象(通常是某种封装指针)。- 通过 Python 的方式调用
action(1, 2)
—— 实际调用的是t.action(1, 2)
。
背后机制
操作 | 作用 |
---|---|
.class_<Thing>("Thing") | 把 C++ 类注册为 Python 类。 |
.def("action", &Thing::action) | 绑定成员函数到 Python。 |
wrapInstance(t) | 把 C++ 实例包成 Python 对象。 |
call("action", ...) | 在 Python 层调用方法(但底层执行 C++ 函数)。 |
示例效果
Python 中表现为:
obj = Thing()
obj.action(1, 2)
这调用的是你写的 C++ Thing::action(1, 2)
。
总结
你这段代码描述了以下概念:
概念 | 描述 |
---|---|
C++ 类暴露给 Python | 允许 Python 实例化 C++ 类,并调用其成员函数 |
Python 调用成员函数 | Python 的 .call(...) 最终会回调到 C++ 成员函数 |
脚本与底层集成 | 非常适用于游戏开发、嵌入式设备等对性能和可扩展性都有要求的项目 |
你这段代码展示了一个 C++ 可变参数模板 + 类型擦除 + 参数打包 + 调用解释器函数的机制,常用于实现 通用的脚本函数调用接口,比如将 C++ 参数传给一个 Python 函数。
逐段理解
1. 基础 unpack 函数(递归终止)
void unpack(vector<Ref>& v) {}
当参数包为空时,递归终止。什么都不做。
2. 参数展开 + 转换 + 收集
template<typename T, typename ...Args>
void unpack(vector<Ref>& v, const T&& a, const Args&&... args) {v.push_back(objectFromT(std::forward<T>(a)));unpack(v, std::forward<Args>(args)...);
}
这段是参数展开递归模板的核心:
步骤 | 描述 |
---|---|
1 | 把当前参数 a 转换成一个 Ref (可能是 Python 对象) |
2 | objectFromT(...) 是类型擦除函数:将任意类型 T 转为统一的 Ref 类型(用于解释器调用) |
3 | 把 Ref 放入 vector<Ref> 容器中 |
4 | 递归处理剩下的参数 |
3. 最终接口:call(...)
template<typename ...Args>
Ref call(const string& name, const Args&&... args) {vector<Ref> argv;unpack(argv, std::forward<Args>(args)...);runCode(lookup(name), argv);
}
这是你提供给用户调用的通用接口:
步骤 | 含义 |
---|---|
name | 要调用的函数名(通常是 Python 脚本中的函数) |
args... | 任意数量的 C++ 参数 |
unpack(...) | 把 C++ 参数全部转换为 Ref 类型并放入 argv 中 |
lookup(name) | 查找解释器中函数的对象(比如 Python 中的 pyfunc ) |
runCode(...) | 用解释器执行函数并传入参数 |
背后思想:类型擦除 + 参数打包
objectFromT(T)
:将int
,string
,double
,Thing*
等全部转换为统一Ref
类型。vector<Ref>
:作为解释器通用参数容器。unpack(...)
:实现递归参数收集。call(...)
:简洁通用的接口隐藏了复杂的参数转换和打包过程。
举个例子
你这样调用:
call("pyfunc", 1, 3.14, "hello", someObject);
实际做了什么:
objectFromT(1)
→Ref(int)
objectFromT(3.14)
→Ref(double)
objectFromT("hello")
→Ref(str)
objectFromT(someObject)
→Ref(object)
- 调用解释器中函数
pyfunc
,传入参数[1, 3.14, "hello", someObject]
总结
你实现了一个 可扩展、类型安全、语法简洁的 C++ 调用解释器函数接口。它支持:
- 任意参数数量(可变参数模板)
- 类型转换封装(
objectFromT
) - 与 Python / Lua / JS 等解释器无缝连接(
Ref
,runCode
,lookup
)
这正是许多嵌入式脚本系统的底层核心机制。
这段代码是一个虚拟机(VM)执行字节码的解释器主循环的简化版,核心思想和 Python 的 CPython 虚拟机非常相似。下面为你逐行解析其含义,并总结设计背后的思想。
你的函数定义
Ref runCode(ByteCode code, vector<Ref> args) {Frame f(args);int pc = 0;
runCode(...)
: 执行解释器中的函数体(字节码)。code
: 表示某个函数或脚本的字节码序列(每 2 字节一个指令 + 参数)。args
: 调用时传入的参数。Frame f(args)
: 新建一个栈帧,保存局部变量、栈等上下文。pc
: 程序计数器(Program Counter),指向当前字节码的偏移量。
主循环:解释执行字节码
while (true) {byte opcode = code[pc];byte param = code[pc + 1];
一次取出一个指令(2 字节),其中:
opcode
:操作码,表示要做什么(如 PUSH、POP、JUMP 等)param
:参数(比如变量索引、跳转地址、参数数量等)
指令解释
switch (opcode) {
你实现了一个字节码解释器的指令集,每种操作码做对应的行为。
PUSH
case PUSH:f.stack.push(f.vars[param]);break;
- 从局部变量数组中取出变量
vars[param]
- 推入运行时栈
stack
POP
case POP:f.vars[param] = f.stack.pop();break;
- 从栈中弹出值
- 存入变量槽
vars[param]
JUMP_TO
case JUMP_TO:pc = param;continue;
- 改变
pc
,跳转到指定位置(param
是偏移量) - 注意
continue
跳过了pc += 2
,不会前进
CALL_FUNCTION
case CALL_FUNCTION:runCode(pop(), popArgs(param));break;
pop()
:从栈中弹出被调用函数(或字节码对象)popArgs(param)
:从栈中弹出param
个参数(调用参数)- 递归调用
runCode(...)
RETURN_VALUE
case RETURN_VALUE:return f.stack.pop();
- 函数结束,返回结果(从栈顶取出)
👣 继续前进
pc += 2;
默认每条指令占 2 字节,所以每轮前进 2 字节。
嵌套调用演示
runCode()
runCode()
runCode()
这说明你支持嵌套函数调用,每次 CALL_FUNCTION
会进入一个新的 Frame
并递归调用 runCode(...)
,这就是解释器的函数调用栈机制。
你实现的核心概念:
概念 | 描述 |
---|---|
字节码虚拟机 | 用程序计数器逐字节解释执行指令 |
栈帧 Frame | 包含局部变量数组 vars 、运行栈 stack 等上下文 |
操作码 opcode | 表示操作类型,例如 PUSH、CALL、RETURN 等 |
参数 param | 当前指令的辅助参数 |
函数调用 | 通过 CALL_FUNCTION 实现函数嵌套和参数传递 |
返回值 | 从当前栈帧返回计算结果 |
总结
你实现的是一个简化的 C++ 字节码解释器运行时核心,具备如下特性:
- 字节码驱动控制流程(如跳转、调用)
- 支持栈式计算模型
- 局部变量访问
- 支持函数调用、返回机制
- 非常类似 Python、Lua 的 VM 设计
代码片段展示了一个简化的 引用计数型对象系统,类似 Python 或 JavaScript 的对象模型。下面是对各个结构和行为的详细中文解释,帮你完全理解这段代码的设计思想和用途。
整体设计目标
你在用类似 Python 的方式构建一个 统一对象类型系统,其中:
- 所有值(int、string、list、dict)都继承自
Object
。 - 所有对象都通过
Ref
来间接引用(类似智能指针)。 - 每个对象通过
m_refcount
管理生命周期(引用计数)。
各部分详细解析
Object
struct Object {int m_refcount;
};
- 所有对象的基类。
- 包含
m_refcount
,用于引用计数,控制对象何时析构。
基本对象类型(派生类)
Str
struct Str : Object {std::string v;
};
- 表示字符串对象。
- 成员
v
是具体的字符串值。
Int
struct Int : Object {int v;
};
- 表示整数对象。
v
是其数值。
List
struct List : Object {std::vector<Ref> v;
};
- 表示列表对象。
- 存储一个
Ref
类型的向量,即引用了其它对象。
Dict
struct Dict : Object {std::map<int, Ref> v;
};
- 表示字典(键值对),这里的键是
int
类型,值是Ref
。 - 可类比 Python 的
dict
。
📎 Ref
:对象引用
struct Ref {Object* m_ptr;
};
Ref
是一个轻量对象引用类型,等价于智能指针。- 指向真实对象(继承自
Object
的任何派生类)。
应该配合引用计数增加/减少使用,例如类似 boost::intrusive_ptr 或手动实现 retain/release。
类型转换与提取
template<typename T>
T extract(Ref ref);
这是一个泛型模板函数,目的是从 Ref
中提取具体类型值。
特化版本(提取 std::string
)
template<>
std::string extract(Ref ref) {return dynamic_cast<Str*>(ref.m_ptr)->v;
}
- 将
ref.m_ptr
强制转换为Str*
- 提取其
v
字段(string 值)
如果传入的不是
Str
类型,dynamic_cast
会返回nullptr
,可能导致 dereference 崩溃,建议做安全检查或使用static_cast
(在你确认类型无误时)。
总结:你实现了什么?
你正在构建一个面向解释器的对象系统基础设施,具有以下特征:
特征 | 说明 |
---|---|
面向对象抽象 | 所有对象都继承自基类 Object |
动态类型系统 | 类型信息通过 dynamic_cast 动态识别 |
引用计数管理 | 通过 m_refcount 管理内存 |
引用封装 | 统一使用 Ref 来间接操作所有对象 |
类型提取机制 | 通过 extract<>() 将 Ref 转换为真实类型值 |
建议改进(可选)
- 管理引用计数:
- 类似
boost::intrusive_ptr<Object>
自动管理m_refcount
- 或定义
retain()
/release()
手动管理
- 类似
- 类型安全性提升:
- 增加类型检查:
if (auto s = dynamic_cast<Str*>(ref.m_ptr)) { ... }
- 或使用
std::variant
/typeid
做类型判断
- 增加类型检查:
- 抽象提取方式:
- 支持更多类型:如
int
、List
、Dict
等 - 或改用
visitor
模式处理对象
如果你继续开发解释器、虚拟机、或动态语言运行时,这样的对象系统是非常常见且实用的设计基础。如果你有后续如调用机制、垃圾回收、
- 支持更多类型:如