🦀

Rust での非 Web 向け Wasm モジュール作成

2022/12/21に公開

TL;DR;

  • wasm32-unknown-unknown もしくは wasm32-wasi 向けにビルドすることで、Wasm モジュールが作成できます。
  • crate-typecdylib を追加することを忘れずに。
  • 数値以外の値を扱う場合は、wit などで定義したインタフェース定義からグルーコードを出力した方が簡単です。

WebAssembly とは

WebAssembly(以下、Wasm)とは、実行可能なコードのバイナリ表現の一種で、仕様は W3C のコミュニティグループで議論され、同じく W3C のワーキンググループで標準化されています。

Web の標準化団体で仕様が策定されていますが、以下のような点からサードパーティーのエコシステムを利用する用途で Web 以外での利用も積極的に行われています。

  • オープンスタンダードである。
  • コンパイルターゲットであって、プログラミング言語やツールチェーンから独立である。
  • 信頼できないコードを実行するため、処理系のほとんどにサンドボックス機能が組み込まれている。

Wasm モジュール

Wasm ファイルはソフトウェアモジュールを表現したもの、として定義されています。つまり Wasm ファイルを実行する視点からは、次の 2 つの組として見なすことができます。

  • 外部から利用可能な関数の集合
  • 外部から与えられる関数の集合

Wasm では前者を exports、後者を imports と呼びます。Wasm モジュールを作成する側は、何を exports とし、何を imports とするのかを決めることになります。

Rust と WebAssembly

Rust は Wasm への対応が進んでいる言語のひとつです。標準的に用意されている機能、ツールのみを利用して Wasm ファイルを作成できます。

Rust は Wasm モジュールの定義を Foreign Function Interface (FFI) と枠組みで行います。extern キーワードがついた関数のうち、実装があるものを exports とし、インターフェースのみ定義されたものを imports とします。例えば次のコードの場合、addadd_e がエキスポートされ、rand がインポートされることになっています。

extern "C" {
    fn rand() -> i32;
}

pub extern "C" fn add(left: i32, right: i32) -> i32 {
    crate::add(left, right)
}

pub extern "C" fn add_e(value: i32) -> i32 {
    unsafe { crate::add(value, rand()) }
}

ビルド

上記のコードから Wasm ファイルを出力するには、次の準備が必要です:

  1. ターゲットアーキテクチャに wasm32-unknown-unknown、もしくは wasm32-wasi を追加
  2. クレートの Cargo.toml を変更し、crate-typecdylib を追加

ビルドの準備その 1:ターゲットアーキテクチャの追加

ターゲットアーキテクチャの追加は rustup コマンドで行えます。次の例では wasm32-unknown-unknown を追加しています。

% rustup target add wasm32-unknown-unknown

実行環境が WASI に対応していることが明らかな場合以外は、 wasm32-unknown-unknown を利用した方が良いでしょう。

ビルドの準備その 2:crate-type の追加

Cargo.toml に次のように記述することで、Wasm ファイルを出力できるようになります。

[lib]
crate_type = ["cdylib", "rlib"]

詳しくは crate_type フィールド の説明を参照ください。

ビルドコマンド

準備が終了したら、次のコマンドでビルドすることで Wasm ファイルが target/wasm32-unknown-unknown/debug フォルダ内に作成されます。

% cargo build --target wasm32-unknown-unknown

最適化処理を行ったリリースビルドを作成する場合は、-r オプションをつけます。この場合は target/wasm32-unknown-unknown/release フォルダに Wasm ファイルが出力されます。

% cargo build --target wasm32-unknown-unknown -r

エキスポートする関数の名前

以下のコードをビルドした場合、関数名は難読化されてしまいます。どのような名前になるかはビルドするまでわからないため、利用する側から見ればとても不都合です。

pub extern "C" fn add(left: i32, right: i32) -> i32 {
    crate::add(left, right)
}

以下のように no_mangle 属性をつけることで、難読化を防げます。

#[no_mangle]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
    crate::add(left, right)
}

また次のように export_name 属性をつけて、エクスポート時の名前を指定することも可能です。

#[export_name = "my-add"]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
    crate::add(left, right)
}

文字列やユーザー定義型を利用するためには

数値を引数として受け取り、数値を返すような関数だけを扱う場合は、上記の方法で比較的容易に対応ができます。

しかしプログラムの大部分は、より複雑な型を扱っています。文字列を引数として受け取るような関数や、自作の構造体を返すような関数を定義することは普通に行われます。

これら複雑な型を扱う関数をエキスポートする場合、関数の定義に加えて以下も定義することになります。

  • 安全にデータ交換を行うためのデータ型の定義
  • データ交換用の型と、プログラムで扱うデータ型との変換

repr 属性でデータ表現を指定したり、CString を使用したり、頑張って変換コードを書くことで対応はできます。しかし開発体験は良いとは言えません。

wasm-bindgen が開発された理由の一つも、この面倒な部分を簡単するためです。別の言い方をすれば、手でゴリゴリ書くより wasm-bindgen のようなコード生成器を利用する方が筋が良いとも言えるでしょう。

Web ブラウザ外での利用を考えた場合、インタフェース定義言語(IDL)で Wasm モジュールのインタフェースを定義し、その定義から生成されたコードを利用して開発を行うのが基本になるように思います。 Wasm での利用を想定した IDL の代表的なものには BYTECODE Alliance が中心となって策定している wit 形式と、wasmer が利用している wai があります。それぞれについては別の記事で記述する予定です。

まとめ

Rust は組み込みの機能を組み合わせることで、Wasm ファイルを作成できます。一方で、実用的なプログラムを Wasm に向けてビルドするには、ツールの力を借りた方が良いでしょう。

個人的には Wasm の仕様策定に影響力の大きいチームが開発している wit-bindgen の方が有力なようにも思ますが、まだわかりません。wai と wit の将来的な統合などもあるかもしれませんし(個人の願望です)、動向も注視する必要がありそうです。

レファレンス

Discussion