この記事について
先日WebAssemblyという概念を知りました。異なる言語間での実装というWebAssemblyの特性を実際に体験したいと思いAWS Lambda上のPythonで実行してみましたので、その過程について記載します。
WebAssemblyの作成
1. プロジェクトの作成
- 言語はRustを選択し、公式サイト記載の下記でインストールします。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- wasm_testという名称でRustのプロジェクトを作成してみます。
--lib
フラグを利用すると、他プログラムからの利用を想定したライブラリとして設計されるようです。
cargo new wasm_test --lib
- 元から用意されているadd関数を利用することにしますが、これに
no_mangle
アトリビュートを追加しておきます。
デフォルトのままコンパイルすると名前衝突を避けるために関数名等が固有の名前にエンコード(mangle)されるそうなのですが、これが行われると外部から定義時の関数名では呼び出せなくなります。
これを避けるため、no_mangle
をつけて、定義時の関数名のまま呼び出すことができるようにします。
#[no_mangle] pub extern "C" fn add(left: u32, right: u32) -> u32 { left + right }
2. WebAssemblyにコンパイル
cargo.toml
にcrate-type
としてcdylib
を設定します。
この設定によりRustのコードが共有ライブラリとしてビルドされ、Pythonから利用できるようになるようです。
[lib] crate-type = ["cdylib"]
- RustコードをWebAssemblyにコンパイルするために、次のコマンドでターゲットを追加します。
wasm32
が32ビットのWebAssemblyを、-unknown-unknown
が特定のOSやハードウェアに依存しないことを意味しており、これを指定してビルドすることによりウェブブラウザや他のWebAssembly対応環境で実行できるバイナリが生成されるようです。
rustup target add wasm32-unknown-unknown
- 最適化のための
--release
オプションと上記で追加した--target wasm32-unknown-unknown
オプションをつけてビルドします。
cargo build --target wasm32-unknown-unknown --release
- wasm_test.wasmファイルが下記ディレクトリに作成されていることを確認します。
./target/wasm32-unknown-unknown/release
SAMの作成
1. SAMの作成
- 上記で作成したWebAssemblyを実行するLamdaを作成します。Python 3.10を使用してのSAMを用意します。
sam init --name SamWasmTest --runtime python3.10
2. layerの準備
- layerディレクトリを作成し、wasmを配置します。
- Python自体にはWebAssemblyを直接実行する機能が組み込まれていないため、ランタイムを使用します。
今回はwasmtimeというものを利用します。wasmtimeのgithubで公開されているものから、lamdaの実行環境であるx86_64-linuxと記載あるものを選択しました。
. ├── README.md ├── __init__.py ├── events │ └── event.json ├── layer │ ├── wasm_test.wasm │ └── wasmtime-v15.0.0-x86_64-linux ├── sam_wasm_test │ ├── __init__.py │ ├── app.py │ └── requirements.txt ├── samconfig.toml └── template.yaml
3. template.yamlの準備
template.yaml
に関数とlayerを定義します。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Resources: SamTestFunction: Type: AWS::Serverless::Function Properties: FunctionName: WasmTestFunction CodeUri: sam_wasm_test/ Handler: app.lambda_handler Runtime: python3.10 MemorySize: 1024 Timeout: 30 Layers: - !Ref SamTestLayer Events: SamTestApiEvent: Type: Api Properties: Path: /sam_wasm_test Method: get SamTestLayer: Type: AWS::Serverless::LayerVersion Properties: ContentUri: ./layer CompatibleRuntimes: - python3.10
4. requirements.txtの編集
requirements.txt
にwasmtimeを追加します。
wasmtime
5. 関数の作成
app.py
に関数を記述します。WASMのロード方法はwasmtimeのGitHubのLanguage Supportを参照しました。- また、layer内のライブラリは/opt配下に配置されるのでインポート検索パスに/optディレクトリを追加する必要がありました。
import json import wasmtime import sys # インポート検索パスに/optディレクトリを追加する sys.path.append("/opt") # WASM モジュールのパス module_path = '/opt/wasm_test.wasm' def lambda_handler(event, context): valueA = int(event.get("valueA")) valueB = int(event.get("valueB")) # WASMモジュールをロードして関数を実行 store = wasmtime.Store() module = wasmtime.Module.from_file(store.engine, module_path) instance = wasmtime.Instance(store, module, []) # no_mangle修飾子をつけているので定義時の名称で関数を取得できる add = instance.exports(store)["add"] output_value = add(store, valueA, valueB) response = { "statusCode": 200, "body": json.dumps({ "output_value": output_value }), "headers": { "Content-Type": "application/json" } } return response
- デプロイを実行します。
sam build --no-cached && sam deploy
6. 実行結果の確認
- Lambdaを実行し、結果を確認します。
aws lambda invoke \ --function-name WasmTestFunction \ --payload $(echo '{"valueA": "1", "valueB": "2"}' | base64) \ output.json && cat output.json; echo
- 引数の合計値が取得できていることを確認できました。
{ "StatusCode": 200, "ExecutedVersion": "$LATEST" } {"statusCode": 200, "body": "{\"output_value\": 3}", "headers": {"Content-Type": "application/json"}}
おわりに
PythonからRustで書かれたプログラムを実行することができました。WebAssemblyを使えば、通常の開発はPythonで進め、高速な実行が必要な処理についてはRustで実装するといった、言語ごとの強みを活かした開発も進めやすくなるのかもしれません。複雑な実装をするとなるとハードルも高いと感じましたが、WebAssemblyが様々な環境で利用できることは面白く感じました。元々ブラウザ上での実行を目的に開発されたとのことなので、今後はJavaScriptと組み合わせたブラウザでの利用についても試してみたいです。