Kotlin/WASM のお勉強
Scala に一番似ていて WasmGC 対応している言語と言えば Kotlin なので、どういう感じでコンパイルしていくのか調べてみよう。
ちなみに LICENSE は Apache 2 https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
-Xwasm
とかで kotlinc-js
から wasm 生成できそう?
$ kotlinc-js -Xwasm helloworld.kt
error: IR: Specify output dir via -ir-output-dir
error: old Kotlin/JS compiler is no longer supported. Please migrate to the new JS IR backend
old Kotlin/JS compiler is no longer supported. Please migrate to the new JS IR backend
いや、backendどうやって切り替えんねん。
isIrBackendEnabled
かつ isPreIrBackendDisabled
だと良さそうですね。
wasm
だけだと isIrBackendEnabled
は true
なだけ。
-
irOnly
かつwasm
だったり -
useK2
やirProduceJs
やirBuildCache
だといけそう。
$ kotlinc-js -Xwasm -Xir-only -ir-output-dir=ir -ir-output-name=helloworld helloworld.kt
exception: java.lang.AssertionError: Built-in class kotlin.Unit is not found
う〜ん?なんかCLIだとどうやって動かすのかわからんな? これだ
このへんがエントリーポイントっぽい
- Kotlin IR はどんなデータ構造をしている?抽象度は?
- Kotlin IR から wasm 向けに lower する、どんな変換をするんですか?
- そして wasm に変換する。どんな変換?
やることは
-
compileToLoweredIr
- JS向けにLowerされたKotlin IRを更にWASM向けにLowerする
-
compileWasm
- LowerしたKotlin IRをWasmに変換
概要
compileToLoweredIr
lowering list
主な lowering phases
InlineFunctions
-
StaticMembersLowering
- static members を top-level declaration に変換する。WASMに static なる概念はない
-
EnumWhenLowering
-
enum class Color { ... }
やwhen (c: Color) { ... }
を単なる if-else に変換する。WASM に enum ない
-
-
StringConcatenationLowering
- 文字列の結合を
StringBuffer
に変換、よく分からんけど良さそう
- 文字列の結合を
-
DefaultParameterValueLowering
- WASMにdefault argumentないので、default args の埋め込み
-
TailrecLowering
- tailrec function call を loop に変換
-
FunctionReferenceLowering:
- function reference を integer に変換して table indices で
call_indirect
に変換? funcref は wasm gc で追加されたはずじゃ?
- function reference を integer に変換して table indices で
-
ClassReferenceLowering
- class reference を integer に、これも reference type でなんとかなりませんか?
-
Interface Lowering
- Interface to table dispatch (call_indirect)
-
BridgesConstruction
- 何も分からん
-
CallableReferenceLowering
- callable references を integer へ???? function reference と何が違う?
compileWasm
-
WasmCompiledModuleFragment
とかいうやつで コンパイル済み wasm module を保持していく -
WasmModuleFragmentGenerator
とかいうやつが IR module を WASM bytecode に変換していって、WasmCompiledModuleFragment
に変換する
val codeGenerator = WasmModuleFragmentGenerator(backendContext, compiledWasmModule, allowIncompleteImplementations = allowIncompleteImplementations)
allModules.forEach { codeGenerator.collectInterfaceTables(it) }
allModules.forEach { codeGenerator.generateModule(it) }
-
collectInterfaceTables
- すべての入力IRモジュールをtraverseして、interface tableを収集。すべてのinterface class を見つけて、interface symbol から index への mappping を構築?
-
generateModule
- IR中の各関数のシンボルに対して function を作る
- 各 fields と class について
global
を作成 (fields は structures 使えば?) - IR type を WASM type に変換 ここ重要では
- interface dispatch の対応
- exception handling をなんとか
-
linkWasmCompiledFragments
- 各IR moduleから作ったWASM fragmentsを一つの wasm module にlinkする
う〜んやっぱり例がないとわかりません!各Lowering phaseのIR入出力とか見せてくれんか? というか Kotlin IR はどういう姿してんだよ
この IrTree
っていうやつが Kotlin IR を定義する DSL になってるらしいが...
To debug WASM tests, run:
./gradlew :wasm:wasm.tests:test -Pfd.kotlin.wasm.debugMode=2
Values of the debug mode:0
(orfalse
),1
(ortrue
),2
.
Debug mode2
will ensure that IR is dumped to a file after each lowering phase.
The IR dumps will appear next to the generated.js
or.wat
file.
おっ、まずはこれの入出力眺めてみるか
❯ ./gradlew :wasm:wasm.tests:test -Pfd.kotlin.wasm.debugMode=2
...
FAILURE: Build failed with an exception.
* What went wrong:
Could not determine the dependencies of task ':kotlin-stdlib:compileKotlinJvm'.
> Requesting vendor list failed: {"message":"Internal Server Error","_links":{"self":{"href":"/disco/v3.0/packages?jdk_version=6&distro=mandrel&operating_system=macos&latest=available&directly_downloadable=true","templated":false}},"_embedded":{"errors":[{"message":"Internal Server Error: Please provide a valid jdkVersion"}]}}
...
BUILD FAILED in 13s
55 actionable tasks: 6 executed, 49 up-to-date
なるほど
$ echo "kotlin.build.isObsoleteJdkOverrideEnabled=true" > local.properties
$ ./gradlew :wasm:wasm.tests:test
BUILD SUCCESSFUL in 2h 5m
547 actionable tasks: 497 executed, 50 up-to-date
ok
$ ./gradlew :wasm:wasm.tests:test \
--tests org.jetbrains.kotlin.wasm.test.K1WasmCodegenBoxTestGenerated \
-Pfd.kotlin.wasm.debugMode=2 \
-Pfd.org.jetbrains.kotlin.compiler.ir.dump.strategy="KotlinLike"
wasm/wasm.tests/build/out/codegen/k1Box/
の下に .ir
や .wat
ファイルを生成する
簡単そうなのでこれを見る
wasm/wasm.tests/build/out/codegen/k1Box/traits/inheritedFun/irdump/main
に IR dump が入ってる
// 02_AFTER.ValidateIrBeforeLowering.kt.ir
// FILE: inheritedFun.kt
// path: /inheritedFun.kt
interface A {
fun f(): Int {
return 239
}
/* fake */ override operator fun equals(other: Any?): Boolean
/* fake */ override fun hashCode(): Int
/* fake */ override fun toString(): String
}
class B : A {
constructor() /* primary */ {
super/*Any*/()
/* <init>() */
}
/* fake */ override fun f(): Int
/* fake */ override operator fun equals(other: Any?): Boolean
/* fake */ override fun hashCode(): Int
/* fake */ override fun toString(): String
}
@JsExport
fun box(): String {
return when {
EQEQ(arg0 = B().f(), arg1 = 239) -> "OK"
true -> "fail"
}
}
最終的にこうなる
// 257_AFTER.ValidateIrAfterLowering.kt.ir
// FILE: inheritedFun.kt
// path: /inheritedFun.kt
interface A {
fun f(): Int {
return 239
}
/* fake */ override operator fun equals(other: Any?): Boolean
/* fake */ override fun hashCode(): Int
/* fake */ override fun toString(): String
}
class B : A {
constructor() /* primary */ {
super/*Any*/()
{ // BLOCK
}
}
/* fake */ override fun f(): Int
/* fake */ override operator fun equals(other: Any?): Boolean
/* fake */ override fun hashCode(): Int
/* fake */ override fun toString(): String
}
fun box(): String {
return when {
wasm_i32_eq(a = { // BLOCK
val tmp: B = B()
tmp.f()
}, b = 239) -> "OK"
true -> "fail"
}
}
@JsExport
@JsName(name = "box")
fun box__JsExportAdapter(): JsString? {
val currentIsNotFirstWasmExportCall: Boolean = #isNotFirstWasmExportCall
{ // BLOCK
{ // RETURNABLE BLOCK
return { // BLOCK
val tmp: JsString? = { // RETURNABLE BLOCK
kotlinToJsAnyAdapter(x = return kotlinToJsAnyAdapter(x = try try return // COMPOSITE {
#isNotFirstWasmExportCall = true
return kotlinToJsStringAdapter(x = box())
// }
catch (e: Throwable)return when {
currentIsNotFirstWasmExportCall -> throw e
else -> throwAsJsException(t = e)
}
catch (t: Throwable)// COMPOSITE {
#isNotFirstWasmExportCall = currentIsNotFirstWasmExportCall
throw t
// }
))
}
// COMPOSITE {
#isNotFirstWasmExportCall = currentIsNotFirstWasmExportCall
// }
tmp
}
}
// COMPOSITE {
#isNotFirstWasmExportCall = currentIsNotFirstWasmExportCall
// }
}
}
interface A {
fun f():Int = 239
}
class B() : A
これは以下のようになってる
(type $B___type_585 (sub $kotlin.Any___type_131
(struct
(field (ref $B.vtable___type_368))
(field (ref null struct))
(field (mut i32))
(field (mut i32)))
))
(type $kotlin.Any___type_131
(struct
(field (ref $kotlin.Any.vtable___type_112))
(field (ref null struct))
(field (mut i32))
(field (mut i32))))
各関数のvtableがあるのはOK、それ以外の3つのフィールドはなんだろう? 2つめは itable
Introducing Kotlin/Wasm · seb.deleuze.fr
typeInfo と hashCode
Any の vtable
(type $kotlin.Any.vtable___type_112
(struct
(field (ref null $____type_731))
(field (ref null $____type_728))
(field (ref null $____type_762))))
(type $____type_731
(func (param
(ref null $kotlin.Any___type_131)
(ref null $kotlin.Any___type_131)) (result i32)))
(type $____type_728
(func (param
(ref null $kotlin.Any___type_131))
(result i32)))
(type $____type_762
(func (param
(ref null $kotlin.Any___type_131))
(result (ref null $kotlin.String___type_514))))
う〜んこのシグネチャは...!? このメソッドたちでは?
/* fake */ override operator fun equals(other: Any?): Boolean
/* fake */ override fun hashCode(): Int
/* fake */ override fun toString(): String
B の vtable
(type $B.vtable___type_368 (sub $kotlin.Any.vtable___type_112
(struct
(field (ref null $____type_731))
(field (ref null $____type_728))
(field (ref null $____type_762))
(field (ref null $____type_728)))))
上の3つは Any と同じやつだとして、最後の $____type_728
は Any -> Int
なので多分これが f()
なんだろうな
vtable の型定義しか見ていない、実体はどこに? いました
こういう書き方できるんだ??? ref $B.vtable___type_368
型の global $B.vtable___g_500
を定義。それぞれのフィールドに funcref を初期値として入れる
(global $B.vtable___g_500 (ref $B.vtable___type_368)
ref.func $kotlin.Any.equals___fun_1738
ref.func $kotlin.Any.hashCode___fun_1741
ref.func $kotlin.Any.toString___fun_1742
ref.func $A.f___fun_3362
struct.new $B.vtable___type_368)
equals くらいは一応みておくか。
(func $kotlin.Any.equals___fun_1738 (type $____type_731)
(param $0_<this> (ref null $kotlin.Any___type_131))
(param $1_other (ref null $kotlin.Any___type_131)) (result i32)
local.get $0_<this> ;; type: kotlin.Any
local.get $1_other ;; type: kotlin.Any?
ref.eq
return)
$A.f___fun_3362
はどうでしょう
(func $A.f___fun_3362 (type $____type_728)
(param $0_<this> (ref null $kotlin.Any___type_131)) (result i32)
i32.const 239
return)
うむ。これoverrideしてるケースとかもみたいな。
type $B___type_585 (sub $kotlin.Any___type_131 の初期化
;; (type $____type_1842 (func (param (ref null $B___type_585)) (result (ref null $B___type_585))))
(func $B.<init>___fun_3363 (type $____type_1842)
(param $0_<this> (ref null $B___type_585)) (result (ref null $B___type_585))
;; Object creation prefix
local.get $0_<this>
ref.is_null
if
;; Any parameters
global.get $B.vtable___g_500
global.get $B.classITable___g_696
i32.const 10720 ;; typeinfo?
i32.const 0 ;; hashcode? 何で0?
struct.new $B___type_585
local.set $0_<this>
end
local.get $0_<this>
return)
-
$B.vtable___g_500
これはさっき見たやつだね -
$B.classITable___g_696
iTable だ Interface Dispatch | Lukas Atkinson
B.classITable____g_696
(global $B.classITable___g_696 (ref $classITable___type_152)
ref.func $A.f___fun_3362 ;; fun 関数の実装
struct.new $A.itable___type_130
struct.new $classITable___type_152)
(func $A.f___fun_3362 (type $____type_728) ;; これは fun 関数の実装だ
(param $0_<this> (ref null $kotlin.Any___type_131)) (result i32)
i32.const 239
return)
(type $A.itable___type_130 (struct (field (ref null $____type_728)))) ;; ___type_728 は fun 関数の型
(type $classITable___type_152 (struct (field (ref null $A.itable___type_130))))
Bのitableには Aのitableがはいっているだけ。Aのitableには fun 関数の reference が入っている。
次は interface dispatch 側を見ていく
(type $____type_1209 (func (param) (result (ref null $kotlin.String___type_514))))
(func $box___fun_3364 (type $____type_1209) (result (ref null $kotlin.String___type_514))
;; val tmp: B = B()
(local $0_tmp (ref null $B___type_585))
ref.null none
call $B.<init>___fun_3363
local.tee $0_tmp ;; type: <root>.B
local.get $0_tmp ;; type: <root>.B
;; tmp.f()
;; interface call: A.f
struct.get $kotlin.Any___type_131 1
ref.cast $classITable___type_152
struct.get $classITable___type_152 0
struct.get $A.itable___type_130 0
call_ref (type $____type_728)
i32.const 239
i32.eq
if (result (ref null $kotlin.String___type_514))
;; const string: "OK"
i32.const 538
i32.const 15934
i32.const 2
call $kotlin.stringLiteral___fun_2417
else
;; const string: "fail"
i32.const 539
i32.const 15938
i32.const 4
call $kotlin.stringLiteral___fun_2417
end
return)
大事なのはこのへん、B
型のインスタンスをstackに2つ積んでいる。
1つめは struct.get ...
で funcref を取得するため、もうひとつが call_ref
の引数
local.tee $0_tmp ;; type: <root>.B
local.get $0_tmp ;; type: <root>.B
;; tmp.f()
;; interface call: A.f
struct.get $kotlin.Any___type_131 1
ref.cast $classITable___type_152
struct.get $classITable___type_152 0
struct.get $A.itable___type_130 0
call_ref (type $____type_728)
これ
もうちょっと難しい diamond 継承の例
ここでは C の実装が呼ばれてほしいはず
(func $box___fun_3365 (type $____type_1212) (result (ref null $kotlin.String___type_517))
(local $0_tmp (ref null $Impl___type_588))
(local $1_tmp (ref null $Impl___type_588))
ref.null none
call $Impl.<init>___fun_3364
local.tee $0_tmp ;; type: <root>.Impl
i32.const 0
local.get $0_tmp ;; type: <root>.Impl
;; interface call: C.<set-bar>
struct.get $kotlin.Any___type_134 1
ref.cast $classITable___type_155
struct.get $classITable___type_155 2
struct.get $C.itable___type_132 0
call_ref (type $____type_1024)
ref.null none
call $Impl.<init>___fun_3364
local.tee $1_tmp ;; type: <root>.Impl
local.get $1_tmp ;; type: <root>.Impl
;; interface call: C.<get-bar>
struct.get $kotlin.Any___type_134 1
ref.cast $classITable___type_155
struct.get $classITable___type_155 2
struct.get $C.itable___type_132 1
call_ref (type $____type_731)
i32.eqz
if
;; const string: "Fail get"
i32.const 539
i32.const 15956
i32.const 8
call $kotlin.stringLiteral___fun_2417
return
else
end
;; const string: "OK"
i32.const 540
i32.const 15972
i32.const 2
call $kotlin.stringLiteral___fun_2417
return)
これなんでBの探索とか実行時にやらないで、最初からC見に行けてるの???
interface call
こういう場合で、静的には A か B かわからないときどうすんのさ。型推論で v: {I & J}
まで分かってる。
interface K
interface I : K {
fun ff(): String
}
interface J : K {}
class A: I, J {
override fun ff() = "OK"
}
class B: I, J {
override fun ff() = "Irrelevant"
}
fun box(): String {
val v = if (true) A() else B()
return v.ff()
}
(func $box___fun_64 (type $____type_93) (result (ref null $kotlin.String___type_34))
(local $0_v (ref null $kotlin.Any___type_16))
(local $1_tmp (ref null $kotlin.Any___type_16))
ref.null none
call $A.<init>___fun_60
local.tee $0_v ;; type: <root>.K
local.tee $1_tmp ;; type: <root>.I <-- RTTI: A
local.get $1_tmp ;; type: <root>.I <-- RTTI: A
;; interface call: I.ff
struct.get $kotlin.Any___type_16 1
ref.cast $classITable___type_20
struct.get $classITable___type_20 1
struct.get $I.itable___type_14 0
call_ref (type $____type_68)
return)
I.itable___type_14
の $____type_68
なる型の function reference を呼び出す。
(type $classITable___type_20 (struct
(field (ref null $K.itable___type_13))
(field (ref null $I.itable___type_14))
(field (ref null $J.itable___type_15))))
(type $K.itable___type_13 (struct))
(type $I.itable___type_14 (struct
(field (ref null $____type_68))))
(type $J.itable___type_15 (struct))
;; ff の型? receiver は Any になるのね
(type $____type_68 (func
(param (ref null $kotlin.Any___type_16))
(result (ref null $kotlin.String___type_34))))
で、class A の itable はこんな感じで、I.itable
に A で override した実装が入っていますね。
(global $A.classITable___g_26 (ref $classITable___type_20)
struct.new $K.itable___type_13
ref.func $A.ff___fun_61
struct.new $I.itable___type_14
struct.new $J.itable___type_15
struct.new $classITable___type_20)
(func $A.ff___fun_61 (type $____type_68)
(param $0_<this> (ref null $kotlin.Any___type_16)) (result (ref null $kotlin.String___type_34))
;; const string: "OK"
i32.const 25
i32.const 998
i32.const 2
call $kotlin.stringLiteral___fun_17
return)
WasmGC のなしだと virtual dispatch や interface dispatch はどうしてる?
- Rust: How does dynamic dispatch work in WebAssembly?
- aggregated type がないので、linear memory 上に allocation して〜メモリ上のオブジェクトへのメモリを〜みたいなのをコンパイラが実装してて大変そう。WasmGC だと実行エンジンがヒープ管理をしてくれるはずなので楽
- typed function references がないので、table に関数を登録して
call_indirect
で呼び出す - virtual dispatch は global vtable あればいい
- interface dispatch も別に call_indirect があればいい?allocate したオブジェクトが、関数の実装のインデックスを知っていれば問題ないはず。
- 問題になってくるのは WasmGC によって subtyping がある場合
- call_indirect の実行時型チェックは subtyping チェックをしてくれますか?
- 上の例で、
ff
の receiver の型は、A とか B みたいな derived type になる。 - でも呼び出し側は実行時の型がわからないはず? なので、
I
とかを receiver の型とする- あるいは rtti を call_indirect になんとか食わせる...
- もしくは型チェック無視 -> unsound やんけ
-
A <: I
ではあるんですが、これ callee の型が caller の(指定した)型の subtype であることは許容しますか? しない場合はcall_indirect
による dynamic dispatch で困るケースでそう。
https://github.com/WebAssembly/gc/pull/339/files#diff-c27d706f56c96b16367feaf98f4057264b3c9d72c2c62c71c8cecdb57767d39d これの ;;Runtime types
を見ると subtyping のチェックにも対応していそう。じゃあ何で call_indirect じゃなくてvtable や itable を持つ実装になっているのだ?
j2cl は?
(なんか Bazel 5.4.0 だと動かなった ()
// google/j2cl/samples/wasm/src/main/java/com/google/j2cl/samples/wasm/HelloWorld.java
package com.google.j2cl.samples.wasm;
class Animal {
String name;
Animal(String name) { this.name = name; }
void eat() { System.out.println(name + " is eating."); }
}
class Dog extends Animal {
Dog(String name) { super(name); }
String bark() { return "Woof! Woof!"; }
}
public class HelloWorld {
public static String getHelloWorld() {
Animal genericAnimal = new Animal("Generic Animal");
genericAnimal.eat();
System.out.println("---------------");
Dog myDog = new Dog("Buddy");
myDog.eat();
return myDog.bark();
}
}
$ cd samples/wasm
$ bazel build src/main/java/com/google/j2cl/samples/wasm:jsapp
;;; Code for com.google.j2cl.samples.wasm.Dog [type definition]
(type $com.google.j2cl.samples.wasm.Dog (sub $com.google.j2cl.samples.wasm.Animal (struct
(field $vtable (ref $com.google.j2cl.samples.wasm.Dog.vtable))
(field $itable (ref $itable))
(field $$systemIdentityHashCode@java.lang.Object (mut i32))
(field $name@com.google.j2cl.samples.wasm.Animal (mut (ref null $java.lang.String)))
))
)
(type $com.google.j2cl.samples.wasm.Dog.vtable (sub $com.google.j2cl.samples.wasm.Animal.vtable (struct
(field $$getClassImpl__java_lang_Class (ref $function.$getClassImpl__java_lang_Class))
(field $m_equals__java_lang_Object__boolean (ref $function.m_equals__java_lang_Object__boolean))
(field $m_getClass__java_lang_Class (ref $function.m_getClass__java_lang_Class))
(field $m_hashCode__int (ref $function.m_hashCode__int))
(field $m_toString__java_lang_String (ref $function.m_toString__java_lang_String))
(field $m_eat__void_$pp_com_google_j2cl_samples_wasm (ref $function.m_eat__void_$pp_com_google_j2cl_samples_wasm))
(field $m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm (ref $function.m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm))
))
)
;;; End of code for com.google.j2cl.samples.wasm.Dog [type definition]
;;; String HelloWorld.getHelloWorld()
(func $m_getHelloWorld__java_lang_String@com.google.j2cl.samples.wasm.HelloWorld
(result (ref null $java.lang.String))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:37:25
(local $genericAnimal (ref null $com.google.j2cl.samples.wasm.Animal))
(local $$qualifier (ref null $java.io.PrintStream))
(local $myDog (ref null $com.google.j2cl.samples.wasm.Dog))
(block
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:37:41
(call $$clinit__void_<once>_@com.google.j2cl.samples.wasm.HelloWorld )
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:38:8
(local.set $genericAnimal (call $$create__java_lang_String@com.google.j2cl.samples.wasm.Animal (call $$getString_|Generic_Animal|__java_lang_String_<once>_@com.google.j2cl.samples.wasm.HelloWorld )))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:39:8
(call_ref $function.m_eat__void_$pp_com_google_j2cl_samples_wasm (ref.as_non_null (local.get $genericAnimal))(struct.get $com.google.j2cl.samples.wasm.Animal.vtable $m_eat__void_$pp_com_google_j2cl_samples_wasm (struct.get $com.google.j2cl.samples.wasm.Animal $vtable(local.get $genericAnimal))))
(block
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:41:8
(local.set $$qualifier (block (result (ref null $java.io.PrintStream))
(call $$clinit__void_<once>_@java.lang.System )
(global.get $out@java.lang.System)
))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:41:8
(call_ref $function.m_println__java_lang_String__void (ref.as_non_null (local.get $$qualifier))(call $$getString_|_______________|__java_lang_String_<once>_@com.google.j2cl.samples.wasm.HelloWorld )(struct.get $java.io.PrintStream.vtable $m_println__java_lang_String__void (struct.get $java.io.PrintStream $vtable(local.get $$qualifier))))
)
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:43:8
(local.set $myDog (call $$create__java_lang_String@com.google.j2cl.samples.wasm.Dog (call $$getString_|Buddy|__java_lang_String_<once>_@com.google.j2cl.samples.wasm.HelloWorld )))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:44:8
(call_ref $function.m_eat__void_$pp_com_google_j2cl_samples_wasm (ref.as_non_null (local.get $myDog))(struct.get $com.google.j2cl.samples.wasm.Animal.vtable $m_eat__void_$pp_com_google_j2cl_samples_wasm (struct.get $com.google.j2cl.samples.wasm.Animal $vtable(local.get $myDog))))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:45:8
(return (call_ref $function.m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm (ref.as_non_null (local.get $myDog))(struct.get $com.google.j2cl.samples.wasm.Dog.vtable $m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm (struct.get $com.google.j2cl.samples.wasm.Dog $vtable(local.get $myDog)))))
)
)
このへん見ると分かるけど、Kotlin/Wasm と同様に各オブジェクトは vtable や itable へのインスタンスを持っている形。
(call_ref $function.m_eat__void_$pp_com_google_j2cl_samples_wasm (ref.as_non_null (local.get $myDog))(struct.get $com.google.j2cl.samples.wasm.Animal.vtable $m_eat__void_$pp_com_google_j2cl_samples_wasm (struct.get $com.google.j2cl.samples.wasm.Animal $vtable(local.get $myDog))))
;;@ com/google/j2cl/samples/wasm/HelloWorld.java:45:8
(return (call_ref $function.m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm (ref.as_non_null (local.get $myDog))(struct.get $com.google.j2cl.samples.wasm.Dog.vtable $m_bark__java_lang_String_$pp_com_google_j2cl_samples_wasm (struct.get $com.google.j2cl.samples.wasm.Dog $vtable(local.get $myDog)))))