Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

ゼロから始めるRuby C拡張機能の作り方 ― CライブラリをRubyから活用する

今回のRuby Magicでは、RubyからC言語で書かれたコードを利用する方法をご紹介します。この手法は、パフォーマンスが重要となる処理の最適化や、CライブラリとRubyをつなぐインターフェースの構築に活用できます。具体的には、Cで書かれたライブラリをラップする「拡張機能(エクステンション)」を作成することで実現します。

C言語には、成熟していて高いパフォーマンスを持つライブラリが数多く存在します。それらをRubyに移植して車輪の再発明をするのではなく、既存のライブラリをそのまま活用するのが賢い選択です。こうすることで、普段どおり好きな言語であるRubyでコードを書きながら、Rubyが伝統的に苦手としている分野ではCライブラリの力を借りることができます。実際、私たちAppSignalでも、rdkafka gemの開発においてこのアプローチを採用しました。

それでは、具体的な手順を見ていきましょう。実際に手を動かして試してみたい方は、サンプルコードもご覧ください。まずは、文字列・数値・真偽値を扱う次のRubyコード(なぜこの3つなのかは、すぐに「C」わかりますよ――ダジャレです)を、Cライブラリへ移植していきます。

module CFromRubyExample
  class Helpers
    def self.string(value)
      "String: '#{value}'"
    end
 
    def self.number(value)
      value + 1
    end
 
    def self.boolean(value)
      !value
    end
  end
end

上記のメソッドは、順に「文字列の連結」「数値への1の加算」「真偽値の反転」を行います。

Cライブラリへの移植

以下は、同じ処理をC言語に移植したコードです。文字列フォーマットを使えるよう、C標準ライブラリとI/Oライブラリをインクルードしています。RubyのStringの代わりにchar*を使用します。char*は、メモリ上のどこかにある文字バッファの場所を指すポインタです。

# include <stdlib.h>
# include <stdio.h>
 
char* string_from_library(char* value) {
  char* out = (char*)malloc(256 * sizeof(char));
  sprintf(out, "String: '%s'", value);
  return out;
}
 
int number_from_library(int value) {
  return value + 1;
}
 
int boolean_from_library(int value) {
  if (value == 0) {
    return 1;
  } else {
    return 0;
  }
}

ご覧のとおり、単純な文字列フォーマットひとつとっても、いくつもの手間を踏む必要があります。文字列を連結するには、まずバッファを確保(malloc)し、その後sprintf関数でフォーマット済みの結果を書き込み、最後にバッファを返すという流れです。

また、このコードには早くもクラッシュやセキュリティ問題の可能性が潜んでいます。入力される文字列が245バイトを超えると、恐ろしい「バッファオーバーフロー」が発生してしまうのです。C言語を書く際は細心の注意が必要です。一歩間違えれば、簡単に自分の足を撃ってしまいます。

次に、ヘッダファイルを作成します。

char* string_from_library(char*);
int number_from_library(int);
int boolean_from_library(int);

このファイルは、Cライブラリの公開APIを記述したものです。他のプログラムはこのヘッダファイルを見て、ライブラリ内のどの関数を呼び出せるのかを把握します。

現代的な方法:ffi gemを使う

これで、Rubyから使いたいCライブラリができました。このCコードをgemでラップする方法は2つあります。モダンな方法はffi gemを使うことです。ffi(Foreign Function Interface)を使えば、先ほどのような面倒な作業の多くを自動化できます。先ほど書いたCコードに対してffiを使うと、次のようになります。

module CFromRubyExample
  class Helpers
    extend FFI::Library
 
    ffi_lib File.join(File.dirname(__FILE__), "../../ext/library.so")
 
    attach_function :string, [:string], :string
    attach_function :number, [:int], :int
    attach_function :boolean, [:int], :int
  end
end

ただし本記事では、理解を深めるために、C拡張機能でCコードをラップする方法についても解説します。こちらの方法を知ることで、Rubyの内部で何が起きているのかをより深く把握できるようになります。

C拡張機能でライブラリをラップする

Rubyから使いたいCライブラリの準備ができたので、次はそれをビルドしてラップするgemを作成します。gemを作成したら、まずgemspecのrequire_pathsextを追加します。

Gem::Specification.new do |spec|
  spec.name          = "c_from_ruby_example"
  # ...
  spec.require_paths = ["lib", "ext"]
end

こうすることで、RubyGemsに対して「ネイティブ拡張機能をビルドする必要がある」ことを伝えられます。RubyGemsはextconf.rbというファイル、またはRakefileを探します。今回はextconf.rbを用意しました。

require "mkmf"
 
create_makefile "extension"

mkmfは「Make Makefile」の略で、Rubyに同梱されているヘルパー群です。Cのビルド環境を整える際の厄介な部分を肩代わりしてくれます。create_makefileを呼び出して拡張機能の名前を指定すると、Cコードのビルドに必要な設定とコマンドすべてを含んだMakefileが生成されます。

次に、ライブラリとRubyをつなぐCコードを書きます。ここでは、char*のようなCの型をStringのようなRubyの型に変換する関数をいくつか作成し、さらにCのコードだけでRubyクラスを作成します。

まず、Rubyのヘッダファイルをインクルードします。これにより、型変換に必要な関数が使えるようになります。あわせて、先ほど作成したlibrary.hヘッダファイルもインクルードし、自作ライブラリを呼び出せるようにします。

#include "ruby/ruby.h"
#include "ruby/encoding.h"
#include "library.h"

続いて、ライブラリ内の各関数をラップする関数を作成します。これは文字列用のラッパーです。

static VALUE string(VALUE self, VALUE value) {
  Check_Type(value, T_STRING);
 
  char* pointer_in = RSTRING_PTR(value);
  char* pointer_out = string_from_library(pointer_in);
  return rb_str_new2(pointer_out);
}

最初に、渡されてきたRubyの値が文字列であるかをチェックします。文字列以外の値を処理すると、あらゆる種類のバグを引き起こしかねないためです。次に、Rubyが提供するRSTRING_PTRヘルパーマクロを使って、RubyのStringchar*に変換します。これで自作のCライブラリを呼び出せるようになりました。戻り値のchar*を変換するには、インクルードしたrb_str_new2関数を使用します。数値と真偽値についても、同様のラッパー関数を追加していきます。

数値の場合も考え方は同じで、NUM2INTINT2NUMというヘルパーを使います。

static VALUE number(VALUE self, VALUE value) {
  Check_Type(value, T_FIXNUM);
 
  int number_in = NUM2INT(value);
  int number_out = number_from_library(number_in);
  return INT2NUM(number_out);
}

真偽値版もほぼ同様です。なお、C言語には実際には真偽値型が存在しないため、慣習として0と1で代用する点に注意してください。

static VALUE boolean(VALUE self, VALUE value) {
  int boolean_in = RTEST(value);
  int boolean_out = boolean_from_library(boolean_in);
  if (boolean_out == 1) {
    return Qtrue;
  } else {
    return Qfalse;
  }
}

最後に、すべてを接続してRubyから呼び出せるようにします。

void Init_extension(void) {
  VALUE CFromRubyExample = rb_define_module("CFromRubyExample");
  VALUE NativeHelpers = rb_define_class_under(CFromRubyExample, "NativeHelpers", rb_cObject);
 
  rb_define_singleton_method(NativeHelpers, "string", string, 1);
  rb_define_singleton_method(NativeHelpers, "number", number, 1);
  rb_define_singleton_method(NativeHelpers, "boolean", boolean, 1);
}

そう、お読みのとおりです。C言語のコードから、Rubyのモジュールやクラス、メソッドを作成できるのです。ここではクラスを定義し、そこにRubyのメソッドを追加しています。メソッドを定義する際は、「Ruby側のメソッド名」「呼び出されるCラッパー関数の名前」「引数の個数」を指定する必要があります。

ここまでの作業を終えると、ついにCのコードをRubyから呼び出せるようになります。

CFromRubyExample::NativeHelpers.string("a string")

まとめ

多くの手間をかけましたが、クラッシュさせることなくC拡張機能を動かすことができました。C拡張機能の作成は、決して心臓の弱い人向けではありません。ffi gemを使ったとしても、まだ簡単にRubyプロセスをクラッシュさせてしまう可能性があります。しかし、やり遂げる価値は十分にあります。高性能で安定したCライブラリの世界が、あなたのRubyアプリケーションに広がるのですから!

  1. Rubyで新しいプログラミング言語を作る:インタプリタ編

    GitHubで完全なソースコードを公開中 Stoffleプログラミング言語の完全な実装はGitHubで公開しています。バグを見つけたり、疑問点がある場合は、ぜひIssueを立ててください。 この記事では、Rubyだけで作られたおもちゃのプログラミング言語「Stoffle」のインタプリタ実装を始めます。このプロジェクトについては、シリーズ第1回目の記事で詳しく紹介しているので、まだ読んでいない方はそちらもチェックしてみてください。 今回構築するのは、いわゆる「ツリーを辿るインタプリタ(tree-walk interpreter)」と呼ばれるものです。前回の記事では、フラットなトークンの

  2. 【完全ガイド】USBメモリを使ってWindows 11をゼロからクリーンインストールする方法

    Microsoftは、世界で最も利用されているパソコン向けOS「Windows」の最新バージョンとなるWindows 11を正式リリースしました。Windows 11には、ゲーム機能の強化、洗練されたシンプルなレイアウト、直感的に操作できるショートカットなど、数多くの新機能が搭載されています。Windows 11の最小ハードウェア要件を満たすWindows 10 PCをお持ちであれば、無料でアップグレードすることも可能です。 Windows 11のインストール方法はいくつか存在しますが、本記事では、既存OSからのアップグレードに頼らず、Microsoft公式のISOイメージや起動可能なUSBメ