DelphiにWasmランタイムを組み込む
WebAssemblyのランタイムをDelphiアプリに組み込んで遊ぶ
Delpi製アプリにWasmランタイムを組み込んで遊べるようにしようという記事です。
日付 | 履歴 |
---|---|
2021/8/1 | Githubリポジトリ更新 |
動機
アプリケーション組み込み用のスクリプトを設計するにあたり、バイトコード実行は自前じゃなくて賢い方々が開発したものを使いたい。Wasmがそろそろ使えるんじゃないかと思って検索したらC向けAPIもあるし良さそう。
とりあえずWindows向けとします。
C-APIと各種ランタイム
wasm.h
がC向けAPIのようなのでまずはこれを移植します。
実際には、各種ランタイムはwasm.hのすべてのAPIを実装していなくてそれぞれ独自のAPIを追加定義しているようですが、まだよくわからない。
軽く調べた感じでは、wasmer
よりもwasmtime
の方が実装済APIが多いようなのでこちらをメインにします。wasmtime
はビルド済dllを配布しているのでこちらのほうが取っつきやすい。
移植
色々苦労しましたが、github
にリポジトリを作りましたのでそちらを見てください。WasmApiForDelphi
実験するためにはwatも扱える方が良かったので、wasmtime
のAPIも移植してwat2wasmを使えるようにしています。
own
な戻り値(ユーザーコード側で寿命管理が必要)のために前回の記事のスマートポインタも使っています。https://zenn.dev/zendenmushi/articles/4e515dd80fdea3
前回のオチにあるように、ジェネリクスレコードを戻り値にすると内部エラーにより型推論できなくなってしまうため、own
で受け取る必要がある型をジェネリクスを使わずに全て展開しています。
今後、Delphiのバージョンアップで内部エラーが修正されたり、純正のスマートポインタができたり、その他ではNull許容型などがサポートされた場合は互換性の無い修正を行う予定です。(そのため、wasm.pas
は半自動生成しています)
テスト
リポジトリ上のプロジェクトは、wasm.h
向けのexampleをいくつか移植したものをテスト代わりに実行できるようにしたものです。未実装APIが多くてエラーが出るものが多く、くじけたのであまり多くは移植できませんでした。
よって、移植したwasm.pas
およびwasmtime.pas
はほとんどテストできていません。
wasm.pasの使い方
そもそもwasm.h
の使い方が完全にはわかっていないので、とりあえず小さなサンプルコードを張り付けておきます。
unit WasmSample_Multi;
interface
uses
System.SysUtils
, Wasm
{$ifndef USE_WASMER}
, Wasmtime
{$else}
, Wasmer
{$ifend}
;
function MultiSample() : Boolean;
implementation
// A function to be called from Wasm code.
function callback(const args : PWasmValVec; results : PWasmValVec) : PWasmTrap; cdecl;
begin
writeln('Calling back...');
write('> ');
writeln( Format('> %u %u %u %u', [
args.Items[0].i32, args.Items[1].i64,
args.Items[2].i64, args.Items[3].i32]));
writeln('');
TWasm.val_copy(results.Items.Pointers[0], args.Items.Pointers[3]);
TWasm.val_copy(results.Items.Pointers[1], args.Items.Pointers[1]);
TWasm.val_copy(results.Items.Pointers[2], args.Items.Pointers[2]);
TWasm.val_copy(results.Items.Pointers[3], args.Items.Pointers[0]);
result := nil;
end;
// A function closure.
function closure_callback(env : Pointer; const args : PWasmValVec; results : PWasmValVec) : PWasmTrap; cdecl;
begin
var i := PInteger(env)^;
writeln('Calling back closure...');
writeln('>'+IntToStr(i));
var d := Default(TWasmVal);
d.kind := WASM_I32;
d.i32 := i;
results.Items[0] := d;
result := nil;
end;
function MultiSample() : Boolean;
begin
// Initialize.
writeln('Initializing...');
var engine := TWasmEngine.New();
var store := TWasmStore.New(+engine);
// Load binary.
writeln('Loading binary...');
var binary := TWasmByteVec.NewEmpty;
{$ifdef USE_WASMFILE}
binary.Unwrap.LoadFromFile('multi.wasm');
{$else}
var wat : AnsiString :=
'(module'+
' (func $f (import "" "f") (param i32 i64 i64 i32) (result i32 i64 i64 i32))'+
''+
' (func $g (export "g") (param i32 i64 i64 i32) (result i32 i64 i64 i32)'+
' (call $f (local.get 0) (local.get 2) (local.get 1) (local.get 3))'+
' )'+
')';
binary.Unwrap.Wat2Wasm(wat);
{$ifend}
// Compile.
writeln('Compiling module...');
var module := TWasmModule.New(+store, +binary);
if module.IsNone then
begin
writeln('> Error compiling module!');
exit(false)
end;
// Create external print functions.
writeln('Creating callback...');
var callback_func := TWasmFunc.New(+store, +TWasmFunctype.New([WASM_I32, WASM_I64, WASM_I64, WASM_I32], [WASM_I32, WASM_I64, WASM_I64, WASM_I32]), callback);
// Instantiate.
writeln('Instantiating module...');
var imports := TWasmExternVec.Create([ (+callback_func).AsExtern ]);
var instance := TWasmInstance.New(+store, +module, @imports, nil);
if instance.IsNone then
begin
writeln('> Error instantiating module!');
exit(false);
end;
// TWasm.func_delete(callback_func);
// Extract export.
writeln('Extracting export...');
var export_s := (+instance).GetExports;
if export_s.Unwrap.size = 0 then
begin
writeln('> Error accessing exports!');
exit(false);
end;
var run_func := export_s.Unwrap.Items[0].AsFunc;
if run_func = nil then
begin
writeln('> Error accessing export!');
exit(false);
end;
// Call.
writeln('Calling export...');
var vals := [
WASM_I32_VAL(1), WASM_I64_VAL(2), WASM_I64_VAL(3), WASM_I32_VAL(4)
];
var res := [
WASM_INIT_VAL, WASM_INIT_VAL, WASM_INIT_VAL, WASM_INIT_VAL
];
var args := TWasmValVec.Create(vals);
var results := TWasmValVec.Create(res);
if run_func.Call( @args, @results).IsError then
begin
writeln('> Error calling function!');
exit(false);
end;
// Print result.
writeln('Printing result...');
writeln( Format('> %u %u %u %u', [
res[0].i32, res[1].i64, res[2].i64, res[3].i32]));
assert(res[0].i32 = 4);
assert(res[1].i64 = 3);
assert(res[2].i64 = 2);
assert(res[3].i32 = 1);
// Shut down.
writeln('Shutting down...');
// All done.
writeln('Done.');
result := true;
end;
end.
Discussion