WebAssembly + C/C++: Reference Types を使いこなす

## 動作確認環境

- [wasi-sdk 14.0](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-14)

## ホストのオブジェクトを関数経由で受け渡しする

関数引数に externref/funcref を指定することで、ホスト側で使用されるオブジェクト (JavaScript オブジェクト/関数) の受け渡しができる。

C/C++ では、アドレス空間 **10** なポインタが **externref**、アドレス空間 **20** なポインタが **funcref** に対応する。

| WebAssembly | C/C++ 型宣言|
|:--:|:--:|
| externref | char \_\_attribute\_\_((address_space(10)))* |
| funcref | char \_\_attribute\_\_((address_space(20)))* |

```cpp:Main.cpp
typedef char __attribute__((address_space(10)))* externref;

extern "C" externref create_empty_object(void);

int main() {
  create_empty_object();
  return 0;
}
```

```js:Main.js
export function create_empty_object() {
  return {};
}
```

```wat:Main.wasm
;; clang -O2 Main.cpp -mreference-types -o Main.wasm --target=wasm32-wasi --sysroot=<path to wasi-sdk>/share/wasi-sysroot/ -nostartfiles -Wl,-no-entry -Wl,--allow-undefined -Wl,--export=main
(module
 (type $none_=>_externref (func (result externref)))
 (type $i32_i32_=>_i32 (func (param i32 i32) (result i32)))
 (import "env" "create_empty_object" (func $fimport$0 (result externref)))
 (memory $0 2)
 (export "memory" (memory $0))
 (export "main" (func $0))
 (func $0 (param $0 i32) (param $1 i32) (result i32)
  (drop
   (call $fimport$0)
  )
  (i32.const 0)
 )
)
```

## ホストのオブジェクトを WebAssembly.Global 経由で受け渡しする

アドレス空間 **1** に所属するグローバル変数は、`WebAssembly.Global` として格納される。

| WebAssembly | C/C++ 型宣言|
|:--:|:--:|
| i32 | int32_t \_\_attribute\_\_((address_space(1))) |
| i64 | int64_t \_\_attribute\_\_((address_space(1))) |
| f32 | float \_\_attribute\_\_((address_space(1))) |
| f64 | double \_\_attribute\_\_((address_space(1))) |
| externref | char \_\_attribute\_\_((address_space(10)))* \_\_attribute\_\_((address_space(1))) |
| funcref | char \_\_attribute\_\_((address_space(20)))* \_\_attribute\_\_((address_space(1))) |

```Main.cpp
char __attribute__((address_space(10)))* __attribute__((address_space(1))) wasm_global_externref;

__attribute__((export_name("receive_externref")))
void receive_externref(char __attribute__((address_space(10)))* ref) {
    wasm_global_externref = ref;
}
```

```wat:Main.wasm
;; clang -O2 Main.cpp -mmutable-globals -mreference-types -o Main.wasm --target=wasm32-wasi --sysroot=<path to wasi-sdk>/share/wasi-sysroot/ -nostartfiles -Wl,-no-entry -Wl,--export=receive_externref -Wl,--export=wasm_global_externref
(module
 (type $externref_=>_none (func (param externref)))
 (global $global$0 (mut externref) (ref.null extern))
 (memory $0 2)
 (export "memory" (memory $0))
 (export "wasm_global_externref" (global $global$0))
 (export "receive_externref" (func $0))
 (func $0 (param $0 externref)
  (global.set $global$0
   (local.get $0)
  )
 )
)
```

## 関数ポインタからホストの関数オブジェクトに変換する

ホストで関数ポインタを受け取って、WebAssembly.Table の要素を参照せずとも、関数ポインタから直接ホストの関数オブジェクトを取得でき、ホストに渡すことができる。

```cpp:Main.cpp
typedef char __attribute__((address_space(20)))* funcref;
typedef void* funcref_ptr;

extern "C" {
  // imported from javascript
  int setTimeout(funcref callback, int timeout);
  funcref funcptr_to_funcref(funcref_ptr);
}

void some_proc() {

}

int main() {
  setTimeout(funcptr_to_funcref((funcref_ptr)&some_proc), 1000);
  return 0;
}
```

```asm:Main.support.S
.globl __indirect_function_table
.tabletype __indirect_function_table, funcref

.globl funcptr_to_funcref

funcptr_to_funcref:
    .functype funcptr_to_funcref(i32) -> (funcref)
        local.get 0
        table.get __indirect_function_table
    end_function
```

```wat:Main.wasm
;; clang -O2 Main.cpp -mreference-types -o Main.wasm --target=wasm32-wasi --sysroot=<path to wasi-sdk>/share/wasi-sysroot/ -nostartfiles -Wl,-no-entry -Wl,--allow-undefined -Wl,--export=main
(module
 (type $funcref_i32_=>_i32 (func (param funcref i32) (result i32)))
 (type $none_=>_none (func))
 (type $i32_i32_=>_i32 (func (param i32 i32) (result i32)))
 (import "env" "setTimeout" (func $fimport$0 (param funcref i32) (result i32)))
 (memory $0 2)
 (table $0 2 2 funcref)
 (elem (i32.const 1) $0)
 (export "memory" (memory $0))
 (export "main" (func $1))
 (func $0
  (nop)
 )
 (func $1 (param $0 i32) (param $1 i32) (result i32)
  (drop
   (call $fimport$0
    (table.get $0
     (i32.const 1)
    )
    (i32.const 1000)
   )
  )
  (i32.const 0)
 )
)
```

## Q&A

### ビルド時にコンパイラが落ちてしまう

* clang に `-mreference-types` オプションを付与する (筆者はこれで 1 時間ほど無駄にしたことがある)
* clang の最適化レベルを `-O1` 以上に設定する
  * `-O0` だと線形メモリ上の一時変数に externref/funcref を格納しようとしてしまい、対応する命令がないためにビルドに失敗してしまうことがある

### どうして WebAssembly.Local を C/C++ 関数内で定義できないのか

アドレス空間が **1** な変数がコンテキストによって、 WebAssembly.Local または  WebAssembly.Global になる。しかし、Clang はスタック変数をアドレス空間で修飾することを許可していないため。

### どうして externref/funcref を構造体のメンバにできないのか

C/C++ の構造体は基本的に線形メモリに格納され、線形メモリには externref/funcref を格納できないため。