- 开始日期: 2018-01-08
- RFC PR: (请留空)
- 跟踪问题: (请留空)
摘要
添加 #[wasm_bindgen]
的功能,用于处理、加载和处理对本地 JS 文件的依赖关系。
-
现在可以使用
module
属性显式导入文件# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... } #}
-
现在可以使用
inline_js
属性内联导入 JS 模块# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
-
--browser
标志被重新用于为浏览器生成 ES 模块,而--no-modules
已被弃用,取而代之的是此标志。 -
--nodejs
不会立即支持本地 JS 代码段,但将在将来支持。
动机
wasm-bindgen
的目标是实现 Rust 和 JS 之间的轻松互操作。虽然编写自定义 Rust 代码非常容易,但实际上编写自定义 JS 并将其与 #[wasm_bindgen]
关联起来非常困难(请参阅 rustwasm/wasm-bindgen#224)。#[wasm_bindgen]
属性目前仅支持从 ES 模块导入函数,但即使这样,支持也受到限制,并且只是假设 ES 模块字符串存在于最终的应用程序构建步骤中。
目前,没有一种可组合的方式可以让一个 crate 拥有它构建的一些辅助 JS,这些 JS 最终会无缝地包含到最终构建的应用程序中。例如,rand
crate 无法轻松地包含本地 JS(也许是为了检测它应该使用哪种随机性 API),除非对最终的工件施加严格的要求。
从自定义 JS 文件进行导入的符合人体工程学支持似乎也是像 stdweb
这样的框架构建类似 js!
的宏所必需的。这涉及在编译时生成 JS 代码段,这些代码段需要包含到最终的包中,而这正是此新属性旨在实现的功能。
利益相关者
此 RFC 的一些主要利益相关者是
#[wasm_bindgen]
的用户- 希望为其 crate 添加 wasm 支持的 crate 作者。
stdweb
作者- 捆绑器 (webpack) 和
wasm-bindgen
集成人员。
这里的大多数人都会被抄送至 RFC,并且随时欢迎联系更多人!
详细说明
此提案涉及许多移动部件,所有这些部件都旨在协同工作,为将本地 JS 文件包含到最终的 #[wasm_bindgen]
工件中提供一个简化的故事。我们将在下面逐个查看每个部分。
新的语法功能
这里提出的最面向用户的更改是对 #[wasm_bindgen]
中 module
属性的重新解释以及 inline_js
属性的添加。现在可以使用它们来导入本地文件并定义本地导入,如下所示
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... definitions } #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
第一个声明表示函数和类型块等都从 /js/foo.js
文件导入,该文件相对于当前文件,并以 crate 根目录为根目录。第二个声明将 JS 内联列为字符串文字,而 extern
块描述了内联模块的导出。
建议使用以下规则来解释 module
属性。
-
如果字符串以指向 cargo 构建目录的绝对路径的平台特定表示形式开头(由
$OUT_DIR
标识),则该字符串将被解释为输出目录中的文件路径。这适用于在构建过程中生成 JS 文件的构建脚本。 -
如果字符串以
/
、./
或../
开头,则它被视为指向本地文件的路径。如果不是,则它将按原样作为 ES 模块导入传递。 -
所有路径都相对于当前文件解析,就像 Rust 自己的
#[path]
、include_str!
等一样。但是,目前尚不清楚我们如何实际对相对文件执行此操作。因此,所有路径都必须以/
开头。当proc_macro
具有稳定的 API(或者我们以其他方式弄清楚如何操作)时,我们可以开始允许以./
和../
为前缀的路径。
这有望大致匹配程序员的期望以及浏览器和捆绑器中已有的约定。
inline_js
属性并非真正用于通用开发,而是用于过程宏的一种方式,这些过程宏目前无法依赖 $OUT_DIR
的存在来生成要导入的 JS。
导入的 JS 格式
所有导入的 JS 都必须使用 ES 模块语法编写。最初,JS 必须手动编写,不能进行后处理。例如,JS 不能使用 TypeScript 编写,也不能由 Babel 或类似工具编译。
例如,一个库可能包含
# #![allow(unused_variables)] #fn main() { // src/lib.rs #[wasm_bindgen(module = "/js/foo.js")] extern "C" { fn call_js(); } #}
伴随
// js/foo.js
export function call_js() {
// ...
}
请注意,js/foo.js
使用 ES 模块语法导出 call_js
函数。当从 Rust 调用 call_js
时,它将调用 foo.js
中的 call_js
函数。
通过依赖关系传播
file
属性的目的是与依赖关系无缝协作。使用 #[wasm_bindgen]
构建项目时,你不应该需要知道你的依赖关系是否使用本地 JS 代码段!
#[wasm_bindgen]
宏将在编译时读取提供的文件的内容(如果有)。此文件将以 wasm-bindgen 特定的格式序列化到 wasm-bindgen 自定义部分中。由 rustc 生成的最终 wasm 工件将在其自定义部分中包含所有引用的 JS 文件内容。
wasm-bindgen
CLI 工具将提取所有这些 JS 并将其写入文件系统。生成的 wasm 文件(或 wasm-bindgen 生成的 shim JS 文件)将使用相对导入导入所有发出的 JS 文件。
更新 wasm-bindgen
输出模式
wasm-bindgen
今天有几种输出生成模式。这些输出模式主要围绕模块与非模块以及模块的定义方式展开。此 RFC 建议我们更多地从这种模式转向环境,例如与 node.js 兼容的代码与与浏览器兼容的代码(这不仅仅涉及模块格式)。这意味着,在环境支持多个模块系统或模块系统是可选的(浏览器支持 es 模块和非模块)的情况下,wasm-bindgen
将选择它认为最适合的模块系统,只要它与该环境兼容即可。
wasm-bindgen
的当前输出模式是
-
默认 - 默认情况下,
wasm-bindgen
发出的输出假设 wasm 模块本身是 ES 模块。这将自然地与本身是 ES 模块的自定义 JS 代码段协同工作,因为它们只是本地输出目录中找到的图中的更多模块。此输出模式目前仅可由 Webpack 等捆绑器使用,默认输出无法在 Web 浏览器或 Node.js 中加载。 -
--no-modules
-wasm-bindgen
的--no-modules
标志与 ES 模块不兼容,因为它旨在通过<script>
标签包含,而<script>
标签不是模块。此模式与今天一样,如果上游 crate 包含本地 JS 代码段,则将无法正常工作。 -
--nodejs
-wasm-bindgen
的此标志指示输出应针对 Node.js 定制,特别是使用 CommonJS 模块约定。在此模式下,wasm-bindgen
最终将使用 Rust 中的 JS 解析器将本地导入的 JS 模块的 ES 语法重写为 CommonJS 语法。 -
--browser
- 目前,此标志与默认输出模式相同,只是输出针对浏览器环境略微定制(例如,假设TextEncoder
是环境可用的)。此 RFC 建议重新利用此标志(将其破坏),改为生成一个可以在 Web 浏览器中本地加载的 ES 模块,但除此之外,它与今天
--no-modules
的接口类似,如下所述。
此 RFC 建议重新考虑这些输出模式,如下所示
目标环境 | CLI 标志 | 模块格式 | 用户体验 | 如何加载本地 JS 代码段? |
---|---|---|---|---|
没有捆绑器的 Node.js | --nodejs | Common.js | require() 主 JS 粘合文件 | 主 JS 粘合文件 require() crate 的本地 JS 代码段。 |
没有捆绑器的 Web | --browser | ES 模块 | 指向主 JS 粘合文件的 <script> ,使用 type=module | import 语句会导致对 crate 的本地代码段的额外网络请求。 |
有捆绑器的 Web | 无 | ES 模块 | 指向主 JS 粘合文件的 <script> | Bundler 将 crate 的本地代码片段链接到主 JS 粘合文件中。除了 wasm 模块本身之外,没有额外的网络请求。 |
值得注意的是,就 wasm-bindgen
而言,使用和不使用 bundler 的浏览器几乎相同:唯一的区别是,如果我们假设使用 bundler,我们可以依赖 bundler 为我们提供 wasm-as-ES-module 的 polyfill。请注意,这里的 --browser
今天相对来说有很大的不同,因此将是一个重大更改。人们认为 --browser
的使用量很小,我们可以摆脱这种改变,但欢迎对此提出反馈!
--no-modules
标志不再适用,因为 --browser
用例旨在取代它。请注意,此 RFC 提案目前只让面向 bundler 和面向浏览器的模式支持本地 JS 代码片段,同时为最终在 Node.js 中支持本地 JS 代码片段铺平道路。--no-modules
最终也可以像 Node.js 一样支持(一旦我们解析 JS 文件并重写导出内容),但这里建议从 --no-modules
迁移到 --browser
。
--browser
输出目前被认为导出一个初始化函数,该函数在被调用后,返回的 promise 被解析(就像今天的 --no-modules
一样),将导致所有导出内容在被调用时生效。在 promise 解析之前,所有导出内容在被调用时都会抛出错误。
依赖其他 JS 文件的 JS 文件
此 RFC 的一个棘手之处是,当本地 JS 代码片段依赖于其他 JS 文件时。例如,您的 JS 代码可能如下所示
// js/foo.js
import { foo } from '@some/npm-package';
import { bar } from './bar.js'
// ...
如上所述,这些导入将无法正常工作。我们打算明确说明这是此设计的一个初始限制。我们目前还不支持 JS 代码片段之间的导入,但我们最终应该能够做到。
从长远来看,为了支持 --nodejs
,我们需要某种程度的 JS ES 模块解析器。一旦我们可以解析导入本身,#[wasm_bindgen]
在扩展期间就可以相对轻松地加载传递性包含的文件。例如,在上面的文件中,我们将 ./bar.js
包含到 wasm 自定义部分中。在这个未来的世界中,我们只需要在最终输出工件被发出时重写 ./bar.js
(如果有必要)。此外,随着 wasm-pack
和 wasm-bindgen
中对 NPM 包的支持(未来的目标),我们可以验证 package.json
中的条目是否与找到的导入相匹配。
访问 wasm 内存/表
与 wasm 模块交互的 JS 代码片段通常需要与与 wasm 模块关联的 WebAssembly.Memory
和 WebAssembly.Table
实例一起工作。此 RFC 提案使用 wasm 本身来传递这些对象,如下所示
# #![allow(unused_variables)] #fn main() { // lib.rs #[wasm_bindgen(module = "/js/local-snippet.js")] extern { fn take_u8_slice(memory: &JsValue, ptr: u32, len: u32); } #[wasm_bindgen] pub fn call_local_snippet() { let vec = vec![0,1,2,3,4]; let mem = wasm_bindgen::memory(); take_u8_slice(&mem, vec.as_ptr() as usize as u32, vec.len() as u32); } #}
// js/local-snippet.js
export function take_u8_slice(memory, ptr, len) {
let slice = new UInt8Array(memory.arrayBuffer, ptr, len);
// ...
}
这里使用 wasm_bindgen::memory()
现有的内在函数将内存对象传递到导入的 JS 代码片段中。为了镜像这一点,我们将在 wasm-bindgen
crate 中添加 wasm_bindgen::function_table()
作为内在函数,以访问函数表并将其作为 JsValue
返回。
最终,我们可能需要一种更明确的方式来导入内存/表,但目前,这对于表达能力来说应该足够了。
缺点
-
最初的 RFC 非常保守。它不适用于
--nodejs
或--no-modules
。此外,它最初也不支持 JS 代码片段导入其他 JS。请注意,所有这些都打算在将来得到支持,只是认为现在可能需要比我们现在需要的更多设计。 -
JS 代码片段必须用纯 ES 模块 JS 语法编写。常见的预处理器,如 TypeScript,无法使用。目前尚不清楚如何导入这种预处理的 JS。希望 JS 代码片段足够小,这不会成为太大问题。较大的 JS 代码片段始终可以提取到 NPM 包中并在那里进行后处理。请注意,作者始终可以手动运行 TypeScript 编译器来处理这些用例。
-
建议弃用相对流行的
--no-modules
标志,转而使用--browser
标志,该标志本身将相对于今天有一个重大更改。不过,人们认为--browser
很少使用,因此可以安全地进行更改,并且人们还认为我们希望避免破坏今天--no-modules
的现状。 -
本地 JS 代码片段需要用 ES 模块语法编写。这可能是一个有点主观的立场,但它旨在让
wasm-bindgen
更容易添加未来的功能,同时继续与 JS 协同工作。然而,ES 模块系统是整个生态系统中唯一已知的官方标准,因此希望这是一个编写本地 JS 代码片段的明确选择。
基本原理和替代方案
此系统的首要替代方案是类似于 stdweb 中的 js!
宏。这允许在 Rust 代码中直接编写小的 JS 代码片段,然后 wasm-bindgen
将拥有生成适当垫片的知识。此 RFC 提案识别 module
路径而不是这种方法,因为它被认为是一种更通用的方法。此外,它打算 js!
宏可以建立在 module
指令上,包括本地文件路径。wasm-bindgen
crate 有一天可能会增长一个类似 js!
的宏,但人们认为最好从更保守的方法开始。
ES 模块的一个替代方案是简单地将所有 JS 代码串联在一起。这样,我们就不必解析任何东西,而是将所有内容都放到一个文件中。然而,这种方法的缺点是,它很容易导致命名空间冲突,并且它还迫使每个人都同意模块格式,并且存在迫使最终产品采用模块格式的风险。
在 wasm-bindgen 时间发出小文件的另一种替代方案是在运行时通过将它们留在 wasm 可执行文件的自定义部分中来解压缩所有文件。然而,这反过来可能会违反某些 CSP 设置(特别是严格的设置)。
未解决的问题
-
是否有必要在最初支持
--nodejs
? -
是否有必要在最初支持本地 JS 代码片段中的本地 JS 导入?
-
今天是否有已知的 JS ES 模块解析器?我们是否被迫包含一个完整的 JS 解析器,或者我们可以使用一个只处理 ES 语法的最小解析器?
-
我们如何处理想要被最终 wasm 文件引用的其他资产,如 CSS、HTML 或图像?