豫构下一阶段设计:构建目标、Makefile 与 C 绑定
状态:设计提案,尚未实现。文中的新增字段、命令与内部协议均不是当前功能承诺。范围是本地构建正确性和工具链解耦,不包含远程包获取、版本求解或性能优化。
实施进展:最小的包内 C 注册、直接编译归档及预构建核心运行时链接已落地;当前实际支持范围见包内原生源码。下文仍是完整后续设计,不能把所有字段视为已支持。
一、结论
下一步优先完成“编译器只生成编译产物,豫构负责原生依赖和最终链接”,再把 Makefile 中的组合目标迁入豫构。不要先把 Makefile 的 shell 命令逐条复制进配置文件。
保持已确定的规则:包与库不分两层;包根由包文件位置决定;包身份是用户与包名两个字段;语言模块只允许自身与直接依赖;单段导入固定取包根总集;配置仍只做静态解析,不执行豫言代码。
C 绑定须分为两件事:一是源码、头文件、归档及系统库如何构建和链接;二是豫言值与 C 值怎样转换、保存和释放。第一件由豫构管理;第二件由编译器、运行时 SDK 和绑定代码共同保证,不能用链接成功代替 ABI 正确。
二、当前实现与具体问题
本节依据当前仓库源码,不是假设。
| 位置 | 当前职责 | 下一阶段需要解决的事 |
|---|---|---|
| 根 Makefile | 首级自举、后续自举、比较、豫构测试、运行时与发布入口 | 配置和目标逻辑仍分散在两处 |
| 运行时支持库/Makefile | 调试/优化、静态/动态、旧桥接、WASI 运行时 | 原生动作还不能由豫构追踪和共享 |
| 编译器的主程序生成。豫 | 调用 make yy_runtime_lib,再调用 clang 链接 | 编译器反向驱动构建系统,依赖工作目录 |
| 编译器的网页汇编。豫 | 调用运行时 Makefile,使用固定归档路径 | WASI 同样需要显式工具链和原生产物输入 |
| 工具/豫构/入口。豫 | 切至编译器所在目录后运行编译器 | 工具链与完整源码仓库耦合 |
| 原生类型定义.h 与外部调用代码生成 | 豫言值为 __uint128_t,外部调用生成 i128 参数及返回值 | 现有 C调用 是豫言运行时调用约定,不是通用 C FFI |
运行时 Makefile 还有三处结构性风险:
- 调试、优化、动态库共用
构建/原生中的对象路径,配置未隔离,无法安全允许这些构建并行。 - 对象文件名使用
notdir,目录身份丢失;不同目录出现同名 C 文件时可能发生碰撞。 - 所有头文件与源码整体挂到归档目标,配方批量编译全部源码;没有按翻译单元记录真实头文件依赖。
此外,包含与链接搜索路径中硬编码了 Homebrew 和 /usr/local。数据库包目前调用 sqlite3、psql 子进程,不是已有 SQLite/libpq 原生绑定;后文 SQLite 示例仅示意未来绑定包。
三、职责与依赖图
目标依赖关系:
包配置 + 工具链配置
↓
豫构:解析包图、选择目标、构造动作图
├─ 豫言模块编译 → 有序对象/位码清单、外调需求
├─ C 编译 → 对象文件、头文件依赖
├─ 原生归档/动态库
└─ 最终链接 → 程序 → 测试/打包/安装
编译器保留语法、类型、优化、目标代码和内部模块阶段依赖的知识;豫构不重新实现这些语义。运行时和绑定的构建、工具链选择、输出命名、最终链接和测试调度归豫构。
三种边不要混用:
- 语言导入边:仍只允许自身与直接依赖。
- 原生编译接口边:控制头文件和必要宏的可见性。
- 原生链接边:沿实际目标的依赖闭包传递归档和系统库。
例如应用依赖数据库绑定,绑定依赖 SQLite:应用不能因此导入未直接声明的其他豫言包,但最终链接必须带上绑定需要的 SQLite。
四、包内目标与原生配置
建议向现有包文件增加三个可选字段:程序、测试、原生。它们描述同一个包的产物及原生组成,不是嵌套的子包或库。包级依赖继续只有一份,第一版不引入目标专属语言依赖;测试有不同依赖时沿用独立测试包。
以下是未来“数据库”绑定包的完整示意,名称尚可调整:
「依赖」者「列」【『标准库』】也。
「程序」者「典」【
「演示」者「列」【『示例』,『入口』】也,
】也。
「测试」者「典」【
「内存数据库」者「典」【
「入口」者「列」【『测试』,『内存数据库』】也,
「种类」者『运行』也,
「退出码」者「零」也,
「超时秒」者「三零」也,
】也,
】也。
「原生」者「典」【
「语言」者『C』也,
「标准」者『c11』也,
「源文件」者「列」【『原生/数据库桥接.c』】也,
「包含目录」者「列」【『原生/包含』】也,
「公开包含目录」者「列」【】也,
「宏」者「典」【「数据库检查」者『1』也,】也,
「系统依赖」者「列」【
「典」【
「名称」者『SQLite』也,
「查询名」者『sqlite3』也,
「链接」者『动态』也,
】,
】也,
「绑定」者「典」【
「数据库_打开」者「典」【「参数数」者「一」也,】也,
「数据库_关闭」者「典」【「参数数」者「一」也,】也,
】也,
】也。
字段语义与约束:
入口旧字段保留为默认程序入口;程序提供多个具名入口,两者不产生第二层包身份。总集与程序入口仍完全无关。- 新增
构建目标 包名 目标名,不改现有构建 包名 [模块段…]的解释,避免目标名与同名模块互相抢占。 测试 包名 [测试名]只运行显式声明项;种类为运行、编译成功、编译失败。输出比对和诊断比对用包内基准文件,更新基准必须显式操作。- 原生源文件必须显式列举,保留相对目录及扩展名;不默认把包内所有 C 文件编进程序。
- 用户提供的源文件、包含目录、基准文件均相对于声明包根;拒绝绝对路径、越界和穿越子包。系统安装路径只能来自工具链探测结果,不是包引用。
- 缺失字段使用明确默认值,未知字段、重复目标、错误类型直接报错。目标名不进入语言模块短名空间。
- 第一版每包一个原生组成;确需多个独立 ABI 或相互矛盾的编译参数时拆包,不先引入复杂的嵌套原生库对象模型。
- 可选
原生中的依赖包字段,列出需要消费的原生包短名;每项必须已在包级直接依赖中声明,不能借 C 配置绕过包身份解析。它选择原生编译/链接接口,不自动导入语言模块。 - 允许只有原生组成的包,不要求总集;它可经原生依赖使用,但语言单段导入仍因缺少总集而失败。
- 第一版仅支持 C 与工具链内部已有的 LLVM IR;不把运行时 Makefile 列过 cpp 当成已经支持 C++。C++ 的运行库、异常和名称修饰另行设计。
以目标实际使用的豫言模块所属包为种子,再递归展开其显式原生依赖,确定要构建和链接的原生组成。因此纯原生包不会遗漏,仅被发现而未使用的包也不会被链接。现阶段以“整个被使用的包”为原生粒度,不承诺按单个 C 符号裁剪。编译器报告实际模块闭包;不能为了省链接依赖而恢复未声明的语言导入。
五、编译器与豫构的协议
新增一个版本化的“只生成产物”工作模式,名称暂定 --emit-build-artifacts。它不得调用 make、编译 C 运行时、链接最终程序或执行程序。
请求包含:包上下文、入口文件、主机/目标平台、编译配置、编译器身份、目标输出目录、绑定符号映射。响应包含:
- 实际访问的模块及包身份。
- 保序的链接输入清单:对象、位码、生成的程序入口,以及必须执行的初始化次序。
- 编译器需要的运行时 ABI 和目标描述。
- 外部调用需求:所属包、逻辑名、物理符号、参数数、ABI、源码位置。
- 每个产物的路径、种类和成功状态;失败不得发布有效清单。
内部协议应独立于配置 parser,用结构化编码和版本号;不要解析人类日志,也不要依靠从缓存目录猜文件名。保序信息尤其重要,不能把现有链接顺序擅自字母排序。
迁移分两步:先保留编译器已有模块编译与并行实现,将整次豫言编译视为复合动作;再考虑向豫构暴露细粒度模块工作。第一步不重写调度器。
统一并发的保守方案:复合编译动作运行期间独占本次构建额度,并把 -j N 传给它;C 动作在其他时段按同一额度调度。不要同时给每个程序各开 N 个工作进程。之后若细分模块动作,再实现更充分的交错并行。这是并发正确性要求,不是本轮性能优化任务。
六、运行时及 C 构建
6.1 工具链
独立的静态工具链描述记录:豫言编译器、clang、归档器、LLVM 工具、目标三元组、sysroot/SDK、运行时源码或归档、运行时 SDK、ABI 版本、支持能力。工具路径只在配置阶段解析一次并固定。
工具链路径属于构建环境,不改变按包名引用的规则。普通包不得指定任意编译器或覆盖运行时。运行时是工具链组件,复用原生动作引擎,但不要求所有语言包重复声明运行时依赖,也不构造“运行时依赖标准库”的语言包环。
区分两组工具:构建机上运行的豫构、编译器和生成器;为目标生成代码的 C 编译器、链接器及目标库。交叉编译时不能运行目标平台的探测程序。平台判断来自目标配置,不从 uname 推断目标。
clang、LLVM IR 工具和 LTO 归档器必须经过组合能力检查;不能仅比较名称或版本大号。编译一个小对象、归档、链接,必要时验证 LLVM 位码可消费。探测失败说明具体组件,不静默关闭 LTO 或改用其他编译器。
初期支持原有 Linux、macOS 原生组合;WASI 保留既有可移植核心与替代接口清单,随后迁入同一引擎。WASI 不允许意外解析到宿主动态库。Windows/通用交叉编译只预留模型,不承诺首批支持。
6.2 一个源码对应一个动作
每个 C 文件独立调用 clang,显式给出 -c、输出、包含路径和参数。对象路径保留源文件相对路径及语言,例如 对象/原生/网络/缓冲.c.o;不再取 basename。
C 编译同时产生依赖文件。默认使用 -MD -MF 记录实际包含的用户与系统头文件,正确解析空格转义和续行;不以正则递归扫描 #include 代替预处理器。首次没有依赖文件必须编译;命中缓存要求旧依赖均存在且一致。已有生成头文件须先声明生成动作再编译,不能等编译失败后猜出生成顺序。Clang 提供这些依赖文件选项,详见命令行参考。
只记录上次实际头文件仍不足以发现“更高优先级包含目录新增同名头文件”。第一版还须将用户包含目录中的头文件清单及顺序纳入检查;工具链系统目录由 SDK 内容身份或重新依赖扫描保证。不能把系统头文件永不改变当作默认前提。
归档动作精确使用当前对象列表,写临时归档后原子替换;删除源文件后不能保留旧 archive member。动态库、静态库、调试、优化、PIC、LTO、目标架构均使用不同构建目录。只有实际请求的运行时变体才构建,不再每次链接都执行运行时“全部”。
6.3 系统依赖与参数传播
优先使用显式系统依赖查询,例如 pkg-config/pkgconf;探测是独立且可缓存的构建动作,不在解析配置时执行。查询结果记录工具身份、目标、环境、版本、元数据与最终解析到的文件。交叉场景隔离宿主搜索路径。缺库只报错和给安装提示,不自动安装或联网下载。
静态查询必须请求静态链接所需依赖,动态查询不得无故把静态私有库集合全部加入。查询输出按其引号与转义规则转为参数向量,不执行 shell,不简单按空格切开。不支持的参数形式应明确失败。
| 内容 | 作用范围 |
|---|---|
| 私有包含路径、宏、编译选项 | 仅本包 C 编译 |
| 公开头文件路径、必要公开宏 | 直接原生消费者;公开头依赖另需显式传播 |
| 归档与系统链接库 | 按最终链接需求传递 |
| 优化级别、诊断开关 | 不从依赖包传播给消费者 |
| 动态库运行时位置 | 进入运行/安装布局,不冒充编译包含路径 |
公开与私有编译需求的区别可参考 CMake usage requirements。豫构只采用这一区分,不引入 CMake 的完整属性系统。外部源码随语言包构建并携带链接信息的做法也可参考 Dune foreign stubs。
原始参数逃生口放到后续;初版优先结构化包含、宏、库与框架。不允许包通过参数覆盖 -o、目标平台、sysroot、依赖文件位置和动作输出根。第三方确需特殊参数时,应明确分类为编译或链接参数,视为执行代码级别的信任,而非宣称其安全。
6.4 链接
从编译器保序对象清单开始,按依赖关系加入包归档和运行时。静态库的使用者通常应在提供者前面;绝不把所有 -l 项排序或粗暴去重。跨归档环首版拒绝并报告环;不把 GNU 的 group 参数直接套到 macOS。
运行时只选择一个 ABI 兼容实例。同一头文件库的不同用户包可以在语言层共存,但这不意味着链接到同一进程的 C 全局符号不会冲突。对绑定导出重复、混用不兼容 ABI 等情况提前报错;第三方同名 C 符号冲突必须重命名、明确隔离或拒绝共链,不能靠链接顺序碰运气。
开发目录允许可定位的动态库路径;可分发产物使用平台对应的相对运行库布局,记录要随包交付的动态库。不要把开发机 /opt/homebrew 绝对 rpath 写成默认发布契约。静态归档链接不等于整个程序完全静态链接。
七、C 绑定 ABI 与生命周期
7.1 首版沿用豫言值 ABI
保持现有外部调用机器约定:参数和返回值均为豫言值。不能直接把 int sqlite3_open(const char *, sqlite3 **) 填进 C调用;必须由桥接函数转换参数、调用第三方 API,再把结果编码成豫言值。
绑定包提供普通豫言模块作为类型安全的公开接口,C调用 留在包内封装层。原生源文件接入不意味着立刻提供自动头文件解析、任意 struct 按值传递、可变参数或自动回调生成。
新增公共运行时 SDK 头文件,只公开稳定的值转换、句柄、内存保护和错误接口;绑定不应包含运行时所有内部实现头。ABI 身份包含值表示、调用约定、目标布局及相关 GC 契约,不以语言版本字符串单独代替。
7.2 符号归属
配置中的绑定表将逻辑名映射到本包桥接符号,并声明参数数。编译器在生成外调需求时校验本包声明、参数数和运行时 ABI;不要从符号前缀猜依赖。
物理符号采用由结构化包身份与 ABI 派生的唯一前缀。豫构生成声明头和名称映射,桥接 C 代码与编译器使用同一映射。逻辑名不包含硬编码用户摘要;用户/包名中的“的”“之”等字无需保留。
首版运行时现有 豫言_ 符号走工具链提供表,不要求一次性重命名所有运行时函数。包绑定新增符号走新表;原始符号直连只保留为明确的兼容/不安全模式。语言包之间通过豫言接口调用,不开放未声明的跨包原始 C 符号引用。
生成 C 原型可检查签名和参数数,但不能证明实现正确处理 GC 或所有权。链接后还需目标相关符号检查;nm 等检查不能替代 C 编译器的 ABI 验证。
7.3 最小安全约定
- 整数转换检查范围;C 错误码不直接当成已编码豫言值。
- 字符串明确 UTF-8、长度和终止符;含 NUL 的数据用字节串,不默默截断。
- 首版跨分配/阻塞调用的缓冲区以复制到原生内存为默认策略;禁止长期保存可能移动的豫言堆裸指针。
- 第三方对象使用显式句柄、关闭操作和已关闭状态;定义谁创建、谁释放,防止双重关闭。先做显式生命周期,不依赖尚未设计的自动析构。
- 回调、长期持有豫言值、线程内分配必须等待作用域 GC 根及释放 API 完备。当前全局根登记函数不等于已具备安全临时根机制,不应拿 C 栈地址永久登记。
- 原生工作线程不得直接操作豫言堆或调用豫言闭包;使用原生结果缓冲区,经调度线程转换并交付。
- 阻塞调用不能仅贴“异步”标签。真正异步桥接须声明启动、完成、取消及清理路径,再使用既有异步调度接口。
- 普通失败用结果类型返回;非局部异常跨越持锁、待释放 C 资源的边界时必须有清理约定。C++ 异常不得穿越边界。
第一批验收从纯整数/字节转换桥接开始,随后才做持有 SQLite 连接等资源型绑定。数据库 CLI 实现不在本轮顺带替换。
八、动作、缓存与可观察性
动作记录包含种类、依赖动作、工具绝对路径、参数向量、环境白名单、工作目录、输入、声明输出、动态依赖和可缓存性。动作类型首批固定为豫言编译、C 编译、归档、链接、运行测试、文件比较、复制、打包,不提供任意 shell 字段。
缓存键至少包含:动作格式版本、规范化配置、工具及运行时 ABI 身份、主机/目标、PIC/LTO/优化模式、参数顺序、环境、输入内容摘要、发现的头文件。时间戳可作为读取摘要的加速信息,但不能成为唯一正确性依据。
系统库升级、删除头文件、更换工具链、调整链接模式、改变绑定映射、移除 C 源文件均必须触发正确失效。改普通豫言源码不应重编无关的 C 对象。
动作临时输出在同文件系统中生成,通过后原子发布;失败不更新成功记录。缓存锁以动作身份为单位,输出路径只允许一个生产者。两个豫构进程同时请求相同动作应等待/复用,而不是写同一对象;不同配置天然隔离。
日志按构建和动作分别保存,不继续共用根目录的一个并行日志。提供计划命令与“为何重建”说明,区分源码变、头文件变、参数变和工具变。不自动缓存运行测试结果,尤其是网络、时间和外部服务测试。
这仍不是完全隔离的可复现构建:系统工具和第三方 C 源码必须可信;编译器可能加载插件或访问环境。第一版承诺显式输入、可诊断失效及受控输出,不声称安全沙箱或跨机器缓存可无条件共享。
九、Makefile 的迁移
9.1 根 Makefile
| 现有目标 | 豫构接管后的职责 |
|---|---|
| yy2_bs | 由稳定豫构和稳定编译器按包构建,见下节 |
| yy3_bs、yy4_bs、连续自举 | 豫构的编译器产物依赖链及文件比较动作 |
| 豫构 | 具名程序目标 |
| 类型检查 | 编译器的检查目标 |
| 豫构测试 | 声明测试集合,不再硬编码三段编译运行命令 |
| 自举包 | 显式编译产物清单加打包动作,不猜缓存路径 |
| 运行时 | 原生组件的按需构建 |
| 清理 | 只清豫构拥有的产物和记录,不删任意输出路径 |
根 Makefile 保留为构建编排入口。包内 C 文件交由豫构管理;平台运行时仍由专门的 Makefile 构建,不再包含旧种子的兼容桥接层。
9.2 引导不能成环
稳定编译器 yy_bs_stable + 稳定构建器 yy豫构_stable
↓ 按软件包构建
yy2_bs
↓ 同一稳定构建器管理原生依赖与链接
yy3_bs → yy4_bs → 比较 → 正式 yy豫构
发布包同时携带两套可移植种子源码,构建后得到稳定编译器和稳定豫构。缺少任一种子就报错,不回退到旧单文件构建机制。文件形式的编译器命令也委托豫构建立所属包的上下文。
平台运行时由顶层构建显式准备;包注册的 C 文件始终由豫构编译和链接。旧单种子启动配方、引导豫构和兼容 C 桥接已移除。
Make 的并发额度不能与豫构各算一套。初期薄 Make 入口串行启动一次豫构;若用户请求多个并行 make 目标,保守串行化顶层入口。后续再实现 GNU Make jobserver 协作,不承诺不受控的嵌套并行。
9.3 第三方已有 Makefile
不要求重写上游项目,也不把它们默认为可信构建钩子。第二阶段可提供独立外部构建适配器:声明源码树输入、构建机工具、出树构建目录、参数向量、必须产生的归档/公开头文件、环境和目标平台。
外部构建结果作为导入的原生产物;不直接在依赖源码目录运行 make install,不自动下载或修改系统。首版不理解第三方 Make 内部依赖时,以整个源码树摘要保守失效。上游不支持出树构建时复制到受控暂存目录。此功能晚于内建 C 编译,不成为运行时迁移的前提。
十、测试与验收
必须建立以下自动化用例:
- 两个目录下同名 C 文件均正确参与构建。
- 修改直接/间接头文件触发正确重编;删除头文件、增加遮蔽头文件不能误用缓存。
- 删除一个 C 源文件后,归档中没有残留对象。
- 同时构建调试/优化与静态/动态变体,产物互不覆盖。
- 两个豫构进程构建同一目标,不出现损坏缓存和半成品。
- 包 A → B → 原生库:链接依赖正确传递,而未声明的语言导入仍失败。
- 静态与动态系统依赖的参数、运行库布局正确;缺库报错不回退。
- 绑定参数数错误、错误返回 ABI、重复导出符号提前报错;GC 压力、关闭两次、错误路径及异步结果清理有专项测试。
- 从与工具链目录无关的项目目录构建,且编译器不再调用 make;路径含空格仍正常。
- 新运行时与旧配方产物分别链接代表程序,行为一致;按仓库要求完成三级自举及字节一致比较。
- Linux 与 macOS 在各自 CI 上验证;WASI 在自己的工具链测试,不以宿主成功替代目标验证。
- 无修改不执行 C 编译/归档/链接;失败动作不发布成功状态。冷启动与原 Python 基线对比不退步即可,不追加优化工程。
十一、实施顺序
| 阶段 | 交付 | 退出条件 |
|---|---|---|
| 一 | 编译产物协议、工具链身份、原生动作与最终链接;暂不开放通用绑定配置 | 同一程序经新旧链接路径行为一致,新路径不调用 make |
| 二 | 运行时静态构建、头文件追踪、缓存和输出隔离 | 自举固定点通过;无改动不重编,修改头文件正确失效 |
| 三 | 包级原生配置、系统依赖和最小绑定 SDK | 一个纯值桥接包跨包调用成功,ABI 与失败用例通过 |
| 四 | 多程序/测试目标、自举及打包动作、薄 Makefile | 日常构建和 CI 组合逻辑由豫构拥有 |
| 五 | 动态库分发、WASI 迁移、外部 Make 适配器 | 分平台验收;不支持项明确报错 |
第一阶段仍可暂用旧配方生成一次运行时归档,再把该归档作为显式输入验证新链接接口;第二阶段才切到豫构编译运行时,降低一次变更范围。不要同时更改 FFI 语言语法、值布局和构建引擎。
建议先批准四个边界:编译器退出最终链接;包原生组成首版只有一个;绑定首版只支持豫言值桥接 ABI;Makefile 保留严格限定的启动兼容层。其余字段名可在实现前再精简。