はじめに
TypeScriptでのプログラムを書くとき、高速に実行したい部分については、AssemblyScriptで書いてWebAssemblyにコンパイルしたものを呼び出すことができます。AssemblyScriptの文法はTypeScriptのサブセット的なものなので、それまでTypeScriptで書いていたコードを、AssemblyScriptのコードとして書き直す作業(ChatGPTにお願いして書き直してもらう作業😆)は、それほど大変なものではありません。高速化したい部分のコードを、あらためてRustやCで書き直して保守していくことに比べれば、ずっと気が楽です。
なお、この記事では、AssemblyScriptのインストールや基本的な使い方については説明を割愛します。
AssebmlyScriptにおけるオブジェクトのコピー渡しと参照渡し
WebAssemblyとJavaScriptの間には「モジュール境界(module boundaries)」があるため、文字列・配列・オブジェクトなどの複雑なデータ型を、そのままやりとりすることはできません。そこでAssemblyScriptでは、コンパイルの過程でグルー・コードを生成し、これを介することで、いろいろなデータのやりとりを実現しています。
The Assembly Script BookのHost bindingsの節には、WebAssembly側からJavaScript側へオブジェクトを渡す際に、「コピー渡し」する方法と、「参照渡し」する方法との違いが説明されています。コード例を一部改変の上で引用します。
// Copied to a JS object
class PlainObject {
field: string;
}
export function getObject(): PlainObject {
return {
field: "hello world"
};
}
上のコード例のような「プレーンなオブジェクト」(コンストラクタを持たず、non-publicなフィールドも持たない)については、モジュール境界を超えるときにはコピーが渡されます。
その一方で、
// Not copied to a JS object
class ComplexObject {
constructor(value: i32) {} // !
value: i32;
}
export function newObject(value: i32): ComplexObject {
return new ComplexObject(value);
}
export function setObjectValue(target: ComplexObject, value: i32): void {
target.value = value;
}
export function getObjectValue(target: ComplexObject): i32 {
return target.value;
}
こちらのコード例のような「複雑なオブジェクト」(コンストラクタを持っていたり、non-publicなフィールドを持っていたりする場合)には、モジュール境界を超えるときにはコピーではなく参照が渡されます。AssemblyScriptでは、こうした参照のことを、"opaque reference to the object"(オブジェクトへの透過的ではない参照)と呼んでいます。
ここで「透過的ではない参照」を行う型について、クラス定義の際にはexport修飾子をつけてはいけない、という点に注意が必要です。上記のComplexObject.tsのコード内でクラス定義をする際には、export class ComplexObject
ではなく、単にclass ComplexObject
として定義している点を確認してください。
ちなみに、export class ComplexObject
のようにエクスポート修飾子をつけてascを実行すると、次のようなエラーが発生します。
WARNING AS235: Only variables, functions and enums become WebAssembly module exports.
:
1 │ export class ComplexObject {
│ ~~~~~~~~~~~~~
└─ in ComplexObject.ts(1,14)
つまり、ascでエクスポートできるのは、変数、関数、列挙型だけである、ということなのです。以前のバージョンのAssemblyScriptでは、クラスのエクスポートについても可能とされていたのですが、開発が進められるうちに、いろいろな複雑性や矛盾が生じてしまい、現在のバージョンではエクスポート不可能なように仕様が変更された、という経緯があるようです。
ascコマンドが生成する型定義ファイルには冪等性がない!
さて、上記のコードをascコマンドでコンパイルしてみましょう。
asc ComplexObject.ts
すると、wasmファイルと併せて、次のような型定義ファイルが生成されます。
export declare function newObject(value: number): __Internref7;
export declare function setObjectValue(target: __Internref7, value: number): void;
export declare function getObjectValue(target: __Internref7): number;
declare class __Internref7 extends Number {
private __nominal7: symbol;
private __nominal0: symbol;
}
TypeScript側から、この型定義ファイルと合わせた形でグルーコードをimportすると、wasmファイルを読み込んで呼び出せるようにしてくれます。
ここで確認するべきなのは、ComplexObjectクラスとして定義されていたものが、この例では __Internref7
というように、__Internref + 数字 という型名に自動的に置き換えられているという点です。この型名が、TypeScript側で、AssemblyScript由来のオブジェクトを「透過的ではない参照」を介して扱う際の型名となります。
ところで、たいへん気持ちの悪いことに、この型名に含まれる数字は、コードをいろいろと書き換えながらascの実行を繰り返すと、一定の謎ルールで変化します。開発の途中で、asc コマンドを実行するたびに、数字部分が変化してしまう可能性がある、ということです。
私がいろいろと試みた・discordのAssemblyScriptのhelpトピックで聞いてみた 限りでは、現状のAssemblyScriptでは、__Internref+数字のような型名について、固定的な別の名前を用いることや、この数字を固定して用いることは不可能なようでした。本来であれば、もともとのクラス名(ここでの例ならば、ComplexObjectというクラス名)を型名として利用したいところなのですが、できない、ということです。
AssemblyScriptが型宣言ファイルを生成する部分の処理内容を読んでみたところ、ここでの数字は、単に型宣言ファイル上での識別子としてしか使っていないようでした。ということは、ちょっとがんばれば、__Internref+数字という型名ではなく、もともとのクラス名でも基本的には大丈な感じがします(型名が重複したときにはエラーを出すとかして...)
しかし、AssemblyScript側でnewした型の種類を、TypeScriptの側ではany型を使うこととして、混乱しないように気を付けるという程度ならば、それほど困るようなことではないようにも思われます。結論として、ここに手間をかけるよりも、ひとまず放置かな〜という判断をしています。
「透過的ではない参照」の型には、anyを指定するしかない(現状では)
TypeScript側で「透過的ではない参照」の型を保持する変数を定義する際には、__Internref+数字というような型名を使ったコードを書くべきではありません。
こうした変数を保持する型としては、現状では、現実的には、any型を使うしかないということになります。
まだまだ未成熟な、過渡的な技術なので、このくらいならば仕方がないということなのでしょう。
ちょっとモヤモヤが残りますが、この記事は、これで終わりです。
追記
ともあれ、AssemblyScriptは、実用上、極めて有用な技術です。ぜひ使ってみてください。