Typescript + WebAssembly(Rust)の環境設定

TypescriptプロジェクトにWebAssemblyを導入してみたのですが、何か所かハマったポイントがあったのでメモしておきます。OSはWindows10です。

#プロジェクト構成例
src/ts/main.ts を webpack + ts-loaderを用いて build/js/main.js にbuildし、index.htmlから読み込む構成を想定します。tsconfig.jsonwebpack.config.js については、自分で作って配置します。

[Project Home]
|_  build
|    |_  js
|         |_  main.js
|
|_  src
|    |_  ts
|         |_  main.ts
|
|_  index.html
|_  package.json
|_ tsconfig.json
|_  webpack.config.js

npm各ライブラリのインストール

webpackを、グローバルインストール

bash
npm i -g webpack

typescript ほか必要なライブラリをインストール

 npm i -D path ts-loader typescript

package.jsonのdevDependenciesは以下のようになります。

“`js:package.json
"devDependencies": {
"path": "^0.12.7",
"ts-loader": "^5.4.5",
"typescript": "^3.4.5",
},

<br /># Cargoのインストール

RustのパッケージマネージャであるCargoをインストールします。
> https://www.rust-lang.org/tools/install

から、RUSTUP-INIT.EXEをダウンロードし、管理者権限で実行します。 インストールが終わったら,以下のコマンドでPATHが通っていることを確認します。

```bash
rustup -V
cargo -V

wasm-packのインストール

今回はwasm-packを使ってWebAssemblyの環境を構築します。次のコマンドでインストールします。

 cargo install wasm-pack

crateの作成

WebAssemblyのプロジェクトのことをcrateと呼んだりするようです。(厳密な解釈ではないかもですが)
次のコマンドでsrcフォルダ内にcrateを作成します。今回はcrateをモジュールとして使いたいので、–libオプションをつけました。crate名は適当につけます。今回はwasmとしました。(ちなみにcrate名を「crate」とすると怒られました。)

cargo new src/wasm --lib 

現時点で、プロジェクト構成は以下のようになります。

[Project Home]
|_  build
|    |_  js
|         |_  main.js
|
|_  src
|    |_  ts
|    |    |_  main.ts
|    |
|    |_  wasm
|         |_  src
|         |    |_  lib.rs  *
|         |
|         |_  Cargo.toml  *
|   
|_  index.html
|_  package.json
|_ tsconfig.json
|_  webpack.config.js

Cargo.tomlとlib.rsの編集

cargo newで生成された2ファイルを以下のように書き換えます。Cargo.tomlは設定ファイルで、lib.rsには実際の処理を記述します。

“`js:Cargo.toml
[package]
name = "wasm"
version = "0.1.0"
authors = ["ユーザー名<メールアドレス>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

&lt;br /&gt;```rust:lib.rs
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &amp;str);
}

#[wasm_bindgen]
pub fn greet(name: &amp;str) {
    alert(&amp;format!(&quot;Hello, {}!&quot;, name));
}

今回の設定例は、MDNのチュートリアルをそのまま引用しています。
javascript側から関数alertをインポートし、関数greetでHello,の後に引数で指定した文字列をalertするという処理です。

Rustのコンパイル

wasm-pack buildコマンドでコンパイルします。
Cargo.tomlの存在するパスを指定する必要があるため、今回はsrc/wasmを指定します。

wasm-pack build src/wasm

コンパイルに成功すると、以下のようにpkgフォルダとtargetフォルダとCargo.lockというファイルが生成されます。

[Project Home]
|_  build
|    |_  js
|         |_  main.js
|
|_  src
|    |_  ts
|    |    |_  main.ts
|    |
|    |_  wasm
|         |_  pkg *
|         |     
|         |_  src
|         |    |_  lib.rs  
|         |   
|         |_  target *
|         |   
|         |_  Cargo.lock  *
|         |_  Cargo.toml  
|   
|_  index.html
|_  package.json
|_ tsconfig.json
|_  webpack.config.js

wasmの読み込み

pkgフォルダ内にwasm.jsというファイルが作成されているので、これをmain.tsからDynamic importして使用します。

s:main.ts
 const wasm = import(&#039;../wasm/pkg/wasm.js&#039;);
 wasm.then(mod =&gt; {
     mod.greet(&#039;WebAssembly&#039;);
 });

webpack.config.jsとtsconfig.jsonの設定

WebpackでTypescriptとWebAssemblyが正しくトランスパイルできるように設定します。

“`js:webpack.config.js
const path = require('path');

module.exports = {
// mode: 'production',
mode: 'development',
entry: './src/ts/main.ts',
output: {
path: path.resolve(__dirname, "build/js"),
publicPath: "./build/js/",
filename: 'main.js',
},
resolve: {
// Add <code>.ts</code> and <code>.tsx</code> as a resolvable extension.
extensions: ['.ts', '.tsx', '.js','.wasm']
},
module: {
rules: [
{
// all files with a <code>.ts</code> or <code>.tsx</code> extension will be handled by <code>ts-loader</code>
test: /.ts$/,
use: 'ts-loader',
},
{
test: /.wasm$/,
type: "webassembly/experimental"
}
]
}
}

&lt;br /&gt;```js:tsconfig.json
{
    &quot;compilerOptions&quot;: {
        &quot;sourceMap&quot;: false,
        &quot;target&quot;: &quot;es5&quot;,
        &quot;lib&quot;: [&quot;dom&quot;,&quot;es6&quot;,&quot;es2015.core&quot;],
        &quot;outDir&quot;: &quot;build/js&quot;,
        &quot;module&quot;: &quot;esNext&quot;
    },
    &quot;include&quot;: [
        &quot;src/ts/**.ts&quot;
    ],
    &quot;exclude&quot;: [
        &quot;node_modules&quot;,
        &quot;**/*.spec.ts&quot;
    ]
}

上記の設定をして、後はwebpackコマンドを実行するだけなのですが、ここで何度かハマりました。

1. Module not found: Error: Can’t resolve ‘./wasm_bg’ …

wasmモジュールが見つからないというエラーです。webpack.config.jsの resolve-> extensionsに’.wasm’ を追加することで解消しました。

“`js:webpack.config.js
resolve: {
// Add .ts and .tsx as a resolvable extension.
extensions: [‘.ts’, ‘.tsx’, ‘.js’, ‘.wasm’] // <– 重要!
},

&lt;br /&gt;## 2. WebAssembly module is included in initial chunk. This is not allowed, because WebAssembly download and compilation must happen asynchronous. ...

WebAssemblyは非同期読み込みする必要があるらしく、同期読み込みした場合に出るエラーです。`tsconfig.json`のコンパイラオプションのモジュールに`esNext`が設定されていない場合も発生します。


```js:tsconfig.json
{
    &quot;compilerOptions&quot;: {
        &quot;sourceMap&quot;: false,
        &quot;target&quot;: &quot;es5&quot;,
        &quot;lib&quot;: [&quot;dom&quot;,&quot;es6&quot;,&quot;es2015.core&quot;],
        &quot;outDir&quot;: &quot;build/ts&quot;,
        &quot;module&quot;: &quot;esNext&quot;  // &lt;-- 重要!
    },
}

3. Uncaught (in promise) TypeError: Failed to execute ‘compile’ on ‘WebAssembly’: HTTP status code is not ok

ローカルサーバでindex.htmlを開くと、ブラウザコンソールに表示されるエラーです。
これは、今回のプロジェクト構成例のようにindex.htmlmain.jsのパスが異なる場合に表示されるエラーのようです。

js:webpack.config.js
output: {
path: path.resolve(__dirname, &quot;build/js&quot;),
publicPath: &quot;./build/js/&quot;, // &lt;--重要!
filename: &#039;main.js&#039;,
},

これを解決するには、webpack.config.jsで、webpackの公開パスを、ビルドされたファイルの出力先パスとは別に、明示的に設定する必要があります。

結果

Hello,WebAssembly.JPG

ちゃんとalertが表示されました。めでたしめでたし。

後はTypescript側からlib.rsに重そうな処理をどんどん引っ越ししていきたいと思います。

DOM操作やレンダリングもWebAssemblyでできたら一番いいのになあ。

#参考にさせていただいたサイト
https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_wasm
https://qiita.com/mizchi/items/dc089c28e4d3afa78207
https://qiita.com/ito-hiroki/items/aaaf66e082f917baacc9
https://github.com/rustwasm/rust-webpack-template/issues/43#issuecomment-426597176

Leave a Reply

Your email address will not be published. Required fields are marked *