結論
複数のクラスを実装しているRuby内部のCコードでbuiltinをいくつか組み合わせるとビルドできなくなる。
起きたこと
TrueClass
をRubyで実装することで高速化できるというチケットを少し前に投げてた。
bugs.ruby-lang.org
パッチも作成してPull Requestで投げたんだけど、CIで軒並み死んでいるということに……(ローカルでちゃんとテスト回してないからこういうことになる……)
テストが落ちた原因としてはbootstraptest/test_insns.rb
でString#freeze
が再定義されていたため。
実は高速化にあたって文字列をfreezeして返すような以下のコードを書いてた。
class TrueClass
def to_s
"true".freeze
end
end
このコードだとfreeze
が再定義されているケースで期待した動作にならなくなってしまうのだった……。
そこで、Ruby内部のC関数を呼び出す実装に変更してみた。
class TrueClass
def to_s
Primitive.attr! 'inline'
Primitive.cexpr! 'true_to_s(self)'
end
end
肝はPrimitive.cexpr! 'true_to_s(self)'
で、このコードでCで定義された関数を実行している(厳密には、このRubyコードをもとにCの関数が新しく定義され、それを実行しているみたい)
あとはビルドすればOKだろうと思い、ビルドすると関数が再定義されているというエラーが発生。
sh@MyComputer:~/rubydev/build$ make benchmark/trueclass.yml -e COMPARE_RUBY=~/.rbenv/shims/ruby -e BENCH_RUBY=../install/bin/ruby
compiling ../ruby/compile.c
compiling ../ruby/object.c
In file included from ../ruby/object.c:4665:
../ruby/trueclass.rbinc:21:1: エラー: ‘mjit_compile_invokebuiltin_for__bi0’ が再定義されました
21 | mjit_compile_invokebuiltin_for__bi0(FILE *f, long index, unsigned stack_size, bool inlinable_p)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from ../ruby/object.c:4664:
../ruby/kernel.rbinc:42:1: 備考: 前の ‘mjit_compile_invokebuiltin_for__bi0’ の宣言はここです
42 | mjit_compile_invokebuiltin_for__bi0(FILE *f, long index, unsigned stack_size, bool inlinable_p)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: 警告: 認識できないコマンドラインオプション ‘-Wno-self-assign’ です
cc1: 警告: 認識できないコマンドラインオプション ‘-Wno-parentheses-equality’ です
cc1: 警告: 認識できないコマンドラインオプション ‘-Wno-constant-logical-operand’ です
Makefile:424: recipe for target 'object.o' failed
make: *** [object.o] Error 1
原因
Rubyの実装(Cのコード)では一つのソースコード内にいくつかのRubyのクラスが定義されていることがある。
例えばobject.c
ではObjectクラスのほかNilClassやTrueClassなども定義されている。
今回は既にKernelモジュールの一部メソッドがRubyで実装されており、そこへ追加でTrueClassを別ファイルで実装したのがまずかった。
RubyでRubyを実装した場合、ビルド時にRubyのコードをもとに.rbinc
拡張子でCのコードが自動生成される。
その.rbinc
をCのコード内でインクルードすることでRubyで実装したメソッドを実行できるようにしている。
つまり今回の場合は以下のような形で二つの.rbinc
をインクルードしていた。
#include "kernel.rbinc"
#include "trueclass.rbinc"
これがまずい背景としては自動生成されたCコードの関数名がかぶってしまっているため。
なので、上記のようにインクルードすると#include "trueclass.rbinc"
の部分で関数の再定義が起きてしまう。
つまり、RubyでRubyを実装する場合は基本的にCコード(object.c
など)に対して、Rubyのコードと.rbinc
が一つづつセットでないとエラーになるということ。
対処法
原因がはっきりしたので対処法をアレコレ考えてみた。
まあ、結論から言えば「自動生成されているCの関数名が重複しなければいい」ので、重複しないように修正した。
RubyのコードをCコードに変換しているのはtool/mk_builtin_loader.rb
というファイルになる。
で、このファイルの中で定義されているmk_builtin_header
メソッド内でCコードへの自動変換を行っている。
def mk_builtin_header file
base = File.basename(file, '.rb')
ofile = "#{file}inc"
code = File.read(file)
collect_iseq RubyVM::InstructionSequence.compile(code).to_a
collect_builtin(base, Ripper.sexp(code), 'top', bs = {}, inlines = {})
begin
f = open(ofile, 'w')
rescue Errno::EACCES
f = open(File.basename(ofile), 'w')
end
begin
if File::ALT_SEPARATOR
file = file.tr(File::ALT_SEPARATOR, File::SEPARATOR)
ofile = ofile.tr(File::ALT_SEPARATOR, File::SEPARATOR)
end
lineno = __LINE__
f.puts "// -*- c -*-"
f.puts "// DO NOT MODIFY THIS FILE DIRECTLY."
f.puts "// auto-generated file"
f.puts "// by #{__FILE__}"
f.puts "// with #{file}"
f.puts '#include "internal/compilers.h" /* for MAYBE_UNUSED */'
f.puts '#include "internal/warnings.h" /* for COMPILER_WARNING_PUSH */'
f.puts '#include "ruby/ruby.h" /* for VALUE */'
f.puts '#include "builtin.h" /* for RB_BUILTIN_FUNCTION */'
f.puts 'struct rb_execution_context_struct; /* in vm_core.h */'
f.puts
lineno = __LINE__ - lineno - 1
line_file = file
inlines.each{|cfunc_name, (body_lineno, text, locals, func_name)|
if String === cfunc_name
f.puts "static VALUE #{cfunc_name}(struct rb_execution_context_struct *ec, const VALUE self)"
lineno += 1
lineno, str = generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
f.write str
else
f.puts "#line #{body_lineno} \"#{line_file}\""
lineno += 1
f.puts text
lineno += text.count("\n") + 1
f.puts "#line #{lineno + 2} \"#{ofile}\"" TODO
lineno += 1
end
}
bs.each_pair{|func, (argc, cfunc_name)|
decl = ', VALUE' * argc
argv = argc \
. times \
. map {|i|", argv[#{i}]"} \
. join('')
f.puts %'static void'
f.puts %'mjit_compile_invokebuiltin_for_#{func}(FILE *f, long index, unsigned stack_size, bool inlinable_p)'
f.puts %'{'
f.puts %' fprintf(f, " VALUE self = GET_SELF();\\n");'
f.puts %' fprintf(f, " typedef VALUE (*func)(rb_execution_context_t *, VALUE#{decl});\\n");'
if inlines.has_key? cfunc_name
body_lineno, text, locals, func_name = inlines[cfunc_name]
lineno, str = generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
f.puts %' if (inlinable_p) {'
str.gsub(/^(?!#)/, ' ').each_line {|i|
j = RubyVM::CEscape.rstring2cstr(i).dup
j.sub!(/^ return\b/ , ' val =')
f.printf(%' fprintf(f, "%%s", %s);\n', j)
}
f.puts(%' return;')
f.puts(%' }')
end
if argc > 0
f.puts %' if (index == -1) {'
f.puts %' fprintf(f, " const VALUE *argv = &stack[%d];\\n", stack_size - #{argc});'
f.puts %' }'
f.puts %' else {'
f.puts %' fprintf(f, " const unsigned int lnum = GET_ISEQ()->body->local_table_size;\\n");'
f.puts %' fprintf(f, " const VALUE *argv = GET_EP() - lnum - VM_ENV_DATA_SIZE + 1 + %ld;\\n", index);'
f.puts %' }'
end
f.puts %' fprintf(f, " func f = (func)%"PRIdPTR"; /* == #{cfunc_name} */\\n", (intptr_t)#{cfunc_name});'
f.puts %' fprintf(f, " val = f(ec, self#{argv});\\n");'
f.puts %'}'
f.puts
}
f.puts "void Init_builtin_#{base}(void)"
f.puts "{"
table = "#{base}_table"
f.puts " // table definition"
f.puts " static const struct rb_builtin_function #{table}[] = {"
bs.each.with_index{|(func, (argc, cfunc_name)), i|
f.puts " RB_BUILTIN_FUNCTION(#{i}, #{func}, #{cfunc_name}, #{argc}, mjit_compile_invokebuiltin_for_#{func}),"
}
f.puts " RB_BUILTIN_FUNCTION(-1, NULL, NULL, 0, 0),"
f.puts " };"
f.puts
f.puts " // arity_check"
f.puts "COMPILER_WARNING_PUSH"
f.puts "#if GCC_VERSION_SINCE(5, 1, 0) || __clang__"
f.puts "COMPILER_WARNING_ERROR(-Wincompatible-pointer-types)"
f.puts "#endif"
bs.each{|func, (argc, cfunc_name)|
f.puts " if (0) rb_builtin_function_check_arity#{argc}(#{cfunc_name});"
}
f.puts "COMPILER_WARNING_POP"
f.puts
f.puts " // load"
f.puts " rb_load_with_builtin_functions(#{base.dump}, #{table});"
f.puts "}"
ensure
f.close
end
end
今回関数名が重複している箇所はf.puts %'mjit_compile_invokebuiltin_for_#{func}(FILE *f, long index, unsigned stack_size, bool inlinable_p)'
なのでこれを以下のように変更した。
f.puts %'mjit_compile_invokebuiltin_for_#{func.hash.abs}(FILE *f, long index, unsigned stack_size, bool inlinable_p)'
func
は呼び出されたCの関数名などが文字列で渡されており、それをハッシュ値に変換している。abs
を使っているのは値がマイナスの際にエラーになるのを回避するため。
あとは、自動生成された関数を呼び出せるようにする箇所を以下のように修正する。
f.puts " RB_BUILTIN_FUNCTION(#{i}, #{func}, #{cfunc_name}, #{argc}, mjit_compile_invokebuiltin_for_#{func.hash.abs}),"
こちらも同じように文字列をハッシュ値にしている。
補足
「なぜこれで関数名が重複しないのか?」と疑問が湧くかもしれないので、補足する。
Rubyで書いた実装はビルド時にCのコードへと変換されるんだけど、これはそれぞれのファイルごとに処理される(今回ならkernel.rb
とtrueclass.rb
は別々に処理される)
そのため文字列をハッシュ値に変換するのがそれぞれのファイルで実行される形になる。
で、Rubyで文字列のハッシュ値に変換する場合は実行するごとに値が異なるので値が同じにならないということ(まあハッシュ値の衝突とかは起こる可能性も考慮しなきゃなんだけども……)
なので、こういった修正でビルドができるようになる。