@ledsun blog

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

ruby.wasmのJS::Object#newにブロックでコールバック関数を渡す

ruby.wasmのKernel#sleepをどう実装したものか? - @ledsun blog の続きです。

次のようにruby.wasmで動くKernel#sleepを実装できます。

<html>

<head>
  <title>Kernel#sleep</title>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby" data-eval="async">
    require 'js'

    module Kernel
      def sleep(time)
        JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await
      end
    end

    start = JS.global[:Date].now.to_i
    sleep(1)
    puts "#{JS.global[:Date].now.to_i - start}ms passed"
  </script>
</head>

</html>

Promiseの初期化を次のように書いています。

JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await

次のようにnewメソッドにコールバック関数をブロックで渡したいです。

JS.global[:Promise].new do |resolve|
  JS.global.setTimeout(resolve, time * 1000)
end.await

これは次のエラーがでます。

TypeError: Promise resolver undefined is not a function

しかしnewメソッドの定義はブロックを考慮していません。

def new(*args)
  JS.global[:Reflect].construct(self, args.to_js)
end

つまり次のJavaScriptを実行しているのと同じです。

new Promise(undefined)

ですので、resolver引数がundefinedであると怒られます。 では、次のようにしてはどうでしょうか?

class JS::Object
  def new(*args, &block)
    JS.global[:Reflect].construct(self, block)
  end
end

module Kernel
  def sleep(time)
    JS.global[:Promise].new do |resolve|
      JS.global.setTimeout(resolve, time * 1000)
    end.await
  end
end

newメソッドでブロックを引数として渡します。 上手く行きそうですが、やはり同様のエラーが起きます。

TypeError: Promise resolver undefined is not a function

これはJavaScriptでかくと、次のようになります。

Reflect.construct(Promise, ()=>{})

Reflect.constructの第二引数には、配列風オブジェクトを渡す必要があります。 次のようにすると良さそうです。

def new(*args, &block)
  JS.global[:Reflect].construct(self, [block])
end

ひとまず動きます。 つぎのようにすると、newメソッドにブロックを渡したときも、引数を渡したときもJavaScriptコンストラクターを呼び出せるようになります。

<html>

<head>
  <title>Kernel#sleep</title>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby" data-eval="async">
    require 'js'

    class JS::Object
      def new(*args, &block)
        if block
          JS.global[:Reflect].construct(self, [block])
        else
          JS.global[:Reflect].construct(self, args)
        end
      end
    end

    module Kernel
      def sleep(time)
        JS.global[:Promise].new do |resolve|
          JS.global.setTimeout(resolve, time * 1000)
        end.await
      end
    end

    start = JS.global[:Date].now.to_i
    sleep(1)
    puts "#{JS.global[:Date].now.to_i - start}ms passed"

    p JS.global[:Date].new("2024-01-27T00:00:00Z")
  </script>
</head>

</html>