C拡張のある gem を作れるようになる
はじめに
fukuokarb.connpass.com この記事は上記イベント会場で書いています。 Speee さん、 Fukuoka.rb の皆様ありがとうございます。
そういえばC拡張のgemって作ったことないなと思ったので作りました。RubyKaigi いくとなんかこういうことやりたくなりますよね。
https://github.com/hkdnet/fibc
このエントリではC拡張の gem ってどうなってるんだっけの解説、というほど高尚なものでもないですが今回認識したことを書きます。
C拡張を動かす
今回はCで複雑なロジックを書くことは考えず、フィボナッチ数列の n 番目の数を返すメソッドを追加することにする。つまりこんなメソッドを追加することを目的とする。
Fibc.fib(5) # => 8
bundle gem --ext fibc で雛形ができる。 --ext でC拡張用のファイルも作ってくれる。gemspec にTODOとかが追加されているので、以下それを解消しているものとして扱う。
ファイル構成はだいたいこんな感じ。
. ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin │ ├── console │ └── setup ├── ext │ └── fibc │ ├── extconf.rb │ ├── fibc.c │ └── fibc.h ├── fibc.gemspec ├── lib │ ├── fibc │ │ └── version.rb │ └── fibc.rb └── spec ├── fibc_spec.rb └── spec_helper.rb
実は Rakefile にもそういう定義が追加されている。さっと確認すると compile などという単語が見え、:default のタスク定義にも compile が追加されているので、ビルドしてからテストするような感じになることがわかる。
$ bundle exec rake -T rake build # Build fibc-0.1.0.gem into the pkg directory rake clean # Remove any temporary products rake clobber # Remove any generated files rake compile # Compile all the extensions rake compile:fibc # Compile fibc rake install # Build and install fibc-0.1.0.gem into system gems rake install:local # Build and install fibc-0.1.0.gem into system gems without net... rake release[remote] # Create tag v0.1.0 and build and push fibc-0.1.0.gem to TODO: ... rake spec # Run RSpec code examples
# Rakefile require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) require "rake/extensiontask" task :build => :compile Rake::ExtensionTask.new("fibc") do |ext| ext.lib_dir = "lib/fibc" end task :default => [:clobber, :compile, :spec]
gemspec の定義にも spec.extensions = ["ext/fibc/extconf.rb"] という設定があり、gem install 時にどう build すればよいのかを示していることがわかる。extconf.rb については後述する……予定だったがまあ実際 makefile を生成してるっぽいことくらいしかわからなかった。生成後の結果は tmp 配下に吐かれるので確認可能。
rake compile すると lib/fibc/fibc.bundle というファイルができる。これがC拡張をコンパイルしたもの。
最初 gem のエントリポイントである lib/fibc.rb に require 'lib/fibc/fibc' と書いてあって、そんなのないじゃんって思ってたのだけど、コンパイルしたものを require している。
ここまでわかったので、あとは ruby/ruby のコードを参考にしながら雑にメソッドを追加してみる。C言語において Ruby のモジュールを定義しているところを適当に参考にしながらやればよい。RubyKaigi で大人気だった RubyVM::AbstractSyntaxTree.of の定義している箇所を参考にする。ちょうどよく module に1引数のシングルトンメソッドを定義するという意味で非常に似通っている。
ruby/ast.c at 03c6cb5e8f503dcf6bc80a70a48a9775bbd2a47e · ruby/ruby · GitHub
cruby の世界では、Rubyのオブジェクトはすべて VALUE 型であり、また Ruby のメソッドは必ず戻り値をもつ。なので (VALUE self, VALUE n) -> VALUE な関数を定義してそれを使うことにする。とりあえずステップバイステップに実装するために、引数をそのまま戻す関数を定義してみる。
diff --git a/ext/fibc/fibc.c b/ext/fibc/fibc.c index 71788c7..4beb8a1 100644 --- a/ext/fibc/fibc.c +++ b/ext/fibc/fibc.c @@ -2,8 +2,15 @@ VALUE rb_mFibc; +static +VALUE fibc_fib(VALUE self, VALUE n) +{ + return n; +} + void Init_fibc(void) { rb_mFibc = rb_define_module("Fibc"); + rb_define_singleton_method(rb_mFibc, "fib", fibc_fib, 1); }
テストは適当に書いて動作確認おk1。
あとは適当にCで書いた関数を使いつつ FIX2INT, INT2FIX を使いながらCの世界とRubyの世界のオブジェクトを変更しておけば動く。
せっかくなのでベンチマークをとってみる。手元のマシンでやった適当な結果ですけどやっぱはやい。
$ cat bench.rb
require 'benchmark/ips'
require 'fibc'
Benchmark.ips do |x|
# These parameters can also be configured this way
x.time = 5
x.warmup = 2
# Typical mode, runs the block as many times as it can
x.report("naive") { Fibc.naive(10) }
x.report("fib") { Fibc.fib(10) }
# Compare the iterations per second of the various reports!
x.compare!
end
$ bundle exec ruby bench.rb
Warming up --------------------------------------
naive 19.611k i/100ms
fib 223.955k i/100ms
Calculating -------------------------------------
naive 195.295k (± 8.4%) i/s - 980.550k in 5.061215s
fib 4.174M (± 9.1%) i/s - 20.828M in 5.043693s
Comparison:
fib: 4174204.0 i/s
naive: 195295.2 i/s - 21.37x slower
動いてC拡張すごい、という気持ちになったところでおわり。ここまでは特に難しくなかったですね。
C拡張書くときに何が使えるのかとかがわからなかったり、Rubyでの処理がCのどの関数呼べばいいのかとかがわからないので次はもうちょい実践的な何かをチャレンジしていきます。
おまけ
objdump するとダミーシンボル的なものがあることがわかる(わからない(読めない
$ objdump -no-show-raw-insn -arch-name x86-64 -macho -x86-asm-syntax intel -D lib/fibc/fibc.bundle
lib/fibc/fibc.bundle:
(__TEXT,__text) section
_Init_fibc:
ec0: push rbp
ec1: mov rbp, rsp
ec4: lea rdi, [rip + 213] ## literal pool for: "Fibc"
ecb: call 0xf5c ## symbol stub for: _rb_define_module
ed0: mov qword ptr [rip + _rb_mFibc], rax
ed7: lea rsi, [rip + 199] ## literal pool for: "fib"
ede: lea rdx, [rip + _fibc_fib]
ee5: mov ecx, 1
eea: mov rdi, rax
eed: pop rbp
eee: jmp 0xf62 ## symbol stub for: _rb_define_singleton_method
ef3: nop word ptr cs:[rax + rax]
efd: nop dword ptr [rax]
_fibc_fib:
f00: push rbp
f01: mov rbp, rsp
f04: mov rdi, rsi
f07: call 0xf68 ## symbol stub for: _rb_fix2int
f0c: mov edi, eax
f0e: call _fibc_fib_int
f13: cdqe
f15: lea rax, [rax + rax + 1]
f1a: pop rbp
f1b: ret
f1c: nop dword ptr [rax]
_fibc_fib_int:
f20: push rbp
f21: mov rbp, rsp
f24: push r14
f26: push rbx
f27: mov r14d, 1
f2d: cmp edi, 2
f30: jl 0xf53
f32: mov ebx, edi
f34: add ebx, 2
f37: mov r14d, 1
f3d: nop dword ptr [rax]
f40: lea edi, [rbx - 3]
f43: call _fibc_fib_int
f48: add r14d, eax
f4b: add ebx, -2
f4e: cmp ebx, 3
f51: jg 0xf40
f53: mov eax, r14d
f56: pop rbx
f57: pop r14
f59: pop rbp
f5a: ret
-
見たければコミットを追ってください↩