この記事について

先日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.tomlcrate-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と組み合わせたブラウザでの利用についても試してみたいです。