💎

RubyのC拡張ライブラリの仕組みを簡単にハンズオンで学ぶ

に公開

こんにちは、yukyanです。RubyKaigi2026 0日目ですが、皆さんいかがお過ごしですか。

今回は、RubyKaigi2026予習シリーズということで、1日目「When Can You Skip a Test? Tracking Test Impact」というタイトルで登壇予定の Andrey Marchenko氏の過去の記事、「How we built a Ruby library that saves 50% in testing time」 で使われていた、Cを使ったRubyの拡張ライブラリ について、自身で勉強した内容をハンズオン形式でアウトプットしていこうと思います。

記事を読んでいて、「C拡張でVMイベントを直接フックする」というくだりがあったのですが、そもそもC拡張とは何なのか、どう書くのか、というところがわからなかったので、手を動かして最小限のプログラムを書き、理解してみようという趣旨です。

RubyのC拡張とは

Rubyのライブラリ(gem)は、Pure Ruby、つまりRubyだけで書くこともできるのですが、C言語で書くこともできます。C言語でRubyのAPIを使うコードを書き、そこからコンパイルされた動的ライブラリ .so ファイル(Linux)や .bundle ファイル(macOS)を含むことができるのです。

有名どころだと、nokogiri(XMLパーサー)、pg(PostgreSQLドライバ)、などがC拡張を含んでいます。

今回は、この後者であるC拡張を、コードを自分で書いて、動的ライブラリにコンパイルし、Rubyから呼び出すところまでをやってみます。

ハンズオン:Hello, WorldなC拡張を作る

require_relative 'hello'

puts Hello.say  # => "Hello, World!"

Hello.say を呼ぶと "Hello, World!" が返ってくる、という最小のC拡張を作ります。

ステップ1:作業ディレクトリの作成

mkdir hello
cd hello

ステップ2:C拡張本体を書く(hello.c

#include "ruby.h"

// Rubyから呼ばれる関数
static VALUE say(VALUE self)
{
  return rb_str_new_cstr("Hello, World!");
}

// require された時に呼ばれる初期化関数
void Init_hello(void)
{
  // Hello モジュールを作る
  VALUE mHello = rb_define_module("Hello");

  // モジュールに say 関数を定義
  rb_define_singleton_method(mHello, "say", say, 0);
}

コードの解説

#include "ruby.h"

RubyのC API(後述する VALUE 型や rb_* 関数群)を使うためのヘッダファイルを読み込む。

static VALUE say(VALUE self)

Cの関数定義。VALUEC拡張内でRubyオブジェクトを表すC言語の型self はこのメソッドが呼ばれたレシーバ(Rubyの self と同じ)を表す。

return rb_str_new_cstr("Hello, World!");

C文字列をRubyの String オブジェクトに変換するAPI。https://docs.ruby-lang.org/ja/latest/function/rb_str_new2.html と同じ

void Init_hello(void)

require 'hello' された時にRubyが自動的にこの関数を探して呼び出す。拡張名は後述の create_makefile に渡す名前と一致させる。

Rubyは拡張ライブラリをロードする時に「Init_ライブラリ名」という関数を自動的に実行します.

https://docs.ruby-lang.org/en/master/extension_ja_rdoc.html#c

rb_define_module("Hello")

Rubyのモジュールを定義するAPI。これでRubyで module Hello と書くのと同じように、Rubyの世界に Hello モジュールが定義される。
https://docs.ruby-lang.org/ja/latest/function/rb_define_module.html

rb_define_singleton_method(mHello, "say", say, 0)

モジュール関数(Module.method の形式で呼べるもの)を定義するAPI。Hello モジュールに say メソッドを定義し、Cの say 関数にマップする。最後の 0 は引数の数。この登録によって、Ruby側で Hello.say を呼ぶと、Cの say 関数が実行されるようになる。
https://docs.ruby-lang.org/ja/latest/function/rb_define_singleton_method.html

ステップ3:ビルド設定を書く(extconf.rb

extconf.rbC拡張のビルド設定ファイル で、この拡張ライブラリのコンパイルに必要な条件のチェックなどを行えます。また、 create_makefile を実行することで、拡張ライブラリをビルドするためのMakefile を生成することができます。

require 'mkmf'
create_makefile('hello')

mkmf は、Rubyに標準で添付されている、拡張ライブラリを作るためのMakefileを生成するライブラリです。create_makefile に渡す名前('hello')は、ターゲットとなるモジュール名です。

ステップ4:動作確認用のスクリプトを書く(test.rb

require_relative 'hello'

puts Hello.say

ステップ5:ビルドする

先ほど作成した extconf.rb を実行します。

ruby extconf.rb

これで Makefile が生成されます。

make

コンパイルが走って、hello.so(macOSでは hello.bundle)が生成されます。

現在、こんな感じのディレクトリ構成になっていると思います。

hello/
├── hello.c
├── hello.o
├── hello.so
├── Makefile
├── extconf.rb
└── test.rb

ステップ6:実行する

ruby test.rb

Hello, World! と表示されたら成功です。

中では何が起きているのか

ざっくり以下のような流れになっています。

1. require_relative 'hello'

2. Rubyが hello.so を見つけて読み込む

3. Init_hello 関数が自動で呼ばれる

4. Ruby世界に Hello モジュールが生まれる

5. Hello.say を呼ぶ

6. Cの say 関数が実行される

7. "Hello, World!" というRubyのStringを返す

8. puts で表示される

Init_hello が自動で呼ばれてモジュールが生えてくるのと、rb_define_singleton_method で明示的にRubyのメソッド名とC関数を紐付けているところが、C拡張が動く仕組みの入り口になっています。

Datadogの記事ではどのように使われていたか

元記事の datadog_cov.c では、rb_thread_add_event_hook というVMのイベントフックAPIを使って「行が実行されるたびにC関数を呼ぶ」仕組みが組まれています。

今回のハンズオンで使った rb_define_modulerb_define_singleton_method と同じく、Rubyが公開しているC APIの一つです。構造としては今回のハンズオンで書いた仕組みの延長線上にある、と考えてもらえればイメージしやすいと思います。

実物のソースコードは DataDog/datadog-ci-rb の datadog_cov.c で読めます。

まとめ

今回は、Rubyの拡張ライブラリ(C拡張)の基本を手を動かして学んでみました。

  • Rubyのライブラリは .rb だけでなくC言語でも書ける
  • 最小のC拡張は、Cソース+extconf.rb の2ファイルで作れる。Init_<拡張名> というエントリポイント関数と、rb_define_modulerb_define_singleton_method のようなAPIで、Rubyの世界にモジュール・メソッドを生やす。

Rubyの話をするときに、C拡張が出てついていけなかったことがあるのですが、これで超ざっくりですが理解できてよかったです。
RubyKaigi当日、Andrey氏の登壇を楽しみにしています!では、また明日会いましょう。

参考資料

GMOペパボ株式会社

Discussion