Closed12

Wasmer で遊ぶ

yukiyuki

1.0 が先日リリースされた Wasmer で遊んでみよう!のスクラップ

https://wasmer.io/

今日はこのドキュメントをやる。

https://docs.wasmer.io/ecosystem/wasmer/getting-started

Wasmer とは

Wasmer は WebAssembly のランタイムの一つ。この上で WebAssembly を実行することができる。

Docker 上でアプリケーションが動いていて、Docker をインストールした先なら本番環境とローカルが同じ環境で動かせるというのが今の時代は主流になっているが、それと似ている。

コンパイル後のターゲットを WebAssembly にすると WebAssembly ランタイムをインストールした OS ならどんな OS でもアプリケーションを動かせるようになる。

WebAssembly ランタイムは現時点でかなりの種類があり、代表的な例だと wasmtime や Lucet がある。

その他さまざまな WebAssembly ランタイムはこのページにまとまっている◎

https://github.com/appcypher/awesome-wasm-runtimes

yukiyuki

Wasmer のインストール自体は

curl https://get.wasmer.io -sSfL | sh

で済ませられる◎

Hello, World は QuickJS と呼ばれるツールを使って行っているので、それを試してみる。

https://wapm.io/package/quickjs

方法は2つありそう。

  1. qjs.wasm をダウンロードする。
  2. wapm install quickjs 経由。

qjs.wasm をダウンロードする。

こっちのほうが簡単だし、公式の手順に従っている。

Explore のページに飛び:

https://wapm.io/package/quickjs#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 >
yukiyuki

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 があがってた😇

https://github.com/Lymia/enumset/issues/17

wasmer の方にも Issue が

https://github.com/wasmerio/wasmer/issues/1986#issuecomment-755050725

yukiyuki

Issue に記載されていた patch を Cargo.toml に追記した(こんなオプションあったんだ)。

[patch.crates-io.enumset_derive]
git = "https://github.com/ocboogie/enumset"
branch = "span-fix" 

build / run ともに動いた◎

yukiyuki

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つ理解してみる。

yukiyuki

wat2wasm

ここで WebASsembly を記述している。正確には「WebAssembly Text Format(in short: wat)」を読んでいる(WebAssembly の勉強会には何度か出ていたので何がどうなってるかはちょっとわかる)。

WebAssembly Text Format については下記。
https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format

wat については S 式によって形成されている。S 式は非常に歴史ある概念ではあるもののツリーをシンプルに表現しやすいなどのメリットがある。

wat そのものは modulefunc を追加しながらさまざまな定義をしていくスタイルになっている。ここを深堀りするとかなり長くなってしまい本筋から外れてしまうので、また後日記事にしてまとめようと思った。

大事なことは、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)))
    "#,
    )?;
yukiyuki

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 を使用する例が載っていた。

yukiyuki

Module

WebAssembly のモジュール全体を表す。Store を受け取ってその設定を読み、与えられた WebAssembly を読み込んで保持する。

let module = Module::new(&store, wasm_bytes)?;

実質この中でコンパイルをしている。ちょっと読み進めたところ、Artifact というところにコンパイルした結果が保持されている感じがした。

pub struct Module {
    store: Store,
    artifact: Arc<dyn Artifact>,
}
yukiyuki

Instance

ここで import するモジュールをついでに流し込む。

    let import_object = imports! {};
    let instance = Instance::new(&module, &import_object)?;

Instanceexports をもっていて、ここで使用可能な関数などが外部で利用できるようになっている(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)?;
yukiyuki

Wasmer は普通に dyn Trait を使用していて、そのおかげで意外とソースコードはシンプルに仕上がっていて読みやすかった。Rust を書く際のいいお手本になりそうだなと思った◎

yukiyuki

wasmtime との比較が載っているページがあるみたい。wasmtime と比較した際、速度やバイナリの小ささなどが優位性になるとのこと。また、wasmtime は Cranelift しか対応していないが、Wasmer はたとえば LLVM をターゲットにすることもできる。

https://wasmer.io/wasmer-vs-wasmtime

このスクラップは2021/04/02にクローズされました