@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ruby.wasmにto_bを実装するには?

ruby.wasmでクエリ文字列を扱おうとしたら - @ledsun blog で、JavaScriptのオブジェクトの返す真理値が真か確認するために

if searchParams.has('phrase') == JS.eval('return true;')

と書きました。

if searchParams.has('phrase').to_b

と書きたいです。 すでにto_sメソッドが存在します。 これを参考にしたら実装出来るでしょうか?

to_sのテストコードの確認

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb#L59-L65

  def test_to_s
    assert_equal "str", JS.eval("return 'str';").to_s
    assert_equal "24", JS.eval("return 24;").to_s
    assert_equal "true", JS.eval("return true;").to_s
    assert_equal "null", JS.eval("return null;").to_s
    assert_equal "undefined", JS.eval("return undefined;").to_s
  end

が、あります。 つぎのようなテストコードを追加して、通れば良さそうです。

  def test_to_b
    assert_true, JS.eval("return true;").to_b
    assert_false "24", JS.eval("return false;").to_b
  end

to_sの実装を確認

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/js/lib/js.rb をみるとto_sメソッドの実装がありません。 Rubyでは実装されていないようです。 C拡張として実装されていそうです。

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/js/js-core.c#L327-L345

/*
 * call-seq:
 *   to_s -> string
 *
 *  Returns a printable version of +self+:
 *   JS.eval("return 'str'").to_s # => "str"
 *   JS.eval("return true").to_s  # => "true"
 *   JS.eval("return 1").to_s     # => "1"
 *   JS.eval("return null").to_s  # => "null"
 *   JS.global.to_s               # => "[object global]"
 *
 *  JS::Object#inspect is an alias for JS::Object#to_s.
 */
static VALUE _rb_js_obj_to_s(VALUE obj) {
  struct jsvalue *p = check_jsvalue(obj);
  rb_js_abi_host_string_t ret0;
  rb_js_abi_host_js_value_to_string(p->abi, &ret0);
  return rb_utf8_str_new(ret0.ptr, ret0.len);
}

で、実装がありました。 また

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/js/js-core.c#L563

  rb_define_method(rb_cJS_Object, "to_s", _rb_js_obj_to_s, 0);

to_sメソッドとして呼び出せるように、していそうなところをみつけました。 どうやら同じような形式で定義して上げるとよさそうです。

_rb_js_obj_to_sの実装を確認しなおすと、rb_js_abi_host_js_value_to_string関数で、JavaScriptのオブジェクトから文字列に変換していそうです。

rb_js_abi_host_js_value_to_string関数の実装を確認

https://github.com/ruby/ruby.wasm/blob/main/ext/js/bindgen/rb-js-abi-host.c#L156-L163

void rb_js_abi_host_js_value_to_string(rb_js_abi_host_js_abi_value_t value, rb_js_abi_host_string_t *ret0) {
  
  __attribute__((aligned(4)))
  uint8_t ret_area[8];
  int32_t ptr = (int32_t) &ret_area;
  __wasm_import_rb_js_abi_host_js_value_to_string((value).idx, ptr);
  *ret0 = (rb_js_abi_host_string_t) { (char*)(*((int32_t*) (ptr + 0))), (size_t)(*((int32_t*) (ptr + 4))) };
}

にありました。 気になるのはこのファイルは関数間に行間がありません。 人間が書くファイルではなく、何らかツールで生成するファイルなのかもしれません。 このファイルのBlameを見ていたら、よさそうなコミットを見つけました。

Add `JS::Object#{to_i,to_f}` · ruby/ruby.wasm@6884a17 · GitHub to_ito_fを追加するコミットです。 to_sではありませんが、参考になりそうです。

コミットの中身

コミットに含まれるファイル

bindgenというディレクトリが複数回でてくるのが気になります。

ruby.wasm/CONTRIBUTING.md at main · ruby/ruby.wasm · GitHubを見ると.witファイルからコードを生成するようです。

wit-bindgenをためす

rustccargoはインストール済みです。 rake check:bindgenを実行してみます。

build/toolchain/wit-bindgen/bin/wit-bindgen guest c --import ext/js/bindgen/rb-js-abi-host.wit --out-dir ext/js/bindgen
Generating "ext/js/bindgen/rb-js-abi-host.c"
Generating "ext/js/bindgen/rb-js-abi-host.h"
build/toolchain/wit-bindgen/bin/wit-bindgen host js --import ext/witapi/bindgen/rb-abi-guest.wit --export ext/js/bindgen/rb-js-abi-host.wit --out-dir
packages/npm-packages/ruby-wasm-wasi/src/bindgen
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/intrinsics.js"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.d.ts"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.js"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.d.ts"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.js"

やはり両方のbindgenディレクトリの中身は自動生成されているようです。

コミットを見直す

rb-js-abi-host.wit

variant raw-integer {
    f64(float64),
    bignum(string),
}

から

rb-js-abi-host.h

  typedef struct {
    uint8_t tag;
    union {
      double f64;
      rb_js_abi_host_string_t bignum;
    } val;
  } rb_js_abi_host_raw_integer_t;
  #define RB_JS_ABI_HOST_RAW_INTEGER_F64 0
  #define RB_JS_ABI_HOST_RAW_INTEGER_BIGNUM 1
  void rb_js_abi_host_raw_integer_free(rb_js_abi_host_raw_integer_t *ptr);

が生成されていそうです。 構造体とそれをfreeする関数を生成してくれるようです。

rb-js-abi-host.wit

js-value-to-integer: func(value: js-abi-value) -> raw-integer

からは

rb-js-abi-host.c

__attribute__((import_module("rb-js-abi-host"), import_name("js-value-to-integer: func(value: handle<js-abi-value>) -> variant { f64(float64), bignum(string) }")))
void __wasm_import_rb_js_abi_host_js_value_to_integer(int32_t, int32_t);
void rb_js_abi_host_js_value_to_integer(rb_js_abi_host_js_abi_value_t value, rb_js_abi_host_raw_integer_t *ret0) {

  __attribute__((aligned(8)))
  uint8_t ret_area[16];
  int32_t ptr = (int32_t) &ret_area;
  __wasm_import_rb_js_abi_host_js_value_to_integer((value).idx, ptr);
  rb_js_abi_host_raw_integer_t variant;
  variant.tag = (int32_t) (*((uint8_t*) (ptr + 0)));
  switch ((int32_t) variant.tag) {
    case 0: {
      variant.val.f64 = *((double*) (ptr + 8));
      break;
    }
    case 1: {
      variant.val.bignum = (rb_js_abi_host_string_t) { (char*)(*((int32_t*) (ptr + 8))), (size_t)(*((int32_t*) (ptr + 12))) };
      break;
    }
  }
  *ret0 = variant;
}

が生成されていそうです。 しかし、条件分岐まで生成されるのでしょうか?

index.ts

        jsValueToInteger(value) {
          if (typeof value === "number") {
            return { tag: "f64", val: value };
          } else if (typeof value === "bigint") {
            return { tag: "bignum", val: BigInt(value).toString(10) + "\0" };
          } else if (typeof value === "string") {
            return { tag: "bignum", val: value + "\0" };
          } else if (typeof value === "undefined") {
            return { tag: "f64", val: 0 };
          } else {
            return { tag: "f64", val: Number(value) };
          }
        },

に分岐があるのでこれが元のでしょうか? でも、分岐数かちがいますね。 謎です。

index.tsを変更してrake check:bindgenしてみる

たとえば次のように変更してみましょう。

        jsValueToInteger(value) {
          if (typeof value === "number") {
            return { tag: "f64", val: value };
          } else if (typeof value === "string") {
            return { tag: "bignum", val: value + "\0" };
          } else if (typeof value === "bigint") {
            return { tag: "bignum", val: BigInt(value).toString(10) + "\0" };
          } else if (typeof value === "undefined") {
            return { tag: "f64", val: 0 };
          } else {
            return { tag: "f64", val: Number(value) };
          }
        },

分岐の順番を入れ替えてました。 rake check:bindgenをしても生成されるファイルに変更はありません。 これが元ではないようです。 rake check:bindgenの出力をよく読みます。

build/toolchain/wit-bindgen/bin/wit-bindgen host js --import ext/witapi/bindgen/rb-abi-guest.wit --export ext/js/bindgen/rb-js-abi-host.wit --out-dir
packages/npm-packages/ruby-wasm-wasi/src/bindgen
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/intrinsics.js"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.d.ts"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.js"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.d.ts"
Generating "packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.js"

ext/witapi/bindgen/rb-abi-guest.witext/js/bindgen/rb-js-abi-host.witが入力ファイルになっていそうです。 後者は見ました。前者はみたこがありません。 https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/witapi/bindgen/rb-abi-guest.witを見た感じでは、to_iとは関係なさそうです。 ということはext/js/bindgen/rb-js-abi-host.witからrb-js-abi-host.d.tsrb-js-abi-host.jsが生成されていそうです。 そう思ってみると、名前も対応しています。

呼び出しフローを整理してみる

  1. JS::Object#to_i
  2. _rb_js_obj_to_i
  3. rb_js_abi_host_js_value_to_integer
  4. bindgenで生成された関数を経由
  5. jsValueToIntegerでJavaScriptの値をハッシュに変換

bindgenで生成した関数は

  1. rb-js-abi-host.jsは、ハッシュの値をバイト列(?)に変換
  2. rb-cs-abi-host.cは、バイト列を構造体に変換

この中間でやりとりするバイト列や構造体は、rb-js-abi-host.witで定義したデータ構造から生成されているようです。

to_b実装作戦

同じ構成を取ると仮定すると

  1. rb-js-abi-host.witに呼び出したいJavaScriptの関数を定義。データ型が必要であればそれも定義
  2. index.tsに呼び出したいJavaScriptの関数を定義
  3. rake check:bindgenを実行してbindgen関数群を生成
  4. 生成された関数を呼び出す関数を、js-core.jsに定義

すればよさそうです。