py2wasm+WasmerでPython製WebAssemblyを動かす

# py2wasm+WasmerでPython製WebAssemblyを動かす

## 概要

[py2wasm](https://github.com/wasmerio/py2wasm)によって、PythonコードをWebAssemblyへコンパイルできるようになりました。

本記事では、PythonコードをWebAssembly（wasm）モジュールに変換し、Wasmerを用いて実行する方法を解説します。

## お断り

サンプルコードは[こちら](https://github.com/maruzmaruz/py2wasm-sample)

私の本職は組み込みエンジニア。Pythonは嗜む程度、WebAssemblyどころかWebアプリも初心者です。
またQiitaは初投稿です。
至らぬ点があればコメントいただけると助かります。

記事の半分くらいはGoogle Geminiが書きました。

## py2wasmのインストール

```bash
pip install py2wasm
```

## Pythonコードの実装

サンプルとして、コマンド実行時引数、標準入力、ファイル入力、ファイル出力を扱うようにしました。
このあたりができると、Webアプリのモジュールとして扱う際にデータをやり取りできるようになります。

```python
import sys
import json

def argv_sample():
    data = sys.argv[1]
    print('argv', data)

def stdin_sample():
    data = input()
    print('stdin', data)

def filein_sample():
    path = 'static/input.txt'
    with open(path, 'r') as f:
        data = f.read()
        print('filein', data)

def fileout_sample():
    data = [{'name':'hoge', 'value':'1'}, {'name':'piyo', 'value':'2'}]
    with open('static/output.json', '+w') as f:
        json.dump(data, f, ensure_ascii=False)

def main():
    argv_sample()

    stdin_sample()

    fileout_sample()

    filein_sample()

if __name__ == '__main__':
    main()
```

まずはそのままPythonで実行します。
コマンド実行時引数、標準入力、ファイル入力の順に入力値を出力しています。

```bash
$ python -m pywasm "hoge"
argv hoge
piyo #標準入力
stdin piyo
filein file input sample
$ cat static/output.json
[{"name": "hoge", "value": "1"}, {"name": "piyo", "value": "2"}]
```

## Pythonコードのwasmへのビルド

py2wasmを用いてwasmにビルドします。
static/main.wasm が生成されていればOKです。

```bash
$ py2wasm pywasm/main.py -o static/main.wasm
```

## wasmerで実行

まずは下記を参考にwasmerをinstallします。
https://docs.wasmer.io/install

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

wasmerでmain.wasmを実行してみます。

```bash
$ wasmer run static/main.wasm "input data" --mapdir /static::./static
argv input data
hoge #標準入力
stdin hoge
filein file input sample
$ cat static/output.json
[{"name": "hoge", "value": "1"}, {"name": "piyo", "value": "2"}]
```

ポイントは、ファイル入出力を行う場合、Wasmerからファイルにアクセスできるようにディレクトリをマッピングしてあげることです。
これによって、マッピングしたディレクトリ内のファイルにWasmerからアクセスできます。

## wasmer.jsでwasmを実行

[wasmer.js](https://github.com/wasmerio/wasmer-js) を利用し、Webアプリからwasmを実行してみます。
Typescriptのサンプルを下記に示します。
コード全文はGitのサンプルコードをご覧ください。

```typescript
import { init, runWasix, RunOptions, Directory } from "@wasmer/sdk";
import moduleUrl from "./static/main.wasm?url";

// 中略

async function runModule(module: WebAssembly.Module) {
    // 入力ファイルの準備 1 ファイルをfetchで取得
    const fetchresult = await fetchAsync({
        url: "./static/input.txt",
        options: {}
    })

    // 入力ファイルの準備 2 wasmer DirectoryにWrite
    const dir = new Directory();
    await dir.writeFile("input.txt", fetchresult);

    // wasm実行オプション 
    const option: RunOptions = {args: ["arg input"], mount: {"/static": dir}};

    // wasm実行
    const instance = await runWasix(module, option);
    
    // 実行中のwasmへの標準入力からの入力
    const encoder = new TextEncoder();
    const stdin = instance.stdin.getWriter();
    await stdin.write(encoder.encode("stdin input\n"));
    await stdin.close();

    // 実行完了までwait
    const result = await instance.wait();
    if (result.stderrBytes) {
        console.log(new TextDecoder().decode(new Uint8Array(result.stderrBytes)));
    }

    // 実行結果取得 1 標準出力
    const message = new TextDecoder().decode(new Uint8Array(result.stdoutBytes));
    console.log(message);

    // 実行結果取得 2 ファイル出力
    const bytes = await dir.readFile("/output.json");
    console.log(new TextDecoder().decode((bytes)))
    outputData = bytes;

    return result.ok ? message : null;;
}

async function main() {
    const module = await initialize();
    const message = await runModule(module);
}
main();
```

コマンドラインからwasmerで実行したときとやることは同じで、ファイルのマウント、コマンド実行時引数を指定する必要があります。
加えて、Typescriptでファイル入力を行う場合、fetchなどでファイルを取得しておく必要があります。

順に説明します。

```typescript
async function runModule(module: WebAssembly.Module) {
    // 入力ファイルの準備 1 ファイルをfetchで取得
    const fetchresult = await fetchAsync({
        url: "./static/input.txt",
        options: {}
    })
```
まずwasmから読み込むファイルのデータを事前にfetchで取得しておきます。

```typescript
    // 入力ファイルの準備 2 wasmer DirectoryにWrite
    const dir = new Directory();
    await dir.writeFile("input.txt", fetchresult);
```
取得したファイルデータをwasmに認識させるため、[Directoryクラス](https://wasmerio.github.io/wasmer-js/classes/Directory.html)のインスタンスを生成し、input.txtというファイル名でファイルを書き込みます。

```typescript
    // wasm実行オプション
    const option: RunOptions = {args: ["arg input"], mount: {"/static": dir}};

    // wasm実行
    const instance = await runWasix(module, option);
```
入力データが揃ったので、wasmを実行します。
実行時オプションを[RunOption](https://wasmerio.github.io/wasmer-js/types/RunOptions.html)型で定義し、[runWasix](https://wasmerio.github.io/wasmer-js/functions/runWasix.html)にwasm moduleとともに渡します。
argsは実行時引数、mountはマウントするディレクトリ名とディレクトリ内容を表すDirectoryクラスインスタンスの組を指定します。
実行すると、実行中のWASIXプログラムにアクセスするための[Instance](https://wasmerio.github.io/wasmer-js/classes/Instance.html)が戻されます。
このinstanceから、標準出力やファイル出力、実行結果などにアクセスできます。

```typescript
    // 実行中のwasmへの標準入力からの入力
    const encoder = new TextEncoder();
    const stdin = instance.stdin.getWriter();
    await stdin.write(encoder.encode("stdin input\n"));
    await stdin.close();
```

今回のプログラムでは実行中に標準入力からの入力を待つ箇所があるため、標準入力に値を入力する必要があります。
```instans.stdin.getWriter()``` で標準入力へのwriterを取得、 ```stdin.write()```で値を書き込みます。

```typescript
    // 実行完了までwait
    const result = await instance.wait();
    if (result.stderrBytes) {
        console.log(new TextDecoder().decode(new Uint8Array(result.stderrBytes)));
    }

    // 実行結果取得 1 標準出力
    const message = new TextDecoder().decode(new Uint8Array(result.stdoutBytes));
    console.log(message);

    // 実行結果取得 2 ファイル出力
    const bytes = await dir.readFile("/output.json");
    console.log(new TextDecoder().decode((bytes)))
    outputData = bytes;
```

今回のPythonコードは標準入力から値を受け取るとあとは終わりまで走り続けるので、```const result = await instance.wait();``` で完了まで待ちます。
このresultが実行結果に関する値として```stderrBytes```, ```stdoutBytes``` などを持っているため、printなどで標準出力に出力された値は```result```から取得できます。
```result.stdoutBytes``` をデコードしてコンソールに出します。

次にファイルに出力された値を確認します。
実行時にマウントしたDirectoryクラスを見ると、wasmが生成したファイルを取得できます。
```dir.writeFile()``` で書き込んだのと同様に、 ```dir.readFile()``` でファイルを読み込むことができます。
ファイルから読み込んだ値もコンソールに出します。

これでwasmer.jsを用いてwasmを実行する準備ができました。
サーバを立ち上げ、ブラウザからアクセスしてみましょう。

```bash
$ npm run dev

> py2wasm-sample@1.0.0 dev
> vite

The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.

  VITE v5.3.1  ready in 125 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824147/474ac719-4bb7-dc04-4ddb-aacb33c514e0.png)

ブラウザのConsoleを見ると、標準出力の値、output.jsonに出力した値をそれぞれ確認できます。

## おわり

py2wasmでPython製WebAssemblyを作成する方法、wasmにwasmerからアクセスする方法を説明しました。
外部ライブラリについては試しにopenpyxlをimportして見ましたが、エラーしてしまいました。
現状どんなPythonコードでもwasmにできる、というわけではありませんが、簡単なコードならwasmにできるため、wasmに興味はあるがRustなどはハードルが高い、という方でもチャレンジできるようになったと思います。
ご参考になれば幸いです。

## 参考

py2wasm
https://github.com/wasmerio/py2wasm
wasmer.js
https://wasmerio.github.io/wasmer-js/index.html