ruby.wasmでRubyからJavaScriptのメソッドを呼び出すしくみ

## JavaScriptオブジェクトのメソッドを呼び出せる

[ruby.wasm](https://github.com/ruby/ruby.wasm)では実行したRubyスクリプトからJavaScriptのメソッドを呼び出せます。
たとえば次のRubyスクリプトを実行するとbodyタグを表すHTML要素が取得出来ます。

```ruby
html_element = JS.global[:document].querySelector('body')
```

JavaScriptで書くと次のようになります。

```javascript
const htmlElement = globalThis.document.querySelector('body')
```

ruby.wasmの`JS.global`はJavaScriptの[globalThis](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)を表す変数です。
JavaScriptのオブジェクトのプロパティを参照するとき`[]`メソッドを使います。
つまり`JS.global[:document]`で`globalThis.document`と同じオブジェクトが取得できます。
というわけで[Document:querySelector()](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)が呼び出せます。

呼び出せるメソッドは特に決まっていません。
任意のメソッドが呼び出せます。ちょっと不思議ですね。
今回はこの仕組みを説明します。

## RubyからJavaScriptのメソッドを呼び出す

Rubyで`JS.global[:document].querySelector('body')`を呼び出したとき、次のように関数をたどって行きます。

1. `JS.global[:document].querySelector`
1. `JS::Object#method_missing`
1. `JS::Object#call`
1. `_rb_js_obj_call`
1. `rb_js_abi_host_reflect_apply`
1. `__wasm_import_rb_js_abi_host_reflect_apply`
1. `reflectApply`
1. `Reflect.apply`

最終的にJavaScriptの [Refect.apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply) 関数が呼び出されます。
この仕組みを順番に追っていきます。

### JavaScriptオブジェクトはJS::Objectインスタンス

ruby.wasmではJavaScriptのオブジェクトは`JS::Object`クラスのメソッドです。
たとえば次のようなRubyスクリプトを実行してみましょう。

```ruby
p JS.global[:document].class
# => JS::Object
```

`document`のクラスは`JS::Object`です。
Rubyでは[method_missing](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/method_missing.html)という仕組みがあります。

> 呼びだされたメソッドが定義されていなかった時、Rubyインタプリタがこのメソッドを呼び出します。

`JS.global[:document].querySelector`を呼び出したとき、`JS::Object#querySelector`メソッドは定義されていません。
`JS::Object#method_missisg`メソッドが呼び出されます。

### JS::Object#method_missisgメソッド

`JS::Object#method_missisg`メソッドの実装を見てみましょう。
ソースコードは https://github.com/ruby/ruby.wasm/blob/d0e144577f4a947638e9a0d354e12716f10c24a0/ext/js/lib/js.rb#L156C25-L167 です。

```ruby
  def method_missing(sym, *args, &block)
    sym_str = sym.to_s
    if sym_str.end_with?("?")
      # When a JS method is called with a ? suffix, it is treated as a predicate method,
      # and the return value is converted to a Ruby boolean value automatically.
      self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True
    elsif self[sym].typeof == "function"
      self.call(sym, *args, &block)
    else
      super
    end
  end
```

第一引数には呼び出したメソッド名がシンボルで入っています。

> [PARAM] name:
>  未定義メソッドの名前（シンボル）です。

今回は`:querySelector`です。

`self[sym].typeof == "function"`では、呼び出したいものが関数かどうか判定しています。
`self[sym]`はプロパティの取得です。
今回は`document[:querySelector]`になります。

取得したプロパティも`JS::Object`インスタンスになります。
`JS::Object#typeof`メソッドはJavaScriptの [typeof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) 演算子の評価結果を返します。

オブジェクトが`Function`型の場合に文字列`fuction`です。
`Document:querySelector`はFunctionです。
if式`if self[sym].typeof == "function"`が成り立ちます。

`self.call(sym, *args, &block)`が実行されます。
レシーバーが `self`なので、`JS::Object#call`メソッドが呼び出されます。

### JS::Object#callメソッド

`JS::Object#call`メソッドは js.rb では定義されていません。
C言語で実装されています。
ソースコードは https://github.com/ruby/ruby.wasm/blob/d0e144577f4a947638e9a0d354e12716f10c24a0/ext/js/js-core.c#L264-L306 にあります。

```c
static VALUE _rb_js_obj_call(int argc, VALUE *argv, VALUE obj) {
  struct jsvalue *p = check_jsvalue(obj);
  if (argc == 0) {
    rb_raise(rb_eArgError, "no method name given");
  }
  VALUE method = _rb_js_obj_aref(obj, argv[0]);
  struct jsvalue *abi_method = check_jsvalue(method);

  rb_js_abi_host_list_js_abi_value_t abi_args;
  int function_arguments_count = argc;
  if (!rb_block_given_p())
    function_arguments_count -= 1;

  abi_args.ptr =
      ALLOCA_N(rb_js_abi_host_js_abi_value_t, function_arguments_count);
  abi_args.len = function_arguments_count;
  VALUE rv_args = rb_ary_tmp_new(function_arguments_count);

  for (int i = 1; i < argc; i++) {
    VALUE arg = _rb_js_try_convert(rb_mJS, argv[i]);
    if (arg == Qnil) {
      rb_raise(rb_eTypeError, "argument %d is not a JS::Object like object",
               1 + i);
    }
    abi_args.ptr[i - 1] = check_jsvalue(arg)->abi;
    rb_ary_push(rv_args, arg);
  }

  if (rb_block_given_p()) {
    VALUE proc = rb_block_proc();
    VALUE rb_proc = _rb_js_try_convert(rb_mJS, proc);
    abi_args.ptr[function_arguments_count - 1] = check_jsvalue(rb_proc)->abi;
    rb_ary_push(rv_args, rb_proc);
  }

  rb_js_abi_host_js_abi_result_t ret;
  rb_js_abi_host_reflect_apply(abi_method->abi, p->abi, &abi_args, &ret);
  raise_js_error_if_failure(&ret);
  VALUE result = jsvalue_s_new(ret.val.success);
  RB_GC_GUARD(rv_args);
  RB_GC_GUARD(method);
  return result;
}
```

ちょっと長いので途中の説明は省略します。
最終的に`rb_js_abi_host_reflect_apply(abi_method->abi, p->abi, &abi_args, &ret);`が実行されます。
`rb_js_abi_host_reflect_apply`は、ちょっと変わった名前です。
もちろん意味があります。
 
`rb_`はRubyのメソッドを表します。
`js_abi_host`はJavaScriptがホストであることを表します。
`reflect_apply`はJavaScript側のメソッド名です。

後半2つについて、それぞれすこし説明します。

#### js_abi_host

ABIはApplication Binary Interfaceの略です。

[Wasm ABIs - WebAssembly Guide](https://www.webassembly.guide/webassembly-guide/webassembly/wasm-abis)

> An Application Binary Interface (ABI) is an interface between two binary program modules.

今回の場合は「WebAssembly化されたCRuby」と「JavaScript」の2つのバイナリ間のインタフェースです。

hostは、WebAssemblyを動かしているホストをあらわします。
ブラウザでWebAssemblyを動かす場合は、ブラウザのAPIつまり、今回で言うとJavaScriptのメソッドを指します。

#### reflect_apply

`reflect_apply`は`reflectApply`を表しています。
最終的に呼び出されるJavaScriptの関数は以下です。
https://github.com/ruby/ruby.wasm/blob/d0e144577f4a947638e9a0d354e12716f10c24a0/packages/npm-packages/ruby-wasm-wasi/src/index.ts#L257-L259

```ts
reflectApply: wrapTry((target, thisArgument, args) => {
  return Reflect.apply(target as any, thisArgument, args);
}),
```

ここから`Reflect.apply`関数を呼び出しています。

### WASI

この先のソースコードを追う前に、RubyとJavaScriptを繋いでるものを説明します。

ruby.wasmではRubyとJavaScriptの間のインタフェース、ABIを定義するためにWASIを使います。
[WASI](https://github.com/WebAssembly/WASI)はWebAssembly System Interfaceの略です。
Bytecode Alianceが策定しているインタフェース仕様です。

WASIの中には[Component Model](https://github.com/WebAssembly/component-model)という概念があります。
JavaScript（あるいはブラウザ）がホストで、その中で動かしている「WebAssembly化したRuby」がComponentに当たります。

#### WIT

Component Modelのインタフェースを定義するIDLに[WIT](https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md)があります。
ruby.wasmではWITをつかって、ABIを定義しています。
実際につぎの2つのwitファイルがあります。

- https://github.com/ruby/ruby.wasm/blob/main/ext/js/bindgen/rb-js-abi-host.wit
- https://github.com/ruby/ruby.wasm/blob/main/ext/witapi/bindgen/rb-abi-guest.wit

#### wit-bindgen

[with-bindgen](https://github.com/bytecodealliance/wit-bindgen)というツールがあります。
このツールでWITから、言語の実装を生成することが出来ます。

ruby.wasmでは`rake check:bindgen`というRakeタスクで生成します。
`rb-js-abi-host.wit`からは、つぎの4つファイルが生成されます。

- https://github.com/ruby/ruby.wasm/blob/main/ext/js/bindgen/rb-js-abi-host.c
- https://github.com/ruby/ruby.wasm/blob/main/ext/js/bindgen/rb-js-abi-host.h
- https://github.com/ruby/ruby.wasm/blob/main/packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.d.ts
- https://github.com/ruby/ruby.wasm/blob/main/packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.js

Ruby用のC言語のファイルと、JavaScript用のファイルです。

### Reflect.applyの場合

`Reflect.apply`を例に具体的な関数定義がどうなるか見ていきましょう。

WITファイルに次の定義があります。

```
reflect-apply: func(target: js-abi-value, this-argument: js-abi-value, arguments: list<js-abi-value>) -> js-abi-result
```

`wit-bindgen`を使うと、次の2つの関数定義が生成されます。

```C
__attribute__((import_module("rb-js-abi-host"), import_name("reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }")))
void __wasm_import_rb_js_abi_host_reflect_apply(int32_t, int32_t, int32_t, int32_t, int32_t);
void rb_js_abi_host_reflect_apply(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_js_abi_value_t this_argument, rb_js_abi_host_list_js_abi_value_t *arguments, rb_js_abi_host_js_abi_result_t *ret0) {
  
  __attribute__((aligned(4)))
  uint8_t ret_area[8];
  int32_t ptr = (int32_t) &ret_area;
  __wasm_import_rb_js_abi_host_reflect_apply((target).idx, (this_argument).idx, (int32_t) (*arguments).ptr, (int32_t) (*arguments).len, ptr);
  rb_js_abi_host_js_abi_result_t variant;
  variant.tag = (int32_t) (*((uint8_t*) (ptr + 0)));
  switch ((int32_t) variant.tag) {
    case 0: {
      variant.val.success = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
      break;
    }
    case 1: {
      variant.val.failure = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
      break;
    }
  }
  *ret0 = variant;
}
```

Ruby側の定義です。
JavaScript側に定義される`__wasm_import_rb_js_abi_host_reflect_apply`関数を呼び出しています。

```javascript
  imports["rb-js-abi-host"]["reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }"] = function(arg0, arg1, arg2, arg3, arg4) {
    const memory = get_export("memory");
    const len0 = arg3;
    const base0 = arg2;
    const result0 = [];
    for (let i = 0; i < len0; i++) {
      const base = base0 + i * 4;
      result0.push(resources0.get(data_view(memory).getInt32(base + 0, true)));
    }
    const ret0 = obj.reflectApply(resources0.get(arg0), resources0.get(arg1), result0);
    const variant1 = ret0;
    switch (variant1.tag) {
      case "success": {
        const e = variant1.val;
        data_view(memory).setInt8(arg4 + 0, 0, true);
        data_view(memory).setInt32(arg4 + 4, resources0.insert(e), true);
        break;
      }
      case "failure": {
        const e = variant1.val;
        data_view(memory).setInt8(arg4 + 0, 1, true);
        data_view(memory).setInt32(arg4 + 4, resources0.insert(e), true);
        break;
      }
      default:
      throw new RangeError("invalid variant specified for JsAbiResult");
    }
  };
```

JavaScript側の定義です。
上から1/3当たりにある`obj.reflectApply(resources0.get(arg0), resources0.get(arg1), result0);`がJavaScriptの関数を呼び出しています。

呼び出された`reflectApply`関数のソースコードは https://github.com/ruby/ruby.wasm/blob/d0e144577f4a947638e9a0d354e12716f10c24a0/packages/npm-packages/ruby-wasm-wasi/src/index.ts#L257-L259 です。

```ts
reflectApply: wrapTry((target, thisArgument, args) => {
  return Reflect.apply(target as any, thisArgument, args);
}),
```

`Reflect.apply` APIを呼び出しています。

## まとめ

ruby.wasmではWASIのComponent Modelに含まれるWITというIDLをつかって、Rubyから呼び出せるインタフェースの定義を書いています。
そこからwit-bindgenをつかってバインディングの実装を生成しています。
その結果、つぎの順番で関数が呼び出されています。


| No. | Method/Function                                                                 | Description                                     |
|-----|---------------------------------------------------------------------------------|-------------------------------------------------|
| 1   | `JS.global[:document].querySelector`                                    | ユーザーのメソッド呼び出し                        |
| 2   | `JS::Object#method_missing`                                                    | ruby.wasmで定義したRubyメソッド                 |
| 3   | `JS::Object#call`                                                              | ruby.wasmで定義したRubyメソッド（C言語実装）      |
| 4   | `_rb_js_obj_call`                                                              | ruby.wasmで定義したRubyメソッド（C言語実装）      |
| 5   | `rb_js_abi_host_reflect_apply`                                                 | wit-bindgenで生成したRuby側のブリッジ関数          |
| 6   | `__wasm_import_rb_js_abi_host_reflect_apply`                                   | wit-bindgenで生成したJavaScript側のブリッジ関数     |
| 7   | `reflectApply`                                                                 | ruby.wasmで定義したJavaScript関数                |
| 8   | `Reflect.apply`                                                                | ブラウザのAPI                                     |