@ledsun blog

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

require_relativeはパッチれない

requrie_relativeをハックしたい

require_relativeの相対参照の起点となるもの - @ledsun blogRubyスクリプトのURLを保持する必要があるとわかりました。 そこで次の感じでURLを保持したVMクラスを作りました。

import { RubyScriptAndSourceURL } from "./RubyScriptAndSourceURL";

// To achieve require_relative, we need to resolve relative paths in Ruby scripts.
// VM to remember the URL of the running Ruby script.
export class RubyVMWithURL {
  private _vm;
  // Stores the URL of the running Ruby script to get the relative path from within the Ruby script.
  private _soruceURLStack: Array<URL>;

  constructor(vm) {
    this._vm = vm;
    this._soruceURLStack = [];
  }

  eval(script: RubyScriptAndSourceURL): void {
    if (script) {
      this.evalOn(script.URL, script.body);
    }
  }

  evalFromCurrentURL(scriptBody: string): void {
    this.evalOn(this.currentURL, scriptBody);
  }

  evalOn(url: URL, scriptBody: string): void {
    this._soruceURLStack.push(url);

    if (scriptBody) {
      this._vm.eval(scriptBody);
    }

    this._soruceURLStack.pop();
  }

  get currentURL(): URL {
    return this._soruceURLStack.at(-1);
  }
}

改めてみると、ちょっと不格好ですね。まあ、とりあえず動くので require_relativeのパッチを書きます。 次のようにrequire_relativeをJavaScriptで定義したrb_require_relativeに置き換えます。

    require "js"

    module Kernel
      def require_relative(relative_feature)
        ret = JS.global.rb_require_relative(relative_feature)

        return ret[:isLoaded] if ret[:errorMessage].to_s.empty?

        raise LoadError.new ret[:errorMessage].to_s
      end
    end

requireに影響が出てしまう

ところがrequire 'csv'が動かなくなります。

require "csv" 起因のエラー

組み込みのGemはパッチを当てなくてもrequire_relativeが動いていました。 つまり、require_relativeへのパッチが過剰に聞いています。 では、alias_method してもとのrequrie_relativeを実行して失敗したときだけ、JavaScript版を動かすと良さそうです。 次のイメージです。

module Kernel
  alias_method :hoge, :require_relative

  def require_relative(relative_feature)
    hoge(relative_feature)
  rescue LoadError
    ret = JS.global.rb_require_relative(relative_feature)
  end
end

ところがこれだ上手く行かないのです。 エラーがかわりません。

requrie_relativeはパッチれない

なぜかというと、これが表題の「requrie_relativeはパッチれない」です。

次のRubyスクリプトをCRubyで実行するとエラーが起きます。

module Kernel
  alias_method :hoge, :require_relative

  def require_relative(relative_feature)
    hoge(relative_feature)
  end
end

require 'csv'
ledsun@MSI:~/ruby.wasm►ruby test.rb
test.rb:five:in `require_relative': cannot load such file -- /home/ledsun/ruby.wasm/csv/fields_converter (LoadError)
        from test.rb:five:in `require_relative'
        from /home/ledsun/.rbenv/versions/3.2.0-preview2/lib/ruby/3.2.0+2/csv.rb:96:in `<top (required)>'
        from <internal:/home/ledsun/.rbenv/versions/3.2.0-preview2/lib/ruby/3.2.0+2/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from <internal:/home/ledsun/.rbenv/versions/3.2.0-preview2/lib/ruby/3.2.0+2/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from test.rb:nine:in `<main>'

ブラウザの時と似た感じのエラーです。 csv/fields_converterの場所がおかしいです。 これはrequire_relative がどうやって読み込むRubyスクリプトのパスを解決しているかに依存する現象です。

https://github.com/ruby/ruby/blob/3fae53a343ebd7686bb20d8f4b6855f4d11019cd/load.c#L958-L966

rb_f_require_relative(VALUE obj, VALUE fname)
{
    VALUE base = rb_current_realfilepath();
    if (NIL_P(base)) {
        rb_loaderror("cannot infer basepath");
    }
    base = rb_file_dirname(base);
    return rb_require_string(rb_file_absolute_path(fname, base));
}

rb_current_realfilepath()で現在実行中のRubyスクリプトのパスを取得しています。

https://github.com/ruby/ruby/blob/913979bede2a1b79109fa2072352882560d55fe0/vm_eval.c#L2552-L2556

rb_current_realfilepath(void)
{
    const rb_execution_context_t *ec = GET_EC();
    rb_control_frame_t *cfp = ec->cfp;
    cfp = vm_get_ruby_level_caller_cfp(ec, RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp));

rb_current_realfilepathではRUBY_VM_PREVIOUS_CONTROL_FRAME(cfp)を使って、require_relativeメソッドの呼び出し元を参照しています。 つまりrequire_relativeメソッドにパッチを当てると、常にパッチをあてた場所が相対パスの起点になってしまいます。

次の手

RubyVM中のrequire_relativeにパッチが当てられません。 RubyVMに与えるRubyスクリプトにパッチを当ててはどうでしょうか?

const patchedScript = scriptBody.replace(/require_relative/g, 'patched_require_relative')
this._vm.eval(patchedScript);

こういう感じです。 *1

*1:知らなかったのですが、TypeScriptってString.ReplaceAll使えないんですね。https://bobbyhadz.com/blog/typescript-string-replace-all-occurrences