Ruby C拡張における引数解析をマスターする:ステップバイステップガイド
Rubyは素晴らしい言語です。「人間を第一に、機械を第二に」という理念のもとに作られており、読み書きがとても簡単です。同じ処理を書く方法がいくつもあり、自分ならこう名付けるだろうと思うメソッド名を入力すれば、標準ライブラリの使い方を推測できることも多いでしょう。
そのため、Rubyの引数は非常に柔軟で、APIをとても明確に表現できます。しかし、これには欠点もあります。C拡張機能の開発者にとって、Rubyは解析がかなり難しいのです!
この記事では、Cで書かれた複雑なRuby APIを構築する2つの方法を見ていきます。
rb_define_methodを使って定義し、rb_scan_argsで解析する方法- Rubyインターフェースを使う方法
それでは始めましょう!
C と Ruby:はじめに
前述のとおり、RubyはC拡張機能の開発者にとって解析が難しい言語です。
例えば:
Ruby自体が書かれているC言語の美しさは、そのシンプルさ、特に関数パラメーターのシンプルさに由来します。
<データ型> <変数識別子>- 可変長引数のための
...
これらのおかげで、理解しやすいコードベースを維持できます。
以下は、C関数を定義する際に最も複雑になるケースです:
Rubyのコードベース向けにC拡張機能を書き始めると、どこから複雑さが生じるのかが分かってきます。でも心配いりません。Ruby MRIの開発者たちが私たちをサポートしてくれています。
Ruby C 拡張でのシンプルなメソッド定義
まずは、必ず一度は使うことになる rb_define_method から始めましょう。
この記事のコード例は、公開されているリポジトリで実際に試すことができます。
rb_define_method のシグネチャは次のとおりです:
そして、Rubyの extension.rdoc によると:
argcは引数の数です。argcが-1の場合、関数はargc、argv、selfの3つの引数を受け取ります。argcが-2の場合、関数はselfとargsの2つの引数を受け取ります。ここでargsは、メソッド引数を格納したRubyの配列です。
要約すると:
つまり、APIが固定長のパラメーターを持つメソッドのみ、あるいは可変長パラメーターが1つだけ(def foo(*bar))で構成されているなら、これ以上読む必要はありません。これで完了です!より柔軟なAPI呼び出し方法をお求めの方は、ぜひ読み進めてください。
ちなみに、rb_define_method のより詳しい例については、Peter Zhuによる Defining Methods の記事を読むことをお勧めします。
Ruby C APIの内部を活用する
では、本題のユースケースに戻りましょう。複雑な引数の解析です。幸い、役立つツールがいくつか用意されています。
しかしまず、rb_define_method だけを使う場合の限界を見てみましょう。
rb_define_method の欠点
ブロック引数についての言及がない
1つ目の制限は、rb_define_method にはブロック引数についての記述がないことです。ブロックは考慮されません。ブロックを渡しても渡さなくても、Rubyにとってはあまり問題にならないからです。ブロックが渡されたことを確認したい場合は、rb_block_given_p や rb_need_block を使うことができます。このトピックの詳細はPeter Zhuの記事に譲ります。
引数は変動しうる
もう1つの重要な制限は、引数が変動しうる一方で、メソッド呼び出し自体はそれほど制約されていないことです。したがって、def foo(bar, *baz) のようなAPIを実現したい場合は、引数を自分で解析する必要があります。そのためのメソッドがいくつか用意されています。rb_check_arity はそのひとつで、rb_define_method の-1バージョンと併せて使用できます。
関数のシグネチャはこちらです:
キーワード引数
最後に取り上げる制限は、キーワード引数の使用です。これは本記事の中核となるテーマなので、あえて最後に紹介します。
キーワード引数を正しく解析するには、まずそれらを取得する必要があります。幸い、Ruby C APIにはそのためのメソッド rb_scan_args が用意されています。
rb_scan_args のシグネチャはこちらです:
rb_scan_args には、rb_define_method から受け取った argc と argv、引数の解析方法を指定する文字列(fmt)、そして解析結果の受け皿となる変数を渡します。これで、Rubyの引数の複雑さすべてを1行で解析できます!まあ、ほぼですが。
fmt の正式な書式については extension.rdoc を参照できますが、以下の例である程度カバーしていきます。
関数の作成と解析:実例
この記事の残りの部分では、次のような関数を書きたいと考えます:
これを rb_scan_args で解析すると、次のようになります:
"1*:" という一見意味不明な文字列の意味は以下のとおりです:
1: 必須の位置引数が1つ*: 必須ではない任意の数の位置引数:: 最後にキーワード引数
キーワード引数の解析
これでメソッドのシグネチャは定義できましたが、残念ながらまだ作業は終わりません。現在のAPIは def voronoi_diagram(envelope, *polygons, **kwargs) となっています。
最後に、キーワード引数を rb_get_kwargs を使って解析する必要があります。
まず、必須引数とオプション引数を選択します。その後、table を使ってRubyに引数の名前を伝え、結果を配列(values)に格納します。
これで完成です!複雑なRubyメソッドをCだけを使って解析できました。ただし、これが複雑すぎると感じる方には、別の選択肢があります。
Rubyインターフェースを使う
この問題への対処法として、実際にRubyの構文を直接使い、Rubyレベルで解析を行う方法もあります。
こうすることで、rb_define_method の第3の形式を直接利用でき、Cメソッド側は次のようにシンプルになります:
これで問題を完全に回避できました。エレガントな解決策で、実際にRuby本体の一部のメソッドでも使われています(Primitive クラスを使用)。
MRIで使われているクラスはかなり複雑で、Cコード自体を生成する仕組みですが、そこからインスピレーションを得ることができます。
オブジェクトを作成してメソッドを接続し、APIのユーザーから c_voronoi_diagram が見えないようにしましょう:
このクラスの実際の使用例は、RGeoのコードベースで確認できます。
引数解析:どちらの方法を使うべきか?
このリポジトリでは、Cで書かれた複雑なRuby APIを構築する2つの方法を紹介しています。おさらいすると:
rb_define_methodを使って定義し、rb_scan_argsで解析する方法- Rubyインターフェースを使う方法
両方の解決策をパフォーマンスの観点から比較すると、ほぼ同等です(筆者のM1環境では、平均してRubyでの解析が1.02倍速い程度)。大きな違いはありません。
RGeoライブラリでは、最初の設計として可変長引数のみを使用するAPIを採用しました。キーワード引数なし、ブロックなしです。これは非常に制限的になりえるため、現在では Primitive 方式を使って、より複雑な引数にも対応できるようにしています。
Rubyインターフェースを使うメリット
私のアドバイスは、以下の理由からRubyインターフェースを使うことです:
- コードサイズが小さくなる
- 変更が容易になる
全体的に、コードベースを読む体験がシンプルになります。
利用しているgemのソースコードをRubyユーザーに読んでもらうことは、私にとって重要なテーマです。Rubyは読みやすいのだから、gemもそうあるべきだからです。
rb_define_method を使うメリット
一方、Cバージョンは、非常に便利ないくつかの内部メソッドを知るきっかけになります。例えば、rb_check_arity は今でも本当に有用です。ブロックを扱うためのメソッド群も優れており、ブロックのためにわざわざRubyファサードを使う必要がないケースもあるでしょう。
大切なのは、自分のユースケースに最適な選択を見つけることです。
まとめ
この記事では、Ruby C拡張で引数を解析する2つの方法を探りました。rb_define_method を使う方法(その限界についても簡単に触れました)と rb_scan_args で解析する方法、そしてRubyインターフェースを使う方法です。
C拡張についてさらに学びたい方には、以下の記事をお勧めします:
- Building a Ruby C Extension From Scratch
- A Rubyist's Walk Along the C-side
- Working with ruby C extensions on Mac
最後のアドバイス:RGeoをチェックしてみてください。活発に開発されている、広く使われているC拡張コードベースです。この記事の例の多くはここから引用しています。
それでは、Happy coding!
P.S. Ruby Magicの記事を公開と同時に読みたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしてください!

Ulysse Buonomo
ゲスト著者のUlysseは元Ruby開発者で、現在はその時間の大半を世界中の旅に捧げています。余暇はRGeoとRubyの開発に充てており、Rubyの内部実装をいじるのが大好きです。
-
Magic DashboardsでRubyアプリのパフォーマンス監視を強化する方法
アプリケーションチームにとって、顧客がどのような体験をしているのかを把握することは不可欠です。これは、ユーザビリティやレスポンスの良さといった全般的な観点だけでなく、日々の運用における一分一秒単位の変化についても当てはまります。 特に分散システムを扱う場合、エラーの発生は避けられません。サイトへのトラフィックは一日の中で変動し、システムが依存するコンポーネントのいずれにも、いつ問題が起きてもおかしくないからです。 本記事では、「マジックダッシュボード(Magic Dashboards)」を活用して、Ruby on Railsアプリケーションのパフォーマンス問題を監視・解決する方法を解説します。
-
JavaScriptイベントでReact.jsとStimulusJSをシームレスに統合する方法
私たちAppSignalは最近、「はじめに(Getting Started)」ページを追加しました。ここには、新規ユーザーがAppSignalを最大限に活用できるよう、おすすめのステップがまとめられています。 初めての利用で手厚いガイドを歓迎するユーザーもいれば、自分のペースで製品を探索したい方や、すでにAppSignalに精通している方もいます。 そこで私たちは、すべてのユーザーがガイド付きでも独力でも快適に使えるよう、「はじめに」ページの表示・非表示を切り替えられるトグル機能を設計しました。 課題 一見簡単そうに見えますが、ユーザーが個人設定メニューから「はじめに」ページを切り替えられるよ