- 编译器
- 语言运行时
- 开发工具
- CLI
【免费下载链接】scriptc
TypeScript-to-Native Compiler
本指南围绕 scriptc(TypeScript-to-Native Compiler)仓库中 native/llvm-codegen/README.md 展开,深入剖析这个进程外(out-of-process)LLVM 代码生成助手的职责边界、构建方式、CLI 协议与发射流水线。读者将掌握:为什么 scriptc 需要独立的 LLVM helper 而编译器不搜索
PATH、如何为 macOS arm64 手工构建该发布产物、version/emit两个子命令的完整参数语义,以及源码层面"优化前验证 → 默认 O2 流水线 → 优化后验证 → 原子发布"的可靠性设计。
一、为什么需要一个"进程外"的代码生成助手
scriptc 是 TypeScript 到原生代码的编译器,其编译器主体(packages/compiler/src)运行在 Node.js 进程中,而真正的机器码(汇编/目标文件)发射能力由 LLVM 提供。两者之间存在明显的技术边界差异:
- ABI 与链接差异:LLVM 官方发行版通常以静态库形式提供,且构建时可能关闭 RTTI;直接内嵌进 Node 进程会带来符号冲突、链接模式冲突等一系列问题。
- 平台差异:LLVM 二进制发行版对宿主系统(libstdc++、glibc/musl、macOS 版本)敏感,与编译器本体解耦后,可以按平台独立打包与替换。
- 进程隔离:LLVM 的
report_fatal_error默认会导致abort()并留下未结构化的崩溃栈。独立进程意味着即使 LLVM 内部出现致命错误,编译器(Node 调用方)也能收到结构化的 JSON 诊断,而不会直接崩溃。
因此,仓库将 LLVM 汇编与目标文件发射封装为一个专门的、版本化的小协议进程外助手:scriptc-llvm-codegen。它只做一件事——接收 LLVM IR 文件与一组参数,输出.o目标文件或.s汇编文件,全部交互通过标准输入/输出完成。这一设计也直接呼应了 CMakeLists.txt 中对 RTTI 模式的显式处理:"官方 Linux 和 Windows LLVM 归档通常禁用 RTTI,混用 RTTI 模式会把普通的 virtual typeinfo 变成未解析的链接符号"。
二、版本矩阵:LLVM 22.1.8、AArch64 后端与 macOS 15/14 双目标
README 明确了该助手的关键版本事实,源码在多个位置与之呼应:
| 维度 | 值 | 出处 |
|---|---|---|
| 绑定的 LLVM 版本 | 精确22.1.8(find_package(LLVM 22.1.8 EXACT REQUIRED CONFIG)) | CMakeLists.txt |
| 当前编译进去的后端 | 仅AArch64(默认SCRIPTC_TARGET_BACKENDS=AArch64) | CMakeLists.txt、target.h |
| 默认目标三元组 | arm64-apple-macosx14.0.0 | target.h |
| 助手可执行文件宿主要求 | macOS 15+(Homebrew 固定 LLVM 22 bottle 的最低部署版本) | CMakeLists.txt |
| 发射产物部署目标 | macOS 14(经由 triplearm64-apple-macosx14.0.0) | README、CMakeLists.txt |
这里有一个很容易混淆的"双版本"概念,需要重点区分:
- 助手可执行文件本身静态链接了 Homebrew
llvm@22的库,而该 bottle 构建于 macOS 15,因此打包的二进制最低要求 macOS 15,否则无法运行。这在 CMake 中以CMAKE_OSX_DEPLOYMENT_TARGET "15.0"强制固化(CMakeLists.txt)。 - 它发射出的汇编/目标文件则通过 triple 单独瞄准 macOS 14,与助手自身的运行环境无关。默认 data layout
e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32由SCRIPTC_DEFAULT_DATA_LAYOUT缓存变量配置(CMakeLists.txt)。
version子命令会把这套矩阵以 JSON 形式如实上报,便于编译器在调用前完成身份校验(详见下文"协议"一节)。
三、构建:何时需要、需要什么、如何构建
3.1 构建触发条件
README 明确指出:工作区常规的pnpm -r build不会重建这个发布产物。该助手作为独立的 npm 包发布,只有当以下两种情况才需要显式构建:
- 正在开发/调试原生发射链路(修改 native/llvm-codegen/src 下的 C++ 源码);
- 准备打包发布
@scriptc/llvm-darwin-arm64。
对应地,packages/llvm-darwin-arm64/package.json 中的build脚本被刻意设为"true"(空操作),真正的原生构建入口是build:native,且prepack会先断言bin/scriptc-llvm-codegen可执行,防止把未构建的包发布出去。
3.2 前置依赖与构建命令
在 macOS arm64 上依次执行(README 原文命令):
$ brew install cmake ninja llvm@22 $ pnpm --filter @scriptc/llvm-darwin-arm64 build:nativeCMake 最低要求 3.24(CMakeLists.txt)。构建脚本(packages/llvm-darwin-arm64/scripts/build.mjs)随后会调用 CMake 配置并编译出bin/scriptc-llvm-codegen可执行文件。
3.3 可裁剪的 CMake 缓存变量
构建可通过 CMake cache 变量定制,这些变量同时也是"协议如实上报"的数据来源:
| Cache 变量 | 默认值 | 作用 |
|---|---|---|
SCRIPTC_PACKAGE_VERSION | 0.0.0-dev | 上报给version子命令的 npm 包版本 |
SCRIPTC_DEFAULT_TARGET | arm64-apple-macosx14.0.0 | 默认目标 triple |
SCRIPTC_DEFAULT_DATA_LAYOUT | arm64 Darwin 布局串 | 默认 data layout |
SCRIPTC_TARGET_BACKENDS | AArch64 | 分号分隔的后端列表(源码已支持AArch64/X86/WebAssembly三个分支,见 CMakeLists.txt) |
SCRIPTC_ALLOWED_TARGETS | arm64-apple-macosx14.0.0 | 逗号分隔、该助手接受的目标 triple 白名单 |
SCRIPTC_HELPER_ARCH | arm64 | 可执行文件的宿主架构 |
SCRIPTC_MUSL_BUILD | OFF | 是否为 Linux musl 构建全静态宿主可执行文件 |
以SCRIPTC_MUSL_BUILD为例:LLVM 官方 Linux 归档依赖宿主 libstdc++/glibc ABI,因此 musl 包把助手构建为全静态宿主可执行文件,而其配置的输出目标与运行时包仍保持 musl(CMakeLists.txt)。该模式下 CMake 会把 LLVM 庞大的静态导入图压平为每个目标的单条绝对归档路径,并按"依赖方在前"的后序逆序组织链接(CMakeLists.txt)。
3.4 针对 Apple 平台的瘦身链接
在 Apple 平台上,链接选项做了三重收束(CMakeLists.txt):
-exported_symbol,_main:只导出进程入口,避免把静态归档中的全局 LLVM 符号变成链接根;-dead_strip:丢弃小协议无法触达的函数与数据,使产物跨 LLVM bottle 布局保持稳定;-dead_strip_dylibs:剔除 Homebrew unversioned LLVM 22 bottle 无条件带入的 Z3 等传递 dylib,保证 npm sidecar 在没有 Homebrew 的环境里也能运行。
同理,CMakeLists.txt 会把zstd::libzstd_shared的导入位置改指静态版本,避免 npm sidecar 残留 Homebrew zstd dylib 依赖。
四、协议:刻意保持"小而带版本号"
README 强调协议"intentionally small and versioned"。它只有两个子命令,协议版本号在 target.h 中定义为字面量ProtocolVersion = "1",由编译期宏注入llvm_version等信息(CMakeLists.txt)。
4.1version:身份与能力上报
scriptc-llvm-codegen version --format=json从 main.cpp 可以看到,version命令严格要求--format=json(否则报usage错误),返回的 JSON 对象包含:
protocol_version:协议版本1;scriptc_package_version:CMake 注入的包版本;llvm_version:LLVM_VERSION_STRING(即 22.1.8);host_triple:sys::getDefaultTargetTriple()得到的宿主三元组;targets:编译进去的后端列表(解析SCRIPTC_TARGET_BACKENDS);supported_targets:白名单 triple 列表(解析SCRIPTC_ALLOWED_TARGETS);default_target与data_layout:来自默认 target machine 的真实布局字符串。
编译器在加载助手时会先调用version做"身份探测"(identity check),并把二进制内容与包清单纳入指纹快照,验证通过后才允许进入发射流程(见 native-codegen.ts)。
4.2emit:发射汇编或目标文件
scriptc-llvm-codegen emit --input app.ll --output app.o --filetype obj \ --target arm64-apple-macosx14.0.0 --opt-level 2 \ --relocation-model pic --diagnostic-format json --source-path app.ts参数语义全部由 emit.cpp 中的parseEmitOptions与后续校验实现,整理如下:
| 参数 | 必选 | 取值/默认 | 校验与源码依据 |
|---|---|---|---|
--input | 是 | 输入 LLVM IR 文件路径 | 缺失直接返回usage错误 |
--output | 是 | 输出文件路径 | 同上 |
--filetype | 否 | obj(默认)或asm | 非二者报invalid_filetype |
--target | 否 | 默认arm64-apple-macosx14.0.0 | 必须命中supported_targets,否则报unsupported_target |
--opt-level | 否 | 0\|1\|2\|3\|s\|z,默认2 | 非枚举值报invalid_opt_level |
--relocation-model | 否 | 仅支持pic(默认) | 其它值报invalid_relocation_model |
--diagnostic-format | 否 | 默认json | 非 JSON 时错误走纯文本 stderr |
--source-path | 否 | 空串即不设置 | 非空时写入 IR 模块的源文件名,供调试信息使用 |
注意--opt-level的取值语义在两个层面生效:
- 优化流水线层面(emit.cpp):
0→O0、1→O1、2→O2、3→O3、s→Os、z→Oz,默认返回O2; - 代码生成层面(target.cpp):
0→CodeGenOptLevel::None、1→Less、3→Aggressive,其余(2/s/z)统一映射到Default。
此外,target.cpp 的createTargetMachine固定使用Reloc::PIC_与CodeModel::Small——这正是--relocation-model pic被硬性约束为唯一选项的底层原因。
五、发射流水线:从 IR 到目标文件的七步走
完整流程实现在 emit.cpp 的emit()函数中,可分解为七个阶段:
- 参数校验:依次检查 target、filetype、opt-level、relocation-model 四类约束(前面表格已列)。
- IR 解析:用
parseIRFile读取--input,失败时报invalid_ir并把 SMDiagnostic 渲染进错误信息;若给了--source-path则写入Mod->setSourceFileName。 - TargetMachine 创建:
createTargetMachine内部先initializeTargets()(按编译期宏注册对应后端的 TargetInfo/Target/TargetMC/AsmPrinter,见 target.cpp),失败报target_machine_failed。 - IR 归一化与优化前验证**:把模块的 triple/data layout 与 TargetMachine 对齐,然后
verifyModule做合法性验证,失败报verification_failed。 - 默认流水线优化:通过
PassBuilder注册并串联 Loop/Function/CGSCC/Module 四层分析管理器,调用buildPerModuleDefaultPipeline按所选优化等级构建LLVM 22 默认的 per-module 流水线——README 特别点名其中包含coroutine lowering(协程降低)(emit.cpp)。 - 优化后验证:
verifyModule再次运行,失败报post_optimization_verification_failed,确保任何优化器缺陷都能在发射前被拦截。 - 原子发射与发布:这一阶段是可靠性的核心——详见下一节。
六、原子发布:失败或中断不会截断目标产物
这是 README 强调的最重要工程细节:"publishes through a private sibling file so a failed or interrupted request cannot truncate the requested output"。
实现上(emit.cpp):
- 在
--output旁构造私有 sibling 临时路径:<output>.tmp-%%%%%%,通过createUniqueFile获得唯一文件与 fd(失败报output_open_failed); - 用
legacy::PassManager+addPassesToEmitFile把代码生成写进临时文件(目标不支持该 filetype 时报emission_not_supported); flush后检查has_error(),有错误报output_write_failed并删除临时文件;- 校验临时文件大小非零,空文件报
output_verify_failed; - 最后
sys::fs::rename把临时文件原子改名为目标路径(失败报output_publish_failed)。
由于写入目标永远发生在rename那一刻,之前的任何失败、中断都只会留下一个可清理的临时文件,--output要么不存在、要么是完整产物,绝不会是半截内容。这一契约被编译器侧的调用方严格依赖:见 native-codegen.ts。
七、编译器侧集成:直接解析 npm 包,绝不搜索 PATH
README 声明"the compiler resolves that package directly and never searchesPATHfor this program"。这个承诺在 native-codegen.ts 中得到完整落实:
- 助手按目标平台从
helperPackage字段解析 npm 包(如 arm64 Darwin 对应@scriptc/llvm-darwin-arm64,定义见 targets.ts); - 定位到包内
bin/scriptc-llvm-codegen(Windows 下为.exe),并逐项校验文件存在、可读、可执行(模式位检查 +access(R_OK | X_OK)),否则抛出SC3003(missing_package / missing_binary / unusable_binary / identity_probe_failed / invalid_version_response 等细分类); - 进程调用使用
execFile(不经过 shell,也不会触发PATH查找),见 native-codegen.ts; - 调用
emit时,把编译器内部选项映射为协议参数:--target取目标平台的llvmTriple、--opt-level取options.optimization ?? "2"、--relocation-model取目标的 relocation model、--diagnostic-format固定json(native-codegen.ts)。
除此之外,编译器还围绕该协议建立了三道防线:
- 输入/输出隔离:IR 输入与阶段产物都写在
--output的 private sibling 路径(native-input、native-<kind>),与最终产物隔离; - 产物校验:
emit正常退出后仍检查 stage 文件存在、非空,否则抛SC3004 empty_output; - 缓存与指纹:用
snapshotNativeArtifactDependencies对包清单与二进制内容做指纹快照,跨调用用nativeArtifactDependenciesStillMatch校验,命中后走validCachedFile/installVerifiedCache跳过重复发射,并pruneBuildCache清理旧缓存(native-codegen.ts)。
也就是说,协议除了本身小,还刻意保持可探测、可验证、可缓存:编译器总是先问"你是谁、支持什么"(version),再带着白名单 triple 去发射(emit),中途任何异常都以结构化的方式被捕获。
八、诊断协议:结构化的失败与"永不裸崩"
所有错误统一走 diagnostics.cpp:
reportError(code, message)在--diagnostic-format json下输出{"ok": false, "code": ..., "message": ...}到 stderr,返回码1;否则输出纯文本scriptc-llvm-codegen: <message>。各阶段的错误码上文已逐一提及(unsupported_target、invalid_ir、verification_failed、post_optimization_verification_failed、output_publish_failed等)。- 更关键的是
installFatalDiagnosticHandler():进程启动即注册 LLVM fatal handler,把report_fatal_error转成 JSON 诊断后以_Exit(70)退出(diagnostics.cpp)。配合 main.cpp 中由环境变量SCRIPTC_LLVM_TEST_FATAL触发的自测分支,可以验证"未来任何 LLVM fatal 都不会变成 Node 调用方看到的裸 abort/栈崩溃"。
九、总结:一个可验证的、版本化的原生发射边界
从 native/llvm-codegen/README.md 出发,结合 native/llvm-codegen/src 各 C++ 源文件、CMakeLists.txt、packages/llvm-darwin-arm64/package.json 与 native-codegen.ts,可以归纳出这套设计的四条主线:
- 边界清晰:LLVM 专属构建链(CMake、静态归档、RTTI/ABI、dylib 依赖)被完全隔离在进程外助手内,Node 编译器只需调用两个子命令;
- 协议可审计:
ProtocolVersion = 1+version自述 + 白名单 triple 校验,任何版本漂移都能在调用前被发现; - 产物可安全发布:临时 sibling 文件 +
rename的原子发布,配合优化前后双重verifyModule与空文件检查,杜绝截断产物与损坏产物; - 失败可结构化:JSON 错误码 + fatal handler + 编译器侧
SC3003/SC3004分类,使整条原生链路在 CI 与生产环境均可诊断。
对想要深入原生发射链路或为其他平台打包助手的开发者,建议按"协议 → 构建 → 流水线 → 集成"的顺序阅读:target.h(协议常量与目标白名单)→ CMakeLists.txt(可裁剪构建)→ emit.cpp(核心流水线)→ native-codegen.ts(调用方契约)。
- 编译器
- 语言运行时
- 开发工具
- CLI
【免费下载链接】scriptc
TypeScript-to-Native Compiler
相关推荐
Mono LLVM 后端深度解析:用 LLVM 替代内置 JIT 的代码生成架构与实战指南
Mono LLVM 后端深度解析:用 LLVM 替代内置 JIT 的代码生成架构与实战指南 Mono 运行时内置了一个自行维护的即时编译器(JIT),而在 .N
语言运行时标准库JIT编译编译器codegen:代码生成的智能助手
codegen:代码生成的智能助手 项目介绍 在软件开发的世界中,代码生成和代码转换一直是提高效率、减少人工干预的重要手段。今天,我要为大家介绍一个强大的开源项
GraalVM Native Image LLVM 后端(LLVM Backend)完全指南:原理、构建、调试与新增目标架构
GraalVM Native Image LLVM 后端(LLVM Backend)完全指南:原理、构建、调试与新增目标架构 本文基于 GraalVM 仓库中的
编译器JIT编译语言运行时高性能计算内存管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考