11个版本 (稳定版)

3.0.0 2023年4月28日
3.0.0-beta.12023年2月14日
3.0.0-alpha.12023年1月31日
2.4.0 2022年10月4日
1.0.0 2022年3月28日

#84WebAssembly 中排名

Download history 19/week @ 2024-04-21 32/week @ 2024-04-28 32/week @ 2024-05-05 34/week @ 2024-05-12 31/week @ 2024-05-19 29/week @ 2024-05-26 26/week @ 2024-06-02 20/week @ 2024-06-09 25/week @ 2024-06-16 28/week @ 2024-06-23 5/week @ 2024-06-30 19/week @ 2024-07-07 61/week @ 2024-07-14 21/week @ 2024-07-21 23/week @ 2024-07-28 16/week @ 2024-08-04

每月 124 次下载
用于 4 个crate3 个直接使用)

MIT/Apache

200KB
4.5K SLoC

fp-bindgen

Crates.io Discord Shield

全栈WASM插件的绑定生成器。

与其他"bindgen"工具的比较

fp-bindgen 不是生成Wasm绑定的唯一工具。最著名的工具可能是 wasm-bindgen,尽管它仅限于浏览器环境中的Rust模块。一个更通用的替代方案,基于Wasm 接口类型提议,是 wit-bindgen。我们确实相信接口类型是Wasm绑定的未来,但短期内,fp-bindgen 提供的绑定可以与稳定的序列化格式一起工作,这有助于我们避免版本问题,并使Serde等工具兼容。

值得一提的是,尽管我们有规范,允许为其他语言贡献生成器,但 fp-bindgen 倾向于 Rust。它使用 Rust 数据结构和函数签名作为其 "协议格式",使它与Rust生态系统中的现有crate紧密集成。

下表旨在突出不同工具之间的主要区别

功能 fp-bindgen wasm-bindgen wit-bindgen
主机环境 Rust (Wasmer), TypeScript* JS/TS Rust/Python (Wasmtime), JS/TS*
客户语言 Rust* Rust Rust, C*
协议格式 Rust (使用宏) N/A .wit
序列化格式 MessagePack JSON 自定义
可以使用现有的Rust类型

*) 这些是目前 支持的 选项。未来可能会有更多。

快速入门

  • 查看仓库,使用 git clone 命令。由于仓库中使用了符号链接,因此在 Windows 系统上请使用以下命令代替:git clone -c core.symlinks=true
  • 要快速构建示例协议和插件并运行所有可用测试,请使用:cargo xtask test

用法

使用 fp-bindgen 是一个三步过程

定义协议

在使用此库生成绑定之前,您首先需要定义一个协议,该协议指定了可以被 运行时(Wasm 宿主)和 插件(Wasm 客户端模块)调用的函数。该协议指定了函数声明,这些声明被放置在两个宏中:fp_import!fp_export!。这些宏指定了哪些函数可以被导入和导出,从 插件的角度来看。换句话说,fp_import! 函数可以被插件调用,并且必须由运行时实现,而 fp_export! 函数可以被运行时调用,并且可能由插件实现。

示例

fp_bindgen::prelude::fp_import! {
    fn my_imported_function(a: u32, b: u32) -> u32;
}

fp_bindgen::prelude::fp_export! {
    fn my_exported_function(a: u32, b: u32) -> u32;
}

重要提示:在调用 fp_bindgen!() 的同一个模块中,必须恰好有一个 fp_import! 块和一个 fp_export! 块。如果您只有导入或只有导出,应为此创建一个空的块。

数据结构

除了原始数据类型之外,函数还可以将其参数和返回值作为 Rust structenum 传递,但只能按值传递(当前不支持通过 Wasm 互通传递引用),并且只能用于实现 Serializable 的类型。

示例

#[derive(fp_bindgen::prelude::Serializable)]
pub struct MyStruct {
    pub foo: i32,
    pub bar: String,
}

fp_bindgen::prelude::fp_import! {
    fn my_function(data: MyStruct) -> MyStruct;
}

请注意,Serializable 默认实现了某些常见的标准类型,例如 OptionVec 以及其他容器类型。

异步函数

函数也可以是 async 的,其行为与预期一致

示例

fp_bindgen::prelude::fp_import! {
    async fn my_async_function(data: MyStruct) -> Result<MyStruct, MyError>;
}

使用现有的 Rust 类型

有时您可能希望为您的协议使用 Rust 类型,同时您还希望在生成的运行时或插件实现中直接使用这些类型。在这种情况下,数据类型的生成可能迫使您执行不必要的复制,因此我们允许显式注解来导入现有的定义而不是生成一个新的定义

示例

use fp_bindgen::prelude::Serializable;

#[derive(Serializable)]
#[fp(rust_module = "my_crate::prelude")]
pub struct MyStruct {
    pub foo: i32,
    pub bar_qux: String,
}

在这个例子中,MyStruct 具有双重功能:它既作为协议的类型定义(通过 fp-bindgenSerializable 特性)存在,仍然可以用于生成 TypeScript 类型定义,例如。 并且 它作为可以由 Rust Wasmer 运行时直接使用的类型存在,假设运行时可以从 my_crate::prelude 中导入它。

请注意,在这种情况下,您有更大的责任确保定义满足代码生成器的需求,这就是为什么需要在这里手动添加Serde的特性和注释,以符合生成器通常会生成的格式。

目前,此功能仅限于通过rust_module注释的Rust生成器。对我们来说,这是有意义的,因为该协议本身也是使用Rust语法指定的。如果需要,我们也可以将其扩展到TypeScript生成器,但这将意味着用户需要承担更大的责任,以保持他们的TypeScript类型与协议同步。

Cargo功能

fp-bindgen包支持可选的Cargo功能,以与一些常见的包生态系统中的类型兼容。

  • bytes-compat:启用与bytes::Bytes类型的兼容性。
  • http-compat:启用与http包中各种类型的兼容性。
  • rmpv-compat:启用与rmpv::Value类型的兼容性。
  • serde-bytes-compat:启用与serde_bytes::ByteBuf类型的兼容性(Bytes类型是一个引用类型,而fp-bindgen通常不支持)。
  • serde-json-compat:启用与serde_json::Mapserde_json::Value类型的兼容性。
  • time-compat:启用与timePrimitiveDateTimeOffsetDateTime类型的兼容性。

生成绑定

要基于您的协议生成绑定,您首先需要创建一个为您生成它们的函数。创建此函数很容易,因为可以使用fp_bindgen宏为您创建其实现。

let bindings_type = fp_bindgen::BindingsType::RustWasmerRuntime;

fp_bindgen::prelude::fp_bindgen!(fp_bindgen::BindingConfig {
    bindings_type,
    path: &format!("bindings/{}", bindings_type)
});

目前,我们支持以下绑定类型

  • BindingsType::RustPlugin:为Rust插件生成绑定。
  • BindingsType::RustWasmerRuntime:为与Wasmer一起使用生成运行时绑定。
  • BindingsType::TsRuntimeWithExtendedConfig:为TypeScript运行时生成绑定。

请注意,某些绑定类型需要额外的配置参数。

使用绑定

使用生成的绑定与不同类型的使用方式不同。

使用Rust插件绑定

生成Rust插件绑定的生成器生成一个完整的crate,允许它被插件链接。该插件可以导入所有从fp_import!块导出的函数,就像任何其他函数一样调用它们。

为了导出在fp_export!块中定义的函数,可以使用以下方式使用导出的fp_export_impl宏。

#[fp_bindgen_macros::fp_export_impl(bindings_crate_path)]
fn my_exported_function(a: u32, b: u32) -> u32 {
    /* ... */
}

bindings_crate_path预计将与导入绑定crate的模块路径匹配。函数签名必须与fp_export!函数之一完全匹配。

在编译插件时,请记住,与“wasm32-unknown-unknown”目标编译,否则您将收到链接器错误。

查看example-plugin/目录,了解使用从我们的example-protocol/生成的绑定创建插件的示例。

使用Rust Wasmer运行时绑定

我们Rust Wasmer运行时的生成器工作方式略有不同。它不是生成一个crate,而是生成两个文件:bindings.rstypes.rs。这些文件可以放置在你选择的模块中(我们在 example-rust-runtime/ 目录中选择了名为 spec 的模块)。

作为运行时的实现者,你需要负责在同一模块中实现生成的文件中的 fp_import! 函数。你可以在 example-rust-runtime/spec/mod.rs 中看到这个例子(请注意,示例运行时只有在你在 example-protocol/ 目录中运行 cargo run 之后才能构建)。

最后,bindings.rs 文件包含一个构造函数(Runtime::new()),你可以用它来使用提供的Wasm模块作为blob实例化Wasmer运行时。在 Runtime 实例上提供了 fp_export! 函数作为方法。请注意,fp_export! 函数的实现始终由插件决定,尝试调用缺失的实现可能会失败并产生一个 InvocationError::FunctionNotExported 错误。

使用TypeScript运行时绑定

TypeScript运行时生成器可以与浏览器、Node.js 和 Deno 一起工作。

它的工作方式与Wasmer运行时类似,但它生成一个 index.ts 和一个 types.tstypes.ts 包含所有数据结构的类型定义,而 index.ts 导出 createRuntime() 函数,你可以用它来实例化运行时。在实例化时,你应提供所有 fp_import! 函数的实现,而返回的 Promise 将提供一个对象,其中包含提供的插件已实现的全部 fp_export! 函数。

示例

请查看 examples/README.md,了解如何使用 fp-bindgen 的各种示例。

规范

我们编写了一个规范,描述了绑定使用的原语。这个规范主要针对那些想要了解绑定底层工作原理的人,如果你想要为你的语言实现绑定,这可能很有价值。

如果你是这样的用户,请查看 docs/SPEC.md

已知限制

  • 数据类型可能只能包含值类型。目前不支持引用。
  • 使用完整的模块路径引用类型可能导致在类型发现期间发生不匹配。请使用 use 语句导入类型,并仅通过它们的名称来引用它们。
  • TypeScript 绑定对 64 位整数处理不够一致。当作为原始数据(作为普通函数参数或返回值)传递时,它们将使用 BigInt 类型进行编码。但当它们是 MessagePack 编码数据类型的一部分时,它们将使用 number 进行编码,这实际上限制了它们的大小,最大为 2^53 - 1。更多信息请参阅:https://github.com/msgpack/msgpack-javascript/issues/115

常见问题解答

我给我的类型添加了 Serializable 继承,为什么我没有看到它在绑定中包含?

你是在 fp_import!fp_export! 函数中使用该类型吗?继承 Serializable 使您可以使用该类型作为协议的一部分,但它只有在实际引用时才会成为生成的绑定的部分。请注意,类型可以通过 fp_import!fp_export! 函数直接引用,或者通过另一个已使用的类型间接引用。

如果类型既没有被任何作为您协议一部分的函数直接引用,也没有被间接引用,您可以通过在 fp_import!fp_export! 部分添加引用该类型的 use 语句来强制包含它

fp_bindgen::prelude::fp_import! {
    use MyType;
}

您是否引用了该类型,但它仍然没有包含在您的绑定中?请 提交一个问题

我可以使用别名吗?

是的,但因为别名不能有 derive 宏,所以请重复在 fp_import!fp_export! 部分中的别名

fp_bindgen::prelude::fp_import! {
  type MyType = SomeOtherType;
}

版本控制呢?

通常,版本控制不在这个项目的范围内。这意味着检查您执行的插件是否针对您的运行时提供的兼容版本协议编译是您的责任。

如果您的协议需要引入破坏性更改,我们建议在协议本身中包含一个 version() -> u32 导出函数,您可以在调用其他函数之前调用它。

至于什么是破坏性更改,我们提供以下指导方针

  • 所有插件导出始终是可选的。因此,始终可以添加新导出,而不会破坏现有的插件,除非您的运行时执行显式检查,强制导出的存在。
  • 添加新的导入始终是安全的,因为它们将被现有插件忽略。
  • struct 添加字段始终是安全的,除非您的运行时强制要求插件中的参数或返回值存在这些字段。
  • 始终可以安全地添加新类型。
  • 其他任何内容都应被视为破坏性更改。

请注意,由于上述指导方针,您在第一轮迭代中永远不需要定义版本控制函数。因为插件导出是可选的,版本控制函数的缺失可以简单地解释为插件版本为 1。

获取帮助

请参阅 COMMUNITY.md 了解如何联系我们。

贡献

请遵循我们的贡献指南,了解如何最好地为此项目做出贡献。

行为准则

请参阅CODE_OF_CONDUCT.md

许可证

本项目以MIT许可证和Apache许可证(版本2.0)的条款进行分发。

请参阅LICENSE-APACHELICENSE-MIT

依赖项

~2–12MB
~135K SLoC