
我第一次真正把“解释”和“编译”这件事想通不是在看编译原理教材的时候而是在折腾 Lua 的 C API 时突然开窍的。那个瞬间我才意识到一个用 C 语言写出来的 Lua 解释器可以让我在 C 程序里执行 Lua 脚本而 Python 的官方实现 CPython本质上也是一个用 C 写的解释器平时我们运行.py文件时它早就在内部把源码编译成了字节码。所以那个说“你也可以解释 C 并编译 Python”的标题并不是什么奇谈它想说的其实是同一件事语言之间的边界远没有想象中那么硬只要你理解了“解释”和“编译”背后的机制你既可以用 C 去解释一门语言也可以把一门语言编译成另一种形式。这篇文章我不会去复述编译原理课的完整内容而是想从 Lua、C、Python 三者的真实运行过程出发带你亲手验证几个关键节点。你会看到嵌入式脚本、字节码、解释器、编译器这些概念原本就是同一套思维在不同方向上的延伸。读完以后你至少能回答三个问题Lua 是怎么被 C 嵌入的Python 的字节码长什么样如果要自己写一个迷你解释器第一步该做什么1. 先搞懂“解释”和“编译”不是语言的属性而是执行策略很多初学者会有一种根深蒂固的分类法C 是编译型语言Python 是解释型语言Lua 也是解释型语言。这个说法在谈“典型实现”时勉强够用但只要深入一点它就会误导你。因为“解释”和“编译”描述的是语言实现时的执行策略而不是语言本身天生的属性。同一门语言既可以被解释执行也可以被编译执行甚至可以在同一套运行时里同时进行“编译”和“解释”。1.1 一个编译型语言也可以被解释C 的另一种执行方式我们通常说 C 是编译型语言是因为大多数 C 程序会经过预处理、编译、汇编、链接四个阶段最终生成机器码之后由操作系统直接加载执行。但这不代表 C 只能被编译。“解释 C”虽然听起来反直觉实际上早在几十年前就有过专门研究。现在也有很多教学用途的小型 C 解释器项目它们不一定覆盖完整的 C 标准但能执行 C 的一个子集支持变量、函数调用、循环、指针甚至结构体的一部分。这类解释器做的事情和解释 Python 脚本本质上是相同的读入源码文本解析成语法结构然后一边遍历一边求值。当然完整解释 C 并不容易。因为 C 涉及指针运算、内存布局、类型强转、预处理等大量底层语义要对这些语义逐一生效工程量不亚于写一个完整的编译器。所以更实际的做法是先做一个能解释“C 表达式子集”的玩具实现用来理解核心原理。1.2 Python 也会“编译”从源码到字节码Python 的运行时路径其实很清晰当你写下python test.py时解释器并不是直接逐行解释.py文件。它先把整个源码文件经过词法分析和语法分析生成一棵抽象语法树再把抽象语法树编译成 Python 字节码最后交给 Python 虚拟机逐条执行。这个“编译”步骤是真实存在的只是它生成的不是机器码而是一种跟具体机器无关的字节码。你甚至可以用内置函数compile()手动触发这一步再用dis模块看字节码。所以如果你说“Python 是解释型语言”其实是忽略了它的编译阶段。更准确的说法是CPython 是一个以“先编译成字节码再用虚拟机执行字节码”为主要运行方式的实现它的对外体验表现为“解释执行”。1.3 两种执行策略的本质差异我们可以用一个表格把几个典型实现放在一起看语言/实现典型阶段最终执行物执行方式传统 C 编译器源码 → 汇编 → 机器码可执行文件操作系统直接执行机器码C 解释器教学型源码 → ASTAST解释器遍历 AST 求值Lua 官方解释器源码 → Lua 字节码Lua 字节码Lua 虚拟机执行字节码CPython源码 → Python 字节码Python 字节码Python 虚拟机执行字节码PyPy源码 → 字节码 → 机器码机器码JIT 编译器运行时生成机器码这些路径的根本区别在于“谁来最终执行程序”。如果是一个程序去逐条读取另一种形式的指令并完成操作那就是解释如果是先翻译成机器能执行的指令并保存下来那就是编译。理解了这一点你就会发现“解释 C”和“编译 Python”并不是什么魔法只是换了一条执行路径而已。2. Lua 为什么是理解这一切的最佳切入点Lua 是一门口子很小的脚本语言但它的实现非常精致。和 Python 动辄几十万行源码不同Lua 官方解释器的核心代码量很小适合阅读。而它最让我觉得有价值的地方是它无处不在的“C 与脚本”的双向关系Lua 解释器本身用 ANSI C 编写所以你可以用 C 写一个宿主程序再嵌入 Lua 脚本反过来Lua 也可以调用 C 函数因为 Lua 的运行时天然留好了 C API 的接口。2.1 Lua 的设计目标从实验室数据到可嵌入脚本Lua 诞生于巴西的 PUC-Rio 大学早期是为了给石油数据分析程序写配置文件。它的核心设计目标就是“可嵌入”。这意味着 Lua 不希望成为独立的应用程序而是希望作为一个库被别的语言通常是 C/C链接进去作为它们的脚本引擎。正是因为这种定位Lua 解释器对资源占用非常克制启动速度很快运行时可以被控制得很小。这个设计目标直接决定了 Lua 与 C 的关系Lua 的每一次执行都必须有一个lua_State也就是一个 Lua 的运行时状态。你可以把它理解成一个“独立的虚拟世界”C 代码负责创建这个状态、向里面推入脚本、再从中取出结果然后销毁这个状态。整个过程都由 C 代码主动控制。2.2 Lua 解释器本身就是用 C 写的这是天然的“C 与脚本”结合点Lua 的官方实现源码主要分为几块llex.c负责词法分析lparser.c负责语法分析lcode.c负责把语法树生成字节码lvm.c负责执行字节码的虚拟机。这些文件全部用 C 编写。所以当你运行一个 Lua 脚本时实际上是这样一个链条Lua 脚本 → 词法分析 → 语法分析 → 生成 Lua 字节码 → Lua 虚拟机逐条执行这个链条并不神秘但它的关键点在于虚拟机是 C 写的。也就是说Lua 字节码的执行最终是 C 函数在操纵内存和寄存器。这其实已经是一种“C 解释 Lua”的典型实现。而标题里“你也可以解释 C”如果要找一个落点就可以理解为你可以用 C 写出一个解释器比如 Lua 解释器的子集或者更直白一点你可以在 C 程序里“解释执行”另外一门脚本语言。2.3 最小实践第一次用 C 代码嵌入 Lua我们做一个最简单但足够真实的实验写一个 C 程序它创建一个 Lua 运行时状态然后直接执行一段 Lua 字符串。环境上你需要先安装 Lua 开发库比如 Debian/Ubuntu 上的liblua5.4-dev或者从 Lua 官网下载源码自己编译。下面是一个最小示例#include stdio.h #include lua.h #include lauxlib.h #include lualib.h int main(void) { // 创建一个新的 Lua 运行时状态 lua_State *L luaL_newstate(); // 打开标准库比如 print、string、table luaL_openlibs(L); // 在 Lua 虚拟机里执行一段脚本 const char *script local a, b 2, 3\nprint(a b , a b); if (luaL_dostring(L, script) ! LUA_OK) { // 出错时错误消息被压到栈顶我们取出来打印 fprintf(stderr, Lua error: %s\n, lua_tostring(L, -1)); lua_close(L); return 1; } lua_close(L); return 0; }在大多数 Linux 系统上使用 gcc 编译时可能需要链接 Lua 库和数学库gcc -o lua_embed_demo lua_embed_demo.c -llua -lm -ldl运行./lua_embed_demo你应该会看到a b 5这个例子看起来简单但它包含了一个非常本质的过程C 程序把 Lua 脚本作为文本数据交给 Lua 解释器解释器内部完成词法、语法、编译、执行再把结果返回给 C。这个过程里C 是宿主Lua 是脚本边界就是中间那一层 C API。这里的 C API 最让人困惑的是“栈”的概念。Lua 不直接让 C 访问 Lua 的变量而是通过一个虚拟栈来交换数据。你调用lua_pushnumber把一个数字压入栈调用lua_call让 Lua 调用函数参数在栈上返回值也在栈上。这种设计避免了 C 和 Lua 之间复杂的内存模型耦合。如果你第一次接触很容易出现“栈不平衡”的报错。我的习惯是每次写 C-API 交互先想清楚“进入时栈上有几个元素调用后栈上还剩几个”再动手写代码。注意luaL_dostring在出错时会返回非零值并把错误信息留在栈顶。实际项目中一定要检查返回值并取出错误信息否则出问题时你只看到一个神秘的非零退出码。3. 你也可以解释 C一个递归下降表达式子集解释器现在我们把“解释 C”这句话落到实处。完整实现一个 C 编译器或解释器当然不是一篇博客能完成的事但理解“解释”的核心逻辑完全可以从一个很小很小的表达式子集开始。我们可以用 Python 写一个解释器它能解析类似2 3 * 4 - (1 2)这样的算术表达式然后计算出结果。虽然这不是完整的 C 语法但它已经覆盖了解释器的两个重要阶段词法分析和语法分析。3.1 别急着做完整 C先做“C 表达式子集”为什么从表达式开始因为表达式是几乎所有编程语言里最核心的语法单元之一。C 语言的if、for、函数调用最终都离不开对表达式的求值。如果你能做表达式解释器再往上加变量、函数、语句就会顺理成章。我见过很多学习者的误区一开始就想实现一个“能编译 C 的编译器”结果在词法分析阶段就写了上千行代码然后被各种边界情况淹没。更聪明的做法是先限定范围只支持加减乘除、括号、整数和一元负号。先把这条狭窄但完整的小路走通再逐步拓宽。3.2 从一串字符到一棵树词法分析和语法分析解释器拿到源码文本后第一步是把字符串切成一个个有意义的 token记号。比如2 3 * 4会被切成数字 2 加号 数字 3 乘号 数字 4这一步叫词法分析每个 token 都带有类型和值。接下来语法分析器会按照语言的规则把这些 token 组成一棵抽象语法树AST。表达式2 3 * 4的 AST 大致是这样 / \ 2 * / \ 3 4注意乘号这层的优先级比加号高所以它在树中更靠近叶子。解释器之后只需要对这棵树做一次后序遍历就能得到结果。3.3 用 Python 写一个迷你解释器解析并求值下面是一个足够小的递归下降解析器实现。它支持加减乘除、括号、整数以及一元负号。为了简化我把词法分析和语法分析写在了一起用递归函数之间的关系来体现运算优先级。import re # 简单的 token 切分匹配数字、四则运算、括号、一元负号 def tokenize(code): token_pattern re.compile(r\s*(?:(\d)|([*\-/( )]))) tokens [] pos 0 while pos len(code): match token_pattern.match(code, pos) if not match: raise SyntaxError(f无法识别字符位置 {pos}: {code[pos]!r}) num, sym match.groups() if num is not None: tokens.append((NUM, int(num))) elif sym is not None: tokens.append((SYM, sym)) pos match.end() return tokens class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): if self.pos len(self.tokens): return self.tokens[self.pos] return (EOF, ) def next(self): token self.peek() self.pos 1 return token # expression : term { (|-) term } def parse_expression(self): node self.parse_term() while self.peek()[1] in (, -): op self.next()[1] right self.parse_term() node (op, node, right) return node # term : factor { (*|/) factor } def parse_term(self): node self.parse_factor() while self.peek()[1] in (*, /): op self.next()[1] right self.parse_factor() node (op, node, right) return node # factor : NUMBER | - factor | ( expression ) def parse_factor(self): token self.next() k, v token if k NUM: return (NUM, v) if v -: child self.parse_factor() return (NEG, child) if v (: node self.parse_expression() self.next() # 跳过右括号 )这里简单假设括号匹配 return node raise SyntaxError(f意外的 token: {token}) def evaluate(node): if node[0] NUM: return node[1] if node[0] NEG: return -evaluate(node[1]) op, left, right node lv, rv evaluate(left), evaluate(right) if op : return lv rv if op -: return lv - rv if op *: return lv * rv if op /: return lv / rv raise ValueError(f未知操作符: {op}) def interpret(code): tokens tokenize(code) ast Parser(tokens).parse_expression() return evaluate(ast) if __name__ __main__: test_cases [ 2 3 * 4, (2 3) * 4, 10 - 2 * 3 1, -5 6, ] for code in test_cases: print(f{code} {interpret(code)})运行这段代码你会得到2 3 * 4 14 (2 3) * 4 20 10 - 2 * 3 1 5 -5 6 1这个实现没有做复杂的优化但它已经是一个完整的“解释器”把文本解析成树再对树求值。如果你把这里的evaluate改成“生成汇编指令”或者“生成 Lua 字节码”它就已经跨向了“编译”的方向。所谓“你也可以解释 C”在这个小例子里指的就是你已经具备了解释 C 语言表达式子集的能力。从这里继续扩展加入变量存储、函数定义、条件语句你就能慢慢接近一个真正的小型 C 解释器。3.4 从解释器走向编译器关键一步是“生成代码”解释器拿到 AST 后直接计算而编译器拿到 AST 后不直接计算而是生成一种中间表示比如字节码或汇编。以 Lua 为例Lua 的解释器实际上也有一个编译阶段它把源码解析成 AST然后把 AST 转成 Lua 字节码交给虚拟机。你可以用luac -l命令查看一个 Lua 脚本编译出来的字节码列表。这样你就看到了“解释”和“编译”之间并非对立很多实现是“先编译成字节码再解释执行字节码”。4. 你也可以编译 Python从源码到字节码再到 C 扩展现在我们转向 Python。很多人说 Python 是“边解释边执行”但如果你真的调试过程序会发现它其实有一个非常明确的“编译”动作。理解这个动作你就会明白标题的后半句“你也可以编译 Python”不是一句口号而是每个 Python 开发者每天都实际经历的过程。4.1 Python 的编译过程用 compile() 和 dis 看真实字节码Python 内置的compile()函数可以把一段源码编译成一个 code 对象然后你可以用dis模块查看它的字节码。下面是一个简单例子import dis source a 1\nb 2\nc a b co compile(source, string, exec) print(co) dis.dis(co)输出会是一串包含指令名的行例如1 0 LOAD_CONST 0 (1) 2 STORE_NAME 0 (a) 2 4 LOAD_CONST 1 (2) 6 STORE_NAME 1 (b) 3 8 LOAD_NAME 0 (a) 10 LOAD_NAME 1 (b) 12 BINARY_OP 0 () 14 STORE_NAME 2 (c) 16 LOAD_CONST 2 (None) 18 RETURN_VALUE这段字节码已经非常接近 Lua 字节码的设计它也是一种“虚拟机器指令”由 CPython 虚拟机解释执行。你在终端里运行python -m py_compile test.py就会在__pycache__目录下生成一个.pyc文件这个文件里存的就是编译后的字节码相当于“编译 Python”的产物。下一次再运行同一个脚本时CPython 会优先检查.pyc文件是否存在以及源码时间戳是否变化如果没变就跳过重新编译直接加载字节码。理解了这一点你就能回答“为什么 Python 第一次启动慢但后续运行有时更快”的部分原因了。4.2 CPython 本身就满足“用 C 解释 Python”“用 C 解释 Python”听起来很厉害但其实 CPython 就是这样的存在。CPython 是用 C 编写的 Python 官方实现它包含了一套完整的 C 代码负责词法分析、语法分析、编译为字节码、执行字节码以及维护对象系统、内存管理、垃圾回收。所以如果你说“我用 C 解释了 Python”在抽象层面你其实在做和 CPython 一样的事只是工程量还差很远。真正要掌握的是Python 对象在 C 层是什么它怎么和 Lua 一样通过“栈”或者“引用计数”来让 C 函数和 Python 对象互操作这是下一节我们要看的。4.3 实操让 Python 调用 C 函数体验“跨语言编译”Python 和 C 互操作有很多方式最简单、最省事的是用标准库ctypes。它可以在运行时动态加载一个 C 库并调用其中的函数。比如我先写一个 C 文件编译成共享库#include stdint.h int64_t add_int64(int64_t a, int64_t b) { return a b; }用 gcc 编译成共享库Linux 上是.somacOS 上是.dylibWindows 上是.dllgcc -shared -fPIC -o libmyadd.so myadd.c然后在 Python 中加载并调用它import ctypes lib ctypes.CDLL(./libmyadd.so) lib.add_int64.argtypes [ctypes.c_int64, ctypes.c_int64] lib.add_int64.restype ctypes.c_int64 result lib.add_int64(100, 23) print(result) # 123这个例子里Python 编译产生的字节码在执行到lib.add_int64(100, 23)时会通过 ctypes 调用 C 共享库里的函数。这其实就是“Python 程序调用 C 代码”的典型路径之一。如果你深入下去可以用 Python C API 写一个真正的扩展模块让 Python 直接import一个 C 编译出的扩展。它的核心要素是写一个 C 文件包含Python.h定义导出函数注册方法表然后调用PyArg_ParseTuple解析 Python 传入的参数再返回PyLong持有 C 的整型结果。这个过程的复杂点在于 Python 对象的引用计数管理稍不留神就会造成内存泄漏或崩溃。注意在 Python 的 C 扩展里一定要正确处理引用计数。常见准则是大多数函数返回“新引用”你需要用Py_DECREF在不需要时释放有的函数返回“借用引用”你则不能随意释放。这块比较绕初学者最好先用ctypes或cffi这类高层封装等理解对象模型后再接触底层的 CPython API。4.4 打包成可执行文件并不是真正的“编译 Python”很多人会用 PyInstaller、Nuitka 把 Python 程序打包成单个可执行文件然后说“我把 Python 编译成了二进制”。这里要区分一下PyInstaller 主要做的是把 Python 解释器、依赖库和你的脚本打包在一起运行时仍然是解释执行本质上是分发了一个“自带运行时”的压缩包。Nuitka 则更进一步会把 Python 代码转换成 C 或 C 代码再编译成机器码它的产品形态更接近“编译 Python”。但无论哪种都比直接用解释器执行复杂得多而且不一定适合所有项目。如果你想深入了解可以从 Nuitka 的原理入手但不要指望它解决所有部署问题。5. 跨语言集成的工程边界和避坑指南从“Lua 嵌入 C”到“Python 调 C”跨语言集成并不总是一帆风顺。我踩过很多坑这里挑几个最典型的说。5.1 嵌入 Lua 时最常见的 5 个坑栈操作不平衡Lua C API 使用虚拟栈交换数据。每调用一次lua_push*栈上多一个元素每调用一次lua_pop栈上少一个元素。如果函数返回时栈里残留了多余数据轻则影响后续调用重则导致无法预测的行为。调试时可以在关键位置打印lua_gettop(L)看栈深。链接库版本不匹配编译时#include lua.h找到的头文件和运行时-llua链接到的库版本不一致可能会报出奇怪的符号错误。先用pkg-config --cflags --libs lua5.4之类的命令确认路径再编译。错误信息没有取回lua_pcall调用失败后错误对象留在栈顶。如果你不取出来它就一直留在那里。正确做法是lua_tostring(L, -1)后立刻lua_pop(L, 1)。多个 Lua state 互相干扰每个lua_State *是独立的运行时状态。如果你在同一个程序里创建多个 state它们之间默认不共享任何全局变量。如果你的业务里必须共享数据要显式设计跨 state 的通信方案比如通过 C 层的数据结构。没有设置安全边界Lua 脚本其实可以调用os.execute这样的函数。如果脚本来自外部不可信输入嵌入时需要删掉危险库函数或设置 hook 限制执行时间。这不是理论问题而是生产环境真正的安全风险。5.2 Python 调用 C 时的常见问题GIL 的影响CPython 的全局解释器锁限制同一时刻只能有一个线程执行 Python 字节码。如果你的 C 扩展函数里执行了耗时计算必须先正确释放 GIL否则会影响其他 Python 线程。对这个机制不熟悉时尽量不要在 C 扩展里做长任务。对象生命周期用 C 扩展处理 Python 对象时要特别小心引用计数。一个函数从参数里拿到的 Python 对象可能是“借用引用”你不该对它做Py_DECREF而从某些接口返回给你的可能是一个“新引用”你必须负责释放。一旦搞混轻则内存泄漏重则段错误。ABI 兼容不同的 Python 版本对应的 C API 有细微差别。在 Python 3.x 内部小版本之间通常保持向后兼容但如果你用了较新的 API在旧版本 Python 中可能无法编译。编译扩展时最好使用与目标 Python 版本匹配的头文件和编译器。ctypes 的坑ctypes虽然简单但它要求你知道 C 函数的参数和返回值类型。如果你漏写argtypes和restype默认会按int处理指针或 64 位整数就会出错甚至导致进程崩溃。所以看 C 函数声明时第一时间把它转换成argtypes/restype。5.3 永远不要试图“完整解释所有 C”或“编译所有 Python”我做这类实验时会给自己设一个很明确的范围。要真正实现一个完整 C 解释器你所面对的是 ISO C 标准中数不清的边角情况位域、结构体对齐、未定义行为、预处理器的宏展开、头文件搜索路径……这些内容随便拿出一个都能写几十篇文章。同样要“编译”所有 Python 代码意味着你要处理动态类型、装饰器、元类、生成器、协程、动态导包等高度动态的特性这也是为什么 JIT 编译器会那么复杂。所以作为个人学习和实验最好的策略是选一个极小的语言子集把整条链路跑通。等你知道“整条链路”长什么样再逐步增加特性远比一上来就想“做大而全”要现实。5.4 这种学习方式适合谁不适合谁如果你是一个应用开发者主要工作是写业务逻辑那么你不需要真的去实现一个解释器。但如果你经常遇到以下问题就很值得花时间走一遍这条路脚本引擎报错时你完全看不懂底层在干什么你想在 C/C 程序里嵌入脚本却不知道如何封装你想给 Python 写性能关键型扩展却不知道 C 扩展怎么组织你调式RuntimeError之外的底层崩溃比如段错误却完全不知道从何入手。反之如果你的目标是快速落地业务功能暂时不需要触碰底层那这一块可以往后放。它不是“必学”但确实是很多高级开发者的分水岭。6. 从“你可以”到“你正在”一条可复制的实践路径最后我想把这条学习路径收拢成一个可以照着执行的清单。我自己走完一遍后最大的体会是不要被“编译器”三个字吓住把它拆成几个可以单独练习的阶段你就能一步一步走下来。6.1 第一步先跑通一个最小嵌入示例从本文的 C 嵌入 Lua 示例开始或者从 Python 的compile()/dis.dis()开始。目标不是写出复杂代码而是亲眼看到“脚本语言在另一种语言里被执行”的过程。这一步通常只需要两个小时但它会让你对“解释器”突然有了直观概念。记下你看到的结果然后在脑海里画出一条线文本 → token → AST → 中间表示 → 执行。这条线是所有语言实现的骨架。6.2 第二步阅读一小段真实源码不要求读完整只挑你最感兴趣的部分。Lua 源码里可以先去读llex.c中关于 token 类型的定义或者在lvm.c里找几条字节码指令的case。CPython 源码里可以读Python/ceval.c中一条指令比如BINARY_OP的执行逻辑。你会发现真实解释器的核心循环并不神秘很多时候就是一个巨大的switch语句。阅读时带着两个问题这个指令从哪里取操作数结果放在哪里你很快就能理清寄存器和栈的概念。6.3 第三步写一个自己的玩具语言不要叫它语言叫它“小计算器”更合适。先写支持数字、变量、加减乘除的解释器再往里面加if判断和循环。每加一个特性你都会重新体会一次“语义到底由什么构成”。我推荐用 Python 来写这个玩具解释器因为 Python 写起来快而且你不需要处理内存释放可以把注意力集中在解析和求值逻辑上。当你觉得 Python 写解释器已经没意思了再用 C 把同一个玩具重写一遍——这时你就会真正遭遇内存和字符串管理的挑战。6.4 第四步做一次真正的跨语言集成你可以选一个真实的小工具用 C 写一个函数做成 Python 扩展然后在 Python 里调用它。或者反过来用 C 写一个小程序嵌入 Lua 脚本让 Lua 脚本控制程序行为。这一步会逼你去理解 ABI、引用计数、数据转换和错误处理而不是停留在“玩具解释器”的舒适区里。一个不错的项目是用 C 写一个“求最大公约数”的函数然后通过 Python 的ctypes调用它再写一个 Lua 脚本通过 Lua 的 C API 调用同一个 C 函数。这样你就会意识到C 作为“桥梁”的价值恰恰在于它既能被脚本语言调用也能调用脚本语言。6.5 这条路径的长期价值我坚持认为“理解解释器和编译器”的价值不在“能写出编译器”而在于它改变了你看待程序的方式。当你遇到性能问题时你会本能地想这段代码是被解释执行的还是被编译成机器码执行的当你遇到脚本引擎报错时你会去推想错误发生在解析阶段还是执行阶段。当你设计跨语言系统时你会主动考虑边界上的数据格式、错误传播和资源管理。这些能力不是靠某一天突然获得的而是通过一次次“亲手跑通一条最小链路”积累出来的。标题里“你也可以解释 C 并编译 Python”如果用一句话来落地那就是不要被语言的名字和实现形态限制住理解执行策略之后你可以在任意两种语言之间建立桥梁。今天从 Lua 入手是因为它足够小但掌握了方法Python、C、JavaScript甚至你自己设计的新语言都会变成同一套思维下的不同入口。