1. 背景
ここ最近、WebAssemblyにハマっています。C++とTypeScriptで作成していて、手順は
- C++でコーディング
- Emscriptenでビルドして.jsファイルと.wasmファイルを生成
- TypeScriptで.jsのラッパーを作成
といった感じなのですが、これがちょっと面倒です。関数の名前・戻り値・引数を決めたら、それをC++とTypeScriptの両方で実装しないといけません。
さらにコールバック関数を実装しようとしたらもっと面倒で、登録する関数の型情報を別途定義する必要があります。
そこで、関数の仕様を決めたらそれに合わせてソースコードを自動で生成するツールを作成しました。
※2025/8/15追記
WASI Preview 2では、WIT形式で関数の仕様を記述したらソースコードを自動生成してくれるwit-bindgenというツールをBytecode Allianceが作っているみたいです(不勉強で知りませんでした)。
今回紹介するツールは、wit-bindgenと似た思想で作っているので、将来Preview 2をベースにして開発していくなら不要になりそうです。。。
2. 作ったもの
GitHubにアップロードしています。
Aggallet
3. 概要
このツールでは、以下のようにYAMLで定義した関数の仕様を読み込み、TypeScriptとC++のソースコードを生成します。
functions:
- name: update # 定期更新
return: void
- name: test_bool_int_string # bool, int, stringのテスト
args:
- name: arg_1_bool # 第1引数(真偽値)
type: bool
- name: arg_2_int # 第2引数(数値)
type: int
- name: arg_3_string # 第3引数(文字列)
type: string
return: string # 文字列を返す
生成されたコードは、以下の処理を実行します。さらに、制限はありますが、YAMLで書かれたコメントはソースコードへ反映させることができます。
- Emscriptenで生成された.jsに対してWebAssembly公開関数を呼び出すためのラッパー (TypeScript)
- WebAssembly公開関数の型定義 (TypeScript)
- WebAssembly公開インターフェースとビジネスロジックの橋渡し (C++)
- コールバック関数の登録・解除・呼び出し (TypeScript, C++)
- コールバック関数の型定義 (TypeScript)
4. 使用方法
Pythonで実行しており、以下のライブラリを使用しています。
ライブラリ | 概要 |
---|---|
Jinja2 | テキストファイル生成のテンプレートエンジン |
ruamel.yaml | YAMLファイル関連処理 PyYAMLと異なり、コメント読み込み可能 |
4.1 実行環境
- OS : Windows 11
- Python : 3.9.13
- Pythonライブラリ
- Jinja2 : 3.1.6
- ruamel.yaml : 0.18.14
4.2 実行手順
GitHubのフォルダ構成に沿って説明します。
- まずは
src/setting.yaml
を必要に応じて変更します。このファイルではWebAssemblyモジュールの名前や生成コードに記述するクラス名・変数名・ビジネスロジック呼び出し時の接頭語など、色々と定義しています。とりあえず試すだけであれば変更しなくても問題ありません -
src/type/catalog.yaml
に使用する型を記載します。デフォルトでは以下の型のみサポートしていますが、typeフォルダの中のYAMLファイルを変更するだけで追加・削除は容易に対応可能です:- void
- bool
- int
- float
- string
- function
-
src/type
の中の他のファイルで、catalog.yaml
で定義した型の対象言語における名称を定義します。例えばts.yamlではTypeScriptの型名を定義しており、"int"を"number"に対応付けています - 関数仕様を作成するためのフォルダを準備し、そこにapi.yamlを作成します
-
api.yaml
に以下のように関数の仕様を記述します:functions: - name: update # 定期更新 return: void - name: test_bool_int_string # bool, int, stringのテスト args: - name: arg_1_bool # 第1引数(真偽値) type: bool - name: arg_2_int # 第2引数(数値) type: int - name: arg_3_string # 第3引数(文字列) type: string return: string # 文字列を返す
- 各関数に対しては以下のルールに基づいて記述する必要があります:
フィールド ルール name 必須項目。関数の名前です。
この行に書かれたコメントはソースコードへ反映されますreturn 必須項目。関数の戻り値の型です。
戻り値が無い場合はvoidと記載してください。
この行に書かれたコメントはソースコードへ反映されますargs 省略可能。関数の引数です。
名前と型の組をリスト形式で記述してください。
nameの行に書かれたコメントはソースコードへ反映されます - コールバックを使用する場合は、
callback.yaml
を作成し、コールバック関数の仕様を記述します。記述ルールはapi.yamlと同じですfunctions: - name: send_parameters # パラメータ送信 args: - name: arg1 # boolデータ type: bool - name: arg2 # intデータ type: int - name: arg3 # stringデータ type: string return: void
-
src/aggallet.py
をオプション付きで実行しますオプション 概要 -o, --output 生成コードの出力先フォルダ -s, --spec api.yaml, callback.yamlを置いているフォルダ -c コールバックを使用する場合のみ必要です - 引数で渡したフォルダに生成コードが出力されています
4.3 生成コード
実行手順にあるサンプル仕様だと、以下のソースコードが生成されます。
addCallbackとremoveCallbackはコールバック有効化時に追加される関数で、YAMLに定義しなくても出力されます。
4.3.1 Emscriptenで生成された.jsからWebAssembly公開関数を呼び出すためのラッパー
/*
* This source code is automatically generated.
* To make changes, please update the YAML file and run the code generation tool.
*/
import WASM from 'wasm/WASM.js';
import { CallbackMap, FUNCTION_ID } from 'types/WasmCallbackId';
export class WasmWrapper {
private mInstance: Awaited<ReturnType<typeof WASM>> | undefined = undefined;
/*
* @brief load WebAssembly
* @return promise for loading WebAssembly
*/
public load(): Promise<void> {
if (this.mInstance) {
return Promise.resolve();
}
return WASM({
locateFile: (fileName: string) => {
if (fileName.endsWith('.wasm')) {
return `/wasm/${fileName}`;
}
return fileName;
}
}).then((result) => {
this.mInstance = result;
});
}
/*
* @brief add callback function
* @param id callback ID
* @param func callback function
* @return true: success, false: fail
*/
public addCallback<ID extends keyof CallbackMap>(id: ID, func: CallbackMap[ID]): boolean {
let result: boolean = false;
if (this.mInstance) {
result = this.mInstance.addCallback(id, func);
}
return result;
}
/*
* @brief remove callback function
* @param id callback ID
* @return true: success, false: fail
*/
public removeCallback(id: typeof FUNCTION_ID[keyof typeof FUNCTION_ID]): boolean {
let result: boolean = false;
if (this.mInstance) {
result = this.mInstance.removeCallback(id);
}
return result;
}
/*
* @brief 定期更新
*/
public update(
): void {
if (this.mInstance) {
this.mInstance.update(
);
}
}
/*
* @brief bool, int, stringのテスト
* @param arg_1_bool 第1引数(真偽値)
* @param arg_2_int 第2引数(数値)
* @param arg_3_string 第3引数(文字列)
* @return 文字列を返す
*/
public test_bool_int_string(
arg_1_bool: boolean, arg_2_int: number, arg_3_string: string): string {
let result: string = "";
if (this.mInstance) {
result = this.mInstance.test_bool_int_string(
arg_1_bool, arg_2_int, arg_3_string);
}
return result;
}
}
4.3.2 WebAssembly公開関数の型定義
/*
* This source code is automatically generated.
* To make changes, please update the YAML file and run the code generation tool.
*/
type WASMOptions = {
locateFile?: (fileName: string) => string;
};
declare module 'wasm/WASM.js' {
const WASM: (options: WASMOptions) => Promise<{
addCallback: (id: number, func: Function) => boolean;
removeCallback: (id: number) => boolean;
update: () => void;
test_bool_int_string: (arg_1_bool: boolean, arg_2_int: number, arg_3_string: string) => string;
}>;
export default WASM;
}
4.3.3 WebAssembly公開インターフェースとビジネスロジックの橋渡し
/*
* This source code is automatically generated.
* To make changes, please update the YAML file and run the code generation tool.
*/
#include <emscripten/bind.h>
#include <emscripten/emscripten.h>
#include "AppWrapper.h"
/*
* @brief add callback function
* @param id callback ID
* @param func callback function
* @return true: success, false: fail
*/
bool addCallback(uint16_t id, emscripten::val func) {
return AppWrapper::getInstance().addCallback(id, func);
}
/*
* @brief remove callback function
* @param id callback ID
* @return true: success, false: fail
*/
bool removeCallback(uint16_t id) {
return AppWrapper::getInstance().removeCallback(id);
}
/*
* @brief 定期更新
*/
void update(
) {
AppWrapper::getInstance().update(
);
}
/*
* @brief bool, int, stringのテスト
* @param arg_1_bool 第1引数(真偽値)
* @param arg_2_int 第2引数(数値)
* @param arg_3_string 第3引数(文字列)
* @return 文字列を返す
*/
std::string test_bool_int_string(
bool arg_1_bool, int arg_2_int, std::string arg_3_string) {
return AppWrapper::getInstance().test_bool_int_string(
arg_1_bool, arg_2_int, arg_3_string);
}
EMSCRIPTEN_BINDINGS(bind_module) {
emscripten::function("addCallback", &addCallback);
emscripten::function("removeCallback", &removeCallback);
emscripten::function("update", &update);
emscripten::function("test_bool_int_string", &test_bool_int_string);
}
4.3.4 コールバック関数の登録・解除・呼び出しに使用するIDの定義(TypeScript)及びコールバック関数の型定義
/*
* This source code is automatically generated.
* To make changes, please update the YAML file and run the code generation tool.
*/
export const FUNCTION_ID = {
/*
* @brief パラメータ送信
* @param arg1 boolデータ
* @param arg2 intデータ
* @param arg3 stringデータ
*/
SEND_PARAMETERS: 0x0001,
} as const;
export type CallbackMap = {
[FUNCTION_ID.SEND_PARAMETERS]: (arg1: boolean, arg2: number, arg3: string) => void;
};
4.3.5 コールバック関数の登録・解除・呼び出しに使用するIDの定義(C++)
C++ではIDのみ定義し、関数の型定義はしません。こちらで実装したCallbackUtilityクラスのように、単一の関数にIDと引数情報を与えて呼び出す仕組みを想定しています。ただ、もっと良い手段が他に無いか今後模索していく必要があると思います。
/*
* This source code is automatically generated.
* To make changes, please update the YAML file and run the code generation tool.
*/
#pragma once
enum class FUNCTION_ID : uint16_t {
/*
* @brief パラメータ送信
* @param arg1 bool value. boolデータ
* @param arg2 int value. intデータ
* @param arg3 std::string value. stringデータ
*/
SEND_PARAMETERS = 0x0001,
};
5. サンプル
GitHubのリポジトリに、Angular・Viteを使用したサンプルを用意しています。ビルド手順など載せていますので、試してみたい方はご覧ください。
6. 最後に
WebAssemblyのソースコードを自動生成するツールを作成しました。
作っている途中で(お盆休みに一体何をしているんだろう・・・)とモチベーションが下がりそうになりましたが、何とか形にできました。引数で与えるべき情報とプログラム自身が持つべき情報を十分に整理できていないので、ここを上手く整理して、プログラムを改変せずとも利用できる、使い勝手の良いツールを目指したいです。
また、WebAssembly側の言語は現在C++だけしかサポートしていないので、他の言語のコードも生成できるよう拡張していきたいです。