requrie_relativeをハックしたい
require_relativeの相対参照の起点となるもの - @ledsun blog でRubyスクリプトの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'
が動かなくなります。
組み込みの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