py2wasm+WasmerでPython製WebAssemblyを動かす
概要
py2wasmによって、PythonコードをWebAssemblyへコンパイルできるようになりました。
本記事では、PythonコードをWebAssembly(wasm)モジュールに変換し、Wasmerを用いて実行する方法を解説します。
お断り
サンプルコードはこちら
私の本職は組み込みエンジニア。Pythonは嗜む程度、WebAssemblyどころかWebアプリも初心者です。
またQiitaは初投稿です。
至らぬ点があればコメントいただけると助かります。
記事の半分くらいはGoogle Geminiが書きました。
py2wasmのインストール
pip install py2wasm
Pythonコードの実装
サンプルとして、コマンド実行時引数、標準入力、ファイル入力、ファイル出力を扱うようにしました。
このあたりができると、Webアプリのモジュールとして扱う際にデータをやり取りできるようになります。
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で実行します。
コマンド実行時引数、標準入力、ファイル入力の順に入力値を出力しています。
$ 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です。
$ py2wasm pywasm/main.py -o static/main.wasm
wasmerで実行
まずは下記を参考にwasmerをinstallします。
https://docs.wasmer.io/install
$ curl https://get.wasmer.io -sSfL | sh
wasmerでmain.wasmを実行してみます。
$ 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 を利用し、Webアプリからwasmを実行してみます。
Typescriptのサンプルを下記に示します。
コード全文はGitのサンプルコードをご覧ください。
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などでファイルを取得しておく必要があります。
順に説明します。
async function runModule(module: WebAssembly.Module) {
// 入力ファイルの準備 1 ファイルをfetchで取得
const fetchresult = await fetchAsync({
url: "./static/input.txt",
options: {}
})
まずwasmから読み込むファイルのデータを事前にfetchで取得しておきます。
// 入力ファイルの準備 2 wasmer DirectoryにWrite
const dir = new Directory();
await dir.writeFile("input.txt", fetchresult);
取得したファイルデータをwasmに認識させるため、Directoryクラスのインスタンスを生成し、input.txtというファイル名でファイルを書き込みます。
// wasm実行オプション
const option: RunOptions = {args: ["arg input"], mount: {"/static": dir}};
// wasm実行
const instance = await runWasix(module, option);
入力データが揃ったので、wasmを実行します。
実行時オプションをRunOption型で定義し、runWasixにwasm moduleとともに渡します。
argsは実行時引数、mountはマウントするディレクトリ名とディレクトリ内容を表すDirectoryクラスインスタンスの組を指定します。
実行すると、実行中のWASIXプログラムにアクセスするためのInstanceが戻されます。
このinstanceから、標準出力やファイル出力、実行結果などにアクセスできます。
// 実行中の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()
で値を書き込みます。
// 実行完了まで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を実行する準備ができました。
サーバを立ち上げ、ブラウザからアクセスしてみましょう。
$ 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
ブラウザの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