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
-
見たければコミットを追ってください↩