Rustでプラグインを作りたいと思うことはないだろうか?
例えば案件ごとに特殊対応をしたいけど、毎回デプロイしたくないとかで動的にできるプラグインがほしいと思うことがある。
Rustでは動的にあれこれする手段は少ない。
- Rustにはリフレクションなどの
動的な言語機能が少ない - StableなABIがない
WasmはRustのStableなABIともいえる。
それはRustからWasmへのコンパイルはtier2でサポートされていて安定していることと、Wasm自体のポータビリティからいうことができる。
Wasmを用いればRustから別のRustのコードを安定的に呼べる。
RustのコードをWasm経由で呼ぶ手法は主に三つある
機能 | fp-bindgen |
wasm-bindgen |
wit-bindgen |
---|---|---|---|
ホスト環境 | Rust (Wasmer), TypeScript* | JS/TS | Rust/Python (Wasmtime), JS/TS* |
ゲスト言語 | Rust* | Rust | Rust, C* |
プロトコル(スキーマ) | Rust (using macros) | N/A | .wit |
シリアライズ | MessagePack | JSON | Custom |
既存のRustの型を使えるか | ✅ | ❌ | ❌ |
上のうち安定的に動き、かつRust自体をHostとしているのは現状fp-bindgenだけである。
このためfp-bindgenでプラグインを呼び出す方法を紹介する。
公式のexampleを見ればいいが、何がどう必要かわかりにくかったので最小限にして解説する。
Wasmerで動かすことを想定する。
プロトコルの設定
fp-bindgenはまずプロトコルを設定する。
共有するべき型と関数を設定する
#[derive(Serializable)]
pub struct Data {
pub name: String,
pub text: String,
}
//pluginが呼ぶ関数
fp_import! {}
//pluginを呼ぶ関数
fp_export! {
//boolでは返せなかったのでu32
fn data_check(data : Data) -> u32;
}
それからプロトコルに従ってコードを生成する。
static PLUGIN_DEPENDENCIES: Lazy<BTreeMap<&str, CargoDependency>> =
Lazy::new(|| BTreeMap::from([(
//生成プラグインで必須なcrateを設定
"regex",
CargoDependency {
version: Some("1.6.0"),
..CargoDependency::default()
}),
(
//このfp-bindgen-supportはほぼ必須
"fp-bindgen-support",
CargoDependency {
version: Some("2.0.0"),
features: BTreeSet::from(["async", "guest"]),
..CargoDependency::default()
},
),
]));
fn main() {
for (bindings_type, path) in [
// host側へのコード生成
(BindingsType::RustWasmerRuntime, "../xx-runtime/src/gen"),
// plugin側へのコード生成
(
BindingsType::RustPlugin(RustPluginConfig {
name: "plugin-bindings",
authors: r#"["aobat"]"#,
version: "0.1.0",
dependencies: PLUGIN_DEPENDENCIES.clone(),
}),
"../xx-bindings",
),
] {
let config = BindingConfig {
bindings_type,
path,
};
fp_bindgen!(config);
}
}
プラグインの作成
プラグインの作成は先ほど生成したbindingをもとに行う.
まず先ほど生成されたplugin側のbindingを依存に持つように設定する
#...省略
[dependencies]
plugin-bindings = { path = "../xx-bindings"}
次にpluginで公開する関数を記述する.
use plugin_bindings::{fp_export_impl, Data};
#[fp_export_impl(plugin_bindings)]
pub fn data_check(data: Data) -> u32{
(data.name.len() > 0 ) as u32
}
これをwasm32-unknown-unknownでbuildする。そのときcdylibでないと.wasmが出力されないことがあるので注意。
ホスト(runtime)の設定
Wasmerで動かすのでその設定をする
# 必要な依存関係
[dependencies]
wasmer = { version = "2.2", default-features = false, features = ["sys"] }
wasmer-engine-universal = {version = "2.1", features = ["compiler"]}
serde = {version = "1.0"}
fp-bindgen-support = { version = "1.0.0", features = ["host", "async"] }
[target.'cfg(any(target_arch = "arm", target_arch = "aarch64"))'.dependencies]
wasmer-compiler-cranelift = {version = "2.1"}
[target.'cfg(not(any(target_arch = "arm", target_arch = "aarch64")))'.dependencies]
wasmer-compiler-singlepass = {version = "2.1"}
プロトコルによって生成されたファイルにはRuntimeという構造体がある。
これはWasmerのModuleをbindgenでwrapしたものになる。
ここに関数が生えているのでwasmのバイナリを受け取り実行する。
fn main() {
//この例では直接バイナリを埋め込んでいるが、ファイルから読むなり、ネットワーク越しに送ったりしたバイナリからRuntimeを生成できる。
let runtime = Runtime::new(include_bytes!(
"../../target/wasm32-unknown-unknown/release/xx_plugin.wasm"
))
.unwrap();
let r = runtime.data_check(Data {
name: "xx".to_owned(),
text: "yy".to_owned(),
});
println!("{r:?}");//1 成功
let r = runtime.data_check(Data {
name: "".to_owned(),
text: "aaa".to_owned()
});
println!("{r:?}");//0 失敗
}
使ってみた感じ
ツール自体がくせがありそうな感じがあるが、使ってみるとなれる。
簡単に後からRustのコードを挿入できる。
ただ現在参照を渡せない…
本命になりそうなwit-bindgen(wasmの公式のプロジェクト)を待ってもいいかも?