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')
を呼び出したとき、次のように関数をたどって行きます。
JS.global[:document].querySelector
JS::Object#method_missing
JS::Object#call
_rb_js_obj_call
rb_js_abi_host_reflect_apply
__wasm_import_rb_js_abi_host_reflect_apply
reflectApply
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の略です。
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
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ファイルがあります。
- 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というツールがあります。
このツールで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つの関数定義が生成されます。
__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 |