Gleam 外部函数单目标实现的编译限制解析:external_only_javascript 测试项目实战
【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam
在 Gleam 编译器的多目标(Erlang 与 JavaScript)构建体系中,通过@external注解绑定的外部函数(External Function)可以只针对单一目标提供实现。本文以仓库中的 external_only_javascript 测试项目为主线,从配置、源码、编译错误到验证脚本,完整剖析"外部函数仅实现于 JavaScript 时,Erlang 目标构建如何失败、JavaScript 目标又如何成功"的完整机制,帮助读者理解 Gleam FFI(外部函数接口)的跨目标约束与编译器校验逻辑。
项目定位:一个"只能跑在 JavaScript 上"的演示工程
external_only_javascript是 Gleam 仓库test/目录下的一组集成测试工程之一。它的 README 用一句话点明了项目本质:
A project that can only be run on JavaScript due to an external function.
即:由于一个外部函数只有 JavaScript 实现,整个项目只能在 JavaScript 目标上运行。与它镜像对应的是 external_only_erlang——一个因外部函数仅实现于 Erlang 而只能运行在 Erlang 上的工程。二者共同验证了 Gleam 编译器对"外部函数缺失目标实现"场景的报错行为。
项目文件结构如下:
test/external_only_javascript/ ├── src/ │ ├── external_only_javascript.gleam # 使用 @external(javascript, ...) 注解的入口模块 │ └── external_only_javascript_ffi.mjs # JavaScript 侧的外部函数实现 ├── gleam.toml # 项目配置(target = "javascript") ├── manifest.toml # 依赖锁定清单 └── test.sh # 自动化验证脚本配置文件解读:target 与依赖
gleam.toml 中的关键配置如下:
name = "external_only_javascript" version = "1.0.0" target = "javascript" [dependencies] hello_joe = "~> 1.0" [dev_dependencies]target = "javascript":声明项目的默认构建目标为 JavaScript。Gleam 支持在配置中指定erlang或javascript作为默认目标,命令行可通过--target覆盖;- 依赖
hello_joe(版本约束~> 1.0):一个纯 Gleam 编写的依赖模块,用于验证"依赖模块在两种目标下都能正常构建运行"这一前提(详见 manifest.toml,其锁定了gleam_stdlib 0.34.0与hello_joe 1.0.0)。
从配置层面看,这个项目本身并没有声明任何特殊机制;真正的"陷阱"藏在入口模块的@external注解中。
核心源码:只绑定 JavaScript 实现的外部函数
入口模块 external_only_javascript.gleam 的完整内容如下:
// This function is only implemented for JavaScript, so if we try and call it // from Erlang, or build this package for Erlang, then the compiler will // (should) emit an error. @external(javascript, "./external_only_javascript_ffi.mjs", "main") pub fn main() -> Nil逐项拆解这个@external注解:
| 参数 | 值 | 含义 |
|---|---|---|
| 目标平台 | javascript | 指明该外部实现只适用于 JavaScript 目标 |
| 实现路径 | "./external_only_javascript_ffi.mjs" | 相对于当前 Gleam 模块的 FFI 文件路径 |
| 导出函数名 | "main" | 在 FFI 文件中调用的导出函数 |
对应的 FFI 实现 external_only_javascript_ffi.mjs 极为简单:
export function main() { console.log("Hello"); }注意两个细节:
- 没有 Erlang 实现:模块中不存在
@external(erlang, ...)注解,也不存在.erl文件,因此该函数在 Erlang 目标下没有任何可用实现; - 注解与模块名同名函数的组合:
main函数完全由外部实现承担,Gleam 模块内没有函数体(body),这正是"外部函数替换 Gleam 实现"的标准用法——当存在外部实现时,Gleam 允许函数体为空,但要求显式标注类型注解。
编译器侧校验:目标实现缺失如何被拦截
当为 Erlang 目标构建该包时,编译器会在类型检查(type checking)阶段发现"当前目标没有实现",并报错。这一逻辑位于 compiler-core/src/analyse.rs#L566-L576:
// Find the external implementation for the current target, if one has been given. let external = target_function_implementation(target, &external_erlang, &external_javascript); // The function must have at least one implementation somewhere. let has_implementation = self.ensure_function_has_an_implementation( &body, &external_erlang, &external_javascript, location, );其中target_function_implementation负责按当前构建目标挑选对应的外部实现(Erlang 实现或 JavaScript 实现),而ensure_function_has_an_implementation则校验函数是否"至少在某处存在实现"。当函数体为空、且当前目标没有任何外部实现时,就会抛出UnsupportedExpressionTarget类型的错误。
对应的错误文案定义在 compiler-core/src/error.rs#L4602-L4630:
This value is not available as it is defined using externals, and there is no implementation for the {target} target. Did you mean to build for a different target?即标题为"Unsupported target"的编译错误,提示"该值由外部函数定义,但当前目标没有实现",并建议改用其他目标构建。
此外,对于公开(pub)函数,编译器还有更严格的约束:所有包内公开函数都必须能针对当前目标编译。对应错误为UnsupportedPublicFunctionTarget,文案见 compiler-core/src/error.rs#L4632-L4645:
The `{name}` function is public but doesn't have an implementation for the {target} target. All public functions of a package must be able to compile for a module to be valid.这意味着"只实现单一目标的外部函数"只能安全地用于**私有(非 pub)**函数,或仅在单一目标下发布/使用的包中。
验证脚本:test.sh 的完整行为矩阵
test.sh 是一份可直接运行的 shell 验证脚本(通过GLEAM_COMMAND环境变量可指定 Gleam 命令,默认为cargo run --quiet --,即从源码运行编译器)。它逐条断言了以下行为:
第一步:依赖模块在任何目标下都可用
g run --module=hello_joe g run --module=hello_joe --target=erlang g run --module=hello_joe --target=javascripthello_joe是纯 Gleam 依赖,没有外部函数,因此在默认目标、Erlang 目标和 JavaScript 目标下都能成功运行。
第二步:JavaScript 目标的构建与运行应当成功
g build --target=javascript g run --target=javascript由于main的外部实现存在于external_only_javascript_ffi.mjs,JavaScript 目标可以完成构建,并输出Hello。
第三步:Erlang 目标的构建与运行应当失败
if g build --target=erlang; then echo "Expected build to fail" exit 1 fi if g run --target=erlang; then echo "Expected run to fail" exit 1 fi脚本特意强调"even if previously an Erlang dependency was built"(即使之前已经构建过 Erlang 依赖)——也就是说,编译缓存中已存在 Erlang 目标产物,也不会让主模块逃过校验:main没有 Erlang 实现,Erlang 构建必然失败,脚本据此反向断言(if ... then exit 1)验证编译器确实报错。
对照实验:external_only_erlang 的镜像场景
为了确认该机制与目标方向无关,仓库提供了镜像工程 external_only_erlang。其入口模块 external_only_erlang.gleam 使用:
@external(erlang, "external_only_erlang_ffi", "main") pub fn main() -> Nil对应验证脚本 test.sh 的行为与 JavaScript 版本完全对称:
g run --module=hello_joe --target=erlang与--target=javascript均成功(依赖无限制);g build --target=erlang与g run --target=erlang成功(Erlang 有实现);g build --target=javascript与g run --target=javascript失败(JavaScript 无实现,即使此前已构建过 JavaScript 依赖)。
这组对照工程共同证明:"外部函数缺失某目标实现"的编译错误由编译器统一保证,不依赖构建缓存、不区分目标方向,且错误在构建(build)与运行(run)阶段都会触发。
实践要点与小结
通过这个测试项目,可以提炼出使用 Gleam 外部函数时的关键实践结论:
@external(target, module, fn)是实现 FFI 的标准入口:目标参数只填erlang或javascript之一,即可声明单目标实现;两个目标各写一个@external注解(配合@target.erlang/@target.javascript条件编译)则实现双目标 FFI;- 单目标实现意味着单目标可用:若一个包对外暴露了仅单目标实现的外部函数,则另一个目标将无法构建该包——这正是
external_only_javascript项目名字的由来; - 编译器在类型检查阶段拦截:相关逻辑位于 compiler-core/src/analyse.rs 的
target_function_implementation与ensure_function_has_an_implementation,错误文案见 compiler-core/src/error.rs 的UnsupportedExpressionTarget与UnsupportedPublicFunctionTarget,并附有"Did you mean to build for a different target?"的提示; - 依赖模块不受影响:纯 Gleam 依赖(如测试中的
hello_joe)在两种目标下均可正常构建运行,限制只作用于自身使用了单目标外部函数的模块; - 构建缓存不掩盖错误:即使此前已成功构建过另一目标的依赖,主模块的目标缺失错误依然会触发,保证跨目标构建的确定性。
对希望为单一平台(如纯 JavaScript 的 Deno/Node 项目,或纯 Erlang/OTP 项目)编写 Gleam 包的开发者而言,external_only_javascript与external_only_erlang这两个测试工程就是最精简的可复现模板:一份 Gleam 入口、一份 FFI 文件、一份target配置,外加test.sh中基于--target的成功/失败断言,即可完整验证你的 FFI 跨目标约束是否符合预期。
【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考