LoginSignup
1
0

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

Posted at

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

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

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

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

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

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

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

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

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

  1. JS.global[:document].querySelector
  2. JS::Object#method_missing
  3. JS::Object#call
  4. _rb_js_obj_call
  5. rb_js_abi_host_reflect_apply
  6. __wasm_import_rb_js_abi_host_reflect_apply
  7. reflectApply
  8. Reflect.apply

最終的にJavaScriptの Refect.apply 関数が呼び出されます。
この仕組みを順番に追っていきます。

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

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

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

documentのクラスはJS::Objectです。
Rubyではmethod_missingという仕組みがあります。

呼びだされたメソッドが定義されていなかった時、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 です。

  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 演算子の評価結果を返します。

オブジェクトが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 にあります。

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

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

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はWebAssembly System Interfaceの略です。
Bytecode Alianceが策定しているインタフェース仕様です。

WASIの中にはComponent Modelという概念があります。
JavaScript(あるいはブラウザ)がホストで、その中で動かしている「WebAssembly化したRuby」がComponentに当たります。

WIT

Component Modelのインタフェースを定義するIDLにWITがあります。
ruby.wasmではWITをつかって、ABIを定義しています。
実際につぎの2つのwitファイルがあります。

wit-bindgen

with-bindgenというツールがあります。
このツールでWITから、言語の実装を生成することが出来ます。

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

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つの関数定義が生成されます。

__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関数を呼び出しています。

  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 です。

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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0