可选收尾:从 mini 调用 C++ runtime
本节阅读量:前面的编译示例都让生成的 main 返回计算结果,再用 $? 观察进程退出状态。这样能检查 42,却不能完整显示超出 0 到 255 的结果。
第四章已经实现了函数调用,正好可以再向外走一步:让生成的汇编调用由 C++ 提供的函数。这个小实验增加两个固定 primitive:
|
|
cin 从标准输入读取一个整数。cout 先求值参数、打印得到的整数,再返回同一个整数。因此下面的程序读入一个数,把它交给本章的 mini 函数 inc,再输出调用结果:
|
|
它已经保存在 examples/io.lang。
这只是第四章末尾的可选 runtime 联调,不是通用 FFI,也不把输入输出加入后续主线。普通 lambda 和普通调用仍然只有一个参数;(cin) 是 parser 单独识别的固定形式,不是新增的零参数函数调用。
与本章的 +、eq? 一样,cout 只接受整数。解释器会检查这个条件;无标签编译器仍只承诺实际执行到的 cout 收到整数的程序。
两个固定 AST 节点
parser 在普通调用的兜底分支之前识别 cin 和 cout:
|
|
它们对应显式的 ExprKind::input 和 ExprKind::output。因此贯穿这个实验的程序会得到:
|
|
解释器和编译器都通过自己的 switch 处理这两个 kind,没有在汇编生成器里偷偷检查某个字符串。
解释器和生成代码共用 runtime
runtime 的 C 接口位于 src/runtime/io.h:
|
|
实现位于 src/runtime/io.cpp:
|
|
extern "C" 关闭 C++ 的名字改写,让生成的汇编能够用确定的符号名找到这两个函数。mini_write_int 返回传入的值,使 (cout expr) 仍然是一个有整数结果的表达式。
解释器遇到 CinExpr 和 CoutExpr 时也调用这两个函数。这样 run 和编译路径对合法整数输入保持同一套“读取、打印并返回”的行为。
可以先运行:
|
|
输入 41 后会看到两行 42:第一行是 mini 程序里的 cout 效果,第二行是 mini run 原本就会显示的顶层表达式结果。两行并不表示 cout 执行了两次。
IR 记录 runtime 操作
lowering 不把这两个固定 primitive 伪装成普通函数值,而是产生显式的 input、output 操作:
|
|
输出是:
|
|
input 没有源操作数,runtime 的返回值成为 t.1,随后作为普通 mini 调用的实参。output 的源操作数是 inc 的调用结果,runtime 返回值保存为 t.3。两个操作都有目的临时变量,因此仍然沿用本章已有的栈槽分配方式。
生成对外部符号的直接 call
普通 mini 调用的 callee 是运行时求出的函数值,所以使用间接调用:
|
|
cin 和 cout 的实现名字已经固定,可以生成链接器稍后解析的直接调用:
|
|
两种调用遵守同一套 System V ABI:有整数实参的普通 mini 调用和 mini_write_int 把它放进 %rdi,三个调用都通过 %rax 返回结果。区别只在目标怎样确定:
| 调用 | 汇编 | 目标何时确定 |
|---|---|---|
| 普通 mini 函数值 | call *%rcx |
程序运行时从函数值取得地址 |
| 固定 C++ runtime | call mini_write_int |
链接时按外部符号找到地址 |
macOS 的 C 符号带前导下划线,因此 emitter 会输出 _mini_read_int 和 _mini_write_int;Linux、WSL 使用没有下划线的名字。
与 C++ runtime 一起链接
先生成汇编:
|
|
在 x86-64 Linux、WSL 或 Intel Mac 上,把汇编和 runtime 一起交给 c++:
|
|
这里使用 c++ 而不是 cc,因为 io.cpp 使用了 iostream,最终链接需要 C++ 标准库。
Apple Silicon Mac 上,汇编和 runtime 必须使用同一种目标架构:
|
|
输入:
|
|
程序会直接输出完整结果:
|
|
这次不需要用 $? 猜计算结果。若随后运行 echo $?,看到的仍只是 main 返回值折叠后的进程退出状态;mini 的 cout 已经把真正结果完整写到标准输出。
这次实验停在哪里
这一小节只建立了两条固定映射:
|
|
它没有实现任意 C++ 函数声明、多参数外部调用、字符串、文件 I/O 或动态库加载,mini compile 也仍然只输出汇编,不负责调用 c++。
第五章开始会回到语言主线,不继承这两个 primitive 和 runtime 文件。这个一次性实验的目的,是让读者亲手看到:只要双方遵守同一套 ABI,我们生成的汇编就能调用由另一种语言实现的函数。