Wasmer で遊ぶ
1.0 が先日リリースされた Wasmer で遊んでみよう!のスクラップ
今日はこのドキュメントをやる。
Wasmer とは
Wasmer は WebAssembly のランタイムの一つ。この上で WebAssembly を実行することができる。
Docker 上でアプリケーションが動いていて、Docker をインストールした先なら本番環境とローカルが同じ環境で動かせるというのが今の時代は主流になっているが、それと似ている。
コンパイル後のターゲットを WebAssembly にすると WebAssembly ランタイムをインストールした OS ならどんな OS でもアプリケーションを動かせるようになる。
WebAssembly ランタイムは現時点でかなりの種類があり、代表的な例だと wasmtime や Lucet がある。
その他さまざまな WebAssembly ランタイムはこのページにまとまっている◎
Wasmer のインストール自体は
curl https://get.wasmer.io -sSfL | sh
で済ませられる◎
Hello, World は QuickJS と呼ばれるツールを使って行っているので、それを試してみる。
方法は2つありそう。
- qjs.wasm をダウンロードする。
- wapm install quickjs 経由。
qjs.wasm をダウンロードする。
こっちのほうが簡単だし、公式の手順に従っている。
Explore のページに飛び:
qjs.wasm をダウンロードする。
ダウンロードしたディレクトリで下記を実行する。
❯ wasmer qjs.wasm
QuickJS - Type "\h" for help
qjs >
wapm install quickjs 経由。
下記のようなディレクトリを最終的に作れるように目指す。
❯ ls --tree wasmer-example
wasmer-example
├── wapm.lock
└── wapm_packages
└── _
└── quickjs@0.0.3
├── build
│ └── qjs.wasm
├── README.md
└── wapm.toml
ここから、Hello, World するために WAPM (WebAssembly Package Manager) を経由して QuickJS というものをダウンロードする。
mkdir wasmer-example
して、
cd wasmer-example
したあとに、
wapm install quickjs
して QuickJS というものをインストールする。(グローバルに入れることに抵抗がないのであれば、wapm install -g quickjs
でもよさそうです)
インストールを実行するとこんな感じになる。
~ on ☁️ ap-northeast-1
❯ wapm install quickjs
[INFO] Installing _/quickjs@0.0.3
Package installed successfully to wapm_packages!
ディレクトリ上で qjs
を起動する。
❯ ./wapm_packages/.bin/qjs
QuickJS - Type "\h" for help
qjs >
起動できる。チュートリアルにしたがって下記を書いていくと、たしかに遊べる。
❯ ./wapm_packages/.bin/qjs
QuickJS - Type "\h" for help
qjs > [1, 2, 3, 4].map(x => x*x)
[1, 2, 3, 4].map(x => x*x)
[ 1, 4, 9, 16 ]
qjs >
Rust のプロジェクトをいつもどおり作って、
[package]
name = "wasmer-sandbox"
version = "0.1.0"
authors = []
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasmer = "1.0"
cargo build
したら。。。
error[E0432]: unresolved import `syn::export`
--> ~/.cargo/registry/src/github.com-1ecc6299db9ec823/enumset_derive-0.5.0/src/lib.rs:10:10
|
10 | use syn::export::Span;
| ^^^^^^ could not find `export` in `syn`
error: aborting due to previous error
Issue があがってた😇
wasmer の方にも Issue が
Issue に記載されていた patch を Cargo.toml に追記した(こんなオプションあったんだ)。
[patch.crates-io.enumset_derive]
git = "https://github.com/ocboogie/enumset"
branch = "span-fix"
build / run ともに動いた◎
1を加算する関数を用意してみるチュートリアルが載っていたのでやってみる。
まずコード全体は下記のようになる。
use wasmer::{imports, wat2wasm, Instance, Module, Store};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let wasm_bytes = wat2wasm(
br#"
(module
(type $add_one_t (func (param i32) (result i32)))
(func $add_one_f (type $add_one_t) (param $value i32) (result i32)
local.get $value
i32.const 1
i32.add)
(export "add_one" (func $add_one_f)))
"#,
)?;
let store = Store::default();
let module = Module::new(&store, wasm_bytes)?;
let import_object = imports! {};
let instance = Instance::new(&module, &import_object)?;
let add_one = instance
.exports
.get_function("add_one")?
.native::<i32, i32>()?;
let result = add_one.call(1)?;
println!("Results of `add_one`: {:?}", result);
assert_eq!(result, 2);
Ok(())
}
1つ1つ理解してみる。
wat2wasm
ここで WebASsembly を記述している。正確には「WebAssembly Text Format(in short: wat)」を読んでいる(WebAssembly の勉強会には何度か出ていたので何がどうなってるかはちょっとわかる)。
WebAssembly Text Format については下記。
wat については S 式によって形成されている。S 式は非常に歴史ある概念ではあるもののツリーをシンプルに表現しやすいなどのメリットがある。
wat そのものは module
に func
を追加しながらさまざまな定義をしていくスタイルになっている。ここを深堀りするとかなり長くなってしまい本筋から外れてしまうので、また後日記事にしてまとめようと思った。
大事なことは、add_one
という関数を定義しているのはここで、これからの実装ではこの中身を呼び出すのだということ。
let wasm_bytes = wat2wasm(
br#"
(module
(type $add_one_t (func (param i32) (result i32)))
(func $add_one_f (type $add_one_t) (param $value i32) (result i32)
local.get $value
i32.const 1
i32.add)
(export "add_one" (func $add_one_f)))
"#,
)?;
Store
Store
というのは、その WebAssembly 全体で使用する状態を保持しておくためのもの。大域環境みたいなイメージ?
let store = Store::default();
default
の実装を探してみる。
// We only implement default if we have assigned a default compiler and engine
#[cfg(all(feature = "default-compiler", feature = "default-engine"))]
impl Default for Store {
fn default() -> Self {
// We store them on a function that returns to make
// sure this function doesn't emit a compile error even if
// more than one compiler is enabled.
#[allow(unreachable_code)]
fn get_config() -> impl CompilerConfig + 'static {
cfg_if::cfg_if! {
if #[cfg(feature = "default-cranelift")] {
wasmer_compiler_cranelift::Cranelift::default()
} else if #[cfg(feature = "default-llvm")] {
wasmer_compiler_llvm::LLVM::default()
} else if #[cfg(feature = "default-singlepass")] {
wasmer_compiler_singlepass::Singlepass::default()
} else {
compile_error!("No default compiler chosen")
}
}
}
#[allow(unreachable_code, unused_mut)]
fn get_engine(mut config: impl CompilerConfig + 'static) -> impl Engine + Send + Sync {
cfg_if::cfg_if! {
if #[cfg(feature = "default-jit")] {
wasmer_engine_jit::JIT::new(config)
.engine()
} else if #[cfg(feature = "default-native")] {
wasmer_engine_native::Native::new(config)
.engine()
} else {
compile_error!("No default engine chosen")
}
}
}
let config = get_config();
let engine = get_engine(config);
let tunables = BaseTunables::for_target(engine.target());
Store {
engine: Arc::new(engine),
tunables: Arc::new(tunables),
}
}
}
いくつか設定やエンジンを切り替えられるようにできているらしい。Wasmer のチュートリアルでは、JIT エンジンとコンパイラバックエンドに Cranelift を使用する例が載っていた。
Module
WebAssembly のモジュール全体を表す。Store
を受け取ってその設定を読み、与えられた WebAssembly を読み込んで保持する。
let module = Module::new(&store, wasm_bytes)?;
実質この中でコンパイルをしている。ちょっと読み進めたところ、Artifact
というところにコンパイルした結果が保持されている感じがした。
pub struct Module {
store: Store,
artifact: Arc<dyn Artifact>,
}
Instance
ここで import するモジュールをついでに流し込む。
let import_object = imports! {};
let instance = Instance::new(&module, &import_object)?;
Instance
は exports
をもっていて、ここで使用可能な関数などが外部で利用できるようになっている(WebAssembly のドキュメントでいうところの Export Instances かな)。
pub struct Instance {
handle: Arc<Mutex<InstanceHandle>>,
module: Module,
/// The exports for an instance.
pub exports: Exports,
}
Exports
は中で Map をもっていて、ここにキーと値の形式でいろいろ登録されている。
pub struct Exports {
map: Arc<IndexMap<String, Extern>>,
}
Extern
で登録できるものは下記の4つ。
pub enum Extern {
/// A external [`Function`].
Function(Function),
/// A external [`Global`].
Global(Global),
/// A external [`Table`].
Table(Table),
/// A external [`Memory`].
Memory(Memory),
}
exports から関数のキーを使って関数を取り出し、それをさらに call
することで、先ほど wat で定義しておいた関数を呼び出せるようになっている。native
は Native ABI を使用していることを示す。
let add_one = instance
.exports
.get_function("add_one")?
.native::<i32, i32>()?;
let result = add_one.call(1)?;
WebAssembly はランタイムの構造が仕様書として定義されていて、困ったときはここを参照すると概観を掴むことができるみたい。
Wasmer は普通に dyn Trait を使用していて、そのおかげで意外とソースコードはシンプルに仕上がっていて読みやすかった。Rust を書く際のいいお手本になりそうだなと思った◎
wasmtime との比較が載っているページがあるみたい。wasmtime と比較した際、速度やバイナリの小ささなどが優位性になるとのこと。また、wasmtime は Cranelift しか対応していないが、Wasmer はたとえば LLVM をターゲットにすることもできる。