原生二进制接口
本篇描述本基线 LLVM 原生后端与 C 运行时的接口。整数数值位宽为 64,值容器为 128;WasmGC 不使用本篇布局。规则依据为值编解码、代码生成准备、直接代码生成、运行时栈及垃圾回收实现;具体路径见基线与实现依据。
原一:目标与基本类型
语言函数的 LLVM 类型必须为 void (i128, i128),调用规范 ccc。两参依次是豫言栈帧地址和当前堆分配地址。C 对应 void 函数(__uint128_t 栈, __uint128_t 堆)。不能改为两指针 C 签名,也不能把 i128 改成 {i64,i64} 后假定调用分配相同。生成 LLVM 并由目标 clang 降级,是遵守宿主寄存器和栈参数规则的直接方法。
原生当前路径面向小端、64 位指针目标;值槽步长 16 字节。对象文件采用目标工具链的对齐与重定位,不硬编码宿主寄存器。C __uint128_t、LLVM i128 和 Mach-O/ELF 的实际调用降级须在目标机验证。不同架构对象不能直接互链;Mach-O 展示的符号前导下划线不属于下述 LLVM 名称字符串。
原二:128 位值
常规打包函数定义为:V(p,t,s,n) = uint128(uint64(p)) | (t<<64) | (s<<72) | (n<<80)。移位前提升至 128 位。拆包为 p = low64(v)、t=(v>>64)&15、s=(v>>72)&15、n=(v>>80)&0xffffffff。
| 位 | 字节偏移(小端) | 用途 |
|---|---|---|
| 0–63 | 0–7 | 整数低位、IEEE binary64 位或地址 |
| 64–71 | 8 | 类型区;当前读取只取低四位 |
| 72–79 | 9 | 子类型区;运行时当前读取只取低四位 |
| 80–111 | 10–13 | 原始长度或构造器编号 |
| 112–127 | 14–15 | 保留;常规新值清零,见负整数例外 |
| 类型号 | 解释 | 子类型/长度 | 指向内容 |
|---|---|---|---|
| 0 | 空值/单位 | 常规全零 | 无 |
| 1 | 元组、闭包、引用、引用数组 | 长度为槽数 | 连续 16 字节值槽,无独立对象头 |
| 2 | 整数 | 解码只读低 64 位为有符号数 | 无 |
| 3 | 小数 | 解码低 64 位为 double |
无 |
| 4 | 静态字符串/字节串 | 长度含隐藏末尾零 | 直接指向字节,不指向对象头 |
| 5 | 爻 | 低位通常 0/1 | 无 |
| 6 | 语言函数代码指针 | 通常其余区零 | void(i128,i128) 函数 |
| 7 | 静态对象地址 | 通常其余区零 | 例如全局 i128 的地址 |
| 8 | 豫言栈地址 | 通常其余区零 | 当前或祖先帧;栈不移动 |
| 9 | GC 转发地址 | GC 内部 | 新堆对象首槽地址 |
| 10 | 堆字符串/字节串 | 长度含隐藏末尾零 | 对象头槽地址 |
| 11 | 字节串头 | 长度含隐藏末尾零 | 本值位于堆中,后随原始字节 |
| 12 | 运行时通用指针 | 分配游标等内部临时值 | 不得当作 GC 根扫描值 |
| 13 | 构造器实例 | 子类型=载荷槽数,长度区=类型内构造器编号 | 载荷槽首地址;零载荷指针为零 |
| 14 | C 栈/局部游标标记 | 代码生成内部使用 | 不得泄入持久语言数据 |
| 15 | 未分配 | 无 | 无 |
GC 只移动类型 1、10、13;类型 11 使扫描器跳过字节数据。类型 9 只能用于转发;普通对象首槽不能伪造它。运行时在堆指针分类中断言不出现类型 12。
原二甲:非规范化值是兼容事实
- 编译器直接生成内建
阳/阴时产生原始i128 1/0,未附类型 5;比较原语及 C爻转豫言值则生成类型 5。条件判断只取低一位。消费者必须容纳这两种来源;不要对任意语言爻都调用带类型断言的 C 解码函数后宣称兼容。 - C
整数转豫言值(int64_t n)先把有符号数转换为无符号 128 位,再清除类型字节并写 2。负数因此保留高位的 1。其精确公式为(uint128(n) & ~(255<<64)) | (2<<64)。LLVM 内建减法则sub i64后零扩展并附 2。同一个负整数可有不同高位。比较整数必须取低 64 位;不能全宽比较或按高位判长度。 - 构造器载荷长度:前端允许至 250,后端写入完整八位,但 C 运行时子类型读取只取四位且 setter 断言不大于 15。故载荷超过 15 的构造器不能视为已安全兼容的原生 GC 对象。独立实现不得悄改读掩码而声称与旧运行时所有对象互通。修复须升级基线或采用显式桥接。
这些差异属于明确的兼容限制,不以“保留位任意”一语掩盖。
原三:具体对象
元组、闭包、引用
长度为 m>0 的元组占 m 个槽:分配 16*m 字节,把字段按源顺序写入,值为 V(p,1,0,m)。读取第 k 项为 load i128 (p+16*k),合法索引从零起。生成器零元素元组降为单位 0;C 元组转豫言值(0,...) 可产生类型 1、长度零、空指针,两者不宜混作普通语言可观察元组。
闭包使用普通元组,字段[0]=V(code,6,0,0),余项依闭包编译器自己的捕获顺序保存。调用者只读取第零项并把整个闭包作为隐含运行时实参;不得读取他方闭包的捕获字段。无捕获闭包仍为单槽元组。闭包的静态多态参数已擦除。
引用是长度一的元组。C 的写引用形参顺序是 豫言_写入引用(新值, 地址);与表层库函数的形参顺序不可混淆。引用数组为普通元组,数组长度在值头;写数组的 C 顺序是 (新值, 序数, 地址)。没有原生写屏障,当前 GC 不是分代增量收集器。
构造器实例
载荷为 [v0,...,v(m-1)],实例值为 V(p,13,m,c);c 从 1 起,在同一源文件中按同一返回类型头的构造声明次序计数。不同数据类型各自从 1 起,不能用全文件构造器总序号。零参构造器为 V(0,13,0,c),不分配堆。实例不携带编译期类型名;类型检查保证使用正确的判别方式。
缀/罄 不是 C 同构数组表示。数组转同构列 产生的是 [元素元组, 整数长度],供特定原语使用。标准库链表必须按其类型的构造器编号及载荷表示。
字符串与字节串
逻辑字节数 L,存储长度 N=L+1;末尾一字节必须为零,不计入逻辑内容。堆串占 1+ceil(N/16) 槽;第一槽为 V(0,11,0,N),字节数据从下一槽开始;外部值为 V(header,10,0,N)。GC 拷贝整块而不扫描其中字节。静态串是 V(data,4,0,N),没有头槽,存活期覆盖程序运行。
逻辑内容可以含零字节,按长度的原语保留它;printf("%s")、strlen 等 C 文本原语仍按零终止。不要因此推导所有字符串操作都支持内嵌零。复制字节为豫言值 接收不含末零的长度;复制字符串为豫言值 接收含末零的长度并检查末字节。逻辑长度须小于 UINT32_MAX。
原四:豫言栈帧
函数硬件参数不是源语言参数。帧 S 指向以下值槽:
| 槽 | 字节偏移 | 内容 |
|---|---|---|
| 0 | 0 | 祖先/调用者帧地址,类型 8 |
| 1 | 16 | 返回到的语言函数代码地址,类型 6 |
| 2 | 32 | 返回续延编号,带类型 2 的整数 |
| 3 | 48 | 此帧接收的调用结果;入调用时置单位 |
| 4 | 64 | 当前函数应执行的块编号;首次进入为整数 1 |
| 5… | 80… | 有序运行时参数,随后是续延存活局部值 |
经闭包调用时,槽 5 是闭包自身,槽 6 才是该次调用的显式参数。已知直接函数调用不传闭包,首个参数在槽 5。是否传闭包由目标入口契约决定,不能只看机器函数类型。
入口编号 1;其他续延编号由该函数生成器分配。本基线内部续延表包含编号 0 的分派入口,但正常初调从 1 开始。外部调用者只需把初调编号设为 1;旧函数保存给自己的返回编号必须原样传回,不得重新解释。
原四甲:源码饱和与机器逐参调用
类型检查要求显式参数给足,但隐式参数擦除把调用脊逐项降成一参数应用,并把显式 λ 降为一参数函数。普通跨模块函数值因此采用逐参闭包协议:f 于 a 于 b 在机器层先以 [f,a] 调 f 的代码,得到闭包 g,再以 [g,b] 调 g 的代码。不能一次把 [f,a,b] 填入旧入口。中间闭包是运行实现细节,不表示源码允许部分应用。
后端 IR 可表达多参数直接函数/闭包,但其实际参数个数必须由目标 IR/接口给出,不能从表层连续箭头数量猜测。独立实现最稳妥的外部值 ABI 是保留逐参闭包包装,内部优化可用自定多参数代码。例如二参数减法的普通闭包入口,先接收被减数并返回中间闭包,再由中间闭包接收减数。
普通调用
设调用者当前占用 q 槽,新帧 T=S+16*q。按以下次序生成逻辑操作:
- 将返回后仍活的值存入调用者帧,确保此前保存的参数槽和新增槽都是有效值;记录下一块编号
k。 - 写
T[0]=V(S,8,0,0)、T[1]=V(当前代码,6,0,0)、T[2]=整数(k)、T[3]=0、T[4]=整数(1)。 - 把运行时实参写到
T[5+i]。写之前保证栈空间,不能等写坏内存才检查。 - 如堆游标超过阈值,按原六执行安全点。GC 后从帧重读移动过的参数/闭包;代码地址不移动。
musttail call void 目标(i128 编码T, i128 当前分配值);紧跟ret void。不依赖 C 调用栈保存续延。
返回
返回值为 r:先取本帧 P=S[0]、F=S[1]、K=S[2],写 P[3]=r,写 P[4]=K,以 P 和当前分配值尾调用 F,随后 ret void。旧运行时注释中“写第零项”不符合当前生成器;写错会毁坏祖先帧链。恢复的函数根据槽 4 分派,从槽 3 读取返回值,从保存槽重载局部量。
尾调用
复用当前帧,保留槽 0、1、2;重置槽 3、4,写新参数,再走安全点和尾跳。必须先将可能被覆盖的新参数求值并暂存,再覆盖帧。尾调用实参数更多时也须检查空间。不可把旧帧指针替换为自身返回参数槽。
原五:直接调用与公开入口
每个数据定义通常有可寻址全局 i128,初值零。后端 IR 还可承接公开直接函数入口,其参数不含闭包;函数作为值使用时仍须以包装闭包适配。下列名称算法作用于原始源文件身份字符串,通常为包上下文解析得到的绝对路径。
转义函数 E 逐字符把 -、。、.、/、*、%、+ 替成 _,其余原样保留。既不去文件后缀,也不做哈希或 Unicode 正规化。
- 模块前缀:
M(P) = 豫言模块_ + E(P)。 - 值全局:
M(P) + _定义_ + E(名字) + _ + 十进制定序。 - 直接函数:
M(P) + _函数_ + E(名字) + _ + 十进制定序。 - 模块计算入口:
M(P) + _计算函数,零源参数、无闭包。 - 程序入口:
豫言_程序入口,同样为void(i128,i128)。
示例:源身份 /甲/乙。豫、名 减一、序号 4,值全局为 豫言模块__甲_乙_豫_定义_减一_4。替换并非单射;甲-乙 与 甲_乙 可碰撞。不得自行改良旧符号算法。名称含 LLVM 引号等特殊字符的兼容范围须额外处理并验证。
定序是检查后/运行时并列定义表中的零基索引,不是源码行号,也不只是有运行时值的定义排名。类型声明与定义先合并;类型、命名空间占位及重导出可能影响表。类型内构造器序号与文件定序是两套数字。链接旧模块时优先从依赖接口/其生成 IR 取得完整定序;只有源文本则须重做相同的定义展开。
模块计算入口按定义次序注册全局根地址、求值并写全局;直接函数声明只提供代码。程序入口按依赖先于使用者的编译顺序各调用模块计算入口。单个模块入口没有自动的一次性初始化屏障;不允许随意再次调用。
原六:分配与 GC
必须提供的可见全局是 豫言_栈指针、豫言_当前分配指针、豫言_当前堆垃圾回收阈值,C 类型均为 豫言值*,LLVM 声明 external global i128*。生成函数持有局部分配游标;C 原语调用前同步游标和当前帧到全局,调用后重新读分配游标。
生成器元组以 bump allocation 分配:在游标处顺序写槽,然后游标前移槽数。没有每对象独立 GC 头,也不在每次分配时自动回收。C 分配器同样前移游标;在 C 调用中一般不能移动堆,因为 C 局部原始指针和 SSA 值未全部登记。不得把 malloc 地址包装成普通堆对象交给旧 GC。
安全点在语言函数调用之前,实参已写帧、存活调用者值已存栈时:比较游标与阈值,严格大于才调用 豫言_函数调用前垃圾回收(新帧, 整数(新帧槽数))。它把全局栈指针暂设到新帧末尾,回收后恢复至新帧首,返回单位。C 调用前后同步分配全局。该接口不返回新参数数组;帧中值与全局根被原地改写。
旧 GC 的协议:
- 根是
豫言_登记垃圾回收根登记的值地址,以及[豫言栈开始, 豫言_栈指针)中每个值槽。不扫描普通 C 栈,不扫描任意机器寄存器。 - 类型 1/10/13 的非空旧堆指针需要搬迁;按值头计算大小。对象已搬迁时其旧首槽为类型 9,复用其中的新地址。
- 首次搬迁按完整槽数复制,把旧首槽覆盖为转发地址,并以原有类型、子类型、长度加新指针重建根值。
- 广度扫描新堆:普通值槽递归搬迁;遇类型 11,跳过
1+ceil(N/16)槽;不得把原始字节当指针。 - 处理完释放旧堆,更新游标、堆界和阈值。存活比例超过 0.45 时下一轮尝试加倍,阈值为当前容量的 90%。容量策略可替换,但可见根更新协议不可改变。
堆不支持指向对象内部字段的任意移动指针。静态根登记的地址必须持续有效,不能登记即将消失的 C 栈局部。循环引用通过转发标记保持,共享引用必须仍共享;复制不能把同一引用变成两个独立引用。
原七:C 外部调用
通常生成 declare i128 @符号(i128,...) 和普通 call i128。所有源值保持豫言编码,不自动解箱为 C 整数、char* 或裸指针。C wrapper 自行解码和编码。外调名称为字符串,参数数目来自实参容器;无参 元→0 参,单变量→1 参,元组变量序列→多参。表层任意表达式需先 虑 成变量。
两个必须特别处理的旧接口:豫言_登记垃圾回收根 实际 C 签名是 void(豫言值*),但旧 LLVM 当 i128(i128) 调用并忽略结果;豫言_运行时报错并中止 实际返回 uint64_t,调用后终止。前者是平台相关的签名错配,不能要求所有目标都成立。新实现与旧 C 运行时互操作可用实际签名调用注册函数,把类型 7 地址解成裸指针;不要使用无意义的返回寄存器。若要逐条复现旧 LLVM,则必须保留错配并在精确目标验证。
引用 C 形参次序、字节长度是否含末零以及返回宽度都以运行时接口表为准,不能由中文函数名猜测。同一 C 符号不能在不同位置以不同参数数目声明而仍要求正常链接。
原八:异常、入口与运行环境
当前异常处理器是已注册全局根的闭包。默认处理器从帧槽 6 读取异常字符串,打印后 exit(1)。豫言_获取当前异常处理器() 返回完整闭包;豫言_设置当前异常处理器(v) 替换它并返回单位。异常控制流的具体展开见执行篇。
续延异常表索引从 1 增长,记录类型 8 帧地址;豫言_设置续延异常处理器(编号,帧)、豫言_获取续延异常处理器(编号) 保存/读取动态返回目标。提前返回用当前帧的返回代码与目标帧地址配合,不能实现为任意 C longjmp 并绕过豫言帧协议。帧生命周期之外的恢复不受稳定契约保护。
正常启动顺序:保存命令行;运行时参数处理;可选入口初始化(默认初始化堆);初始化全局异常处理器并登记根;随机种子初始化;分配豫言栈;设置根帧;进入 豫言_程序入口。
根帧:[自身栈指针, 退出续延代码指针, 整数1, 单位, 整数1]。退出续延收到根帧,验证槽 4 为 1 后正常返回;运行时返回状态 0。显式退出和未捕获异常另行终止。保留 main 的原始 C 参数签名,不能将其视为豫言函数。
原生默认栈为 32*1024*1024 个槽,即 512 MiB。环境变量 YY_GC_INITIAL_STACK_SIZE_MB 在原生实现实际按“值×1024×1024 槽”处理,与其名称不符;WASI 则按 MiB 换算。堆变量 YY_GC_INITIAL_HEAP_SIZE_MB、YY_GC_MAX_HEAP_SIZE_MB 按字节再除 16。重实现若修正这些外部配置行为,须明确标为不完全兼容。