C++で作るRuby拡張

はじめに

この記事は「Ruby Advent Calendar 2019」の8日目の記事です。

C++でのRuby拡張実装について、つらつらと書いている記事になります。

内容としてはTataraというRubyで型を使えるライブラリを作ってみたで紹介した自作Ruby拡張を作るにあたって得たC++でのRuby拡張実装知見の記事になります。

Ruby拡張って?

皆さんが普段使っているRuby(ここではCRubyのことです)はCによって実装されています。ですので、Cを使ってRuby拡張機能を作成することもできます。

つまり、Cで既に作成されているライブラリなどをRuby拡張として作成することができるというメリットがあります。CでRuby拡張を実装した場合、Rubyで実装するよりも高速に処理できるケースもあるようです。

実際にCで拡張機能が実装されているgemとしてはsqlite3mysql2などがあります。

またRustやC++Ruby拡張機能を作成するケースもあります。

例えば最近面白いなぁと思ったのはRustでのRuby拡張を実装できるHelixですね。HelixRustを使うことでCやC++よりも安全にRuby拡張を書くことができます。

また実装コード自体もかなり読みやすく以下のようなコードでクラスとメソッドを実装できます(※ HelixのREADMEより引用)。

ruby! {
    class Console {
        def log(string: String) {
            println!("LOG: {}", string);
        }
    }
}

ただHelix公式のチュートリアルではRails向けに拡張機能を実装する内容になっています。そのためRuby向けの拡張を作成する際のドキュメントがあまりなく、少し辛いところがあります。

実際にHelixでRuby拡張を作成しているものとしては以下の記事などがあります。

ref: Rustでgemを書く際のハマりどころ in 2017

ref: Writing Ruby gems with Rust and Helix

またC++ではRiceExt++などのRuby拡張を実装できるライブラリも存在しています。

RubyKaigi 2017ではImprove extension API: C++ as better language for extensionにてC++でのRuby拡張実装について紹介されています。

興味のある方はそちらも確認してみると良いでしょう。

今回はC++でのRuby拡張の実装方法について解説します。具体的にはRiceExt++C++のみでの実装方法などを解説していきます。

つくるもの

今回は、Helloというクラスを作成し、Hello Ruby Extension!と画面に表示するsayというメソッドを実装します。

具体的には以下のようなコードが実行できるRuby拡張を実装していきます。

require 'hello'

Hello.new.say
# => "Hello Ruby Extension!"

今回はRiceExt++C++でそれぞれ実装していきます。

今回の記事作成にあたって各ライブラリでの実装サンプルをGitHubに上げておきました。興味のある方はこちらも見ると良いかも。

S-H-GAMELINKS/RubyAdventCalendarExtensionSample

実装

Riceでの実装

Riceとは?

Riceとは、C++を使ってRuby拡張を簡単に作成できるライブラリになります。

RiceはgemとしてRubyGemsからインストールすることができます。

gem install rice

これでRiceが使えるようになります!

ちなみに、実際にRiceを使ったサンプルコードは以下のようになります。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

このようにRiceを使う場合、非常に簡単にRuby拡張を作ることができます。

またC++のテンプレートなどを使って以下のようなコードを書くこともできます。

template <class T>
class CppArray {
    public:
        CppArray<T>() {};
};

Data_Type<CppArray<int>> rb_cIntArray = define_class<CppArray<int>>("IntArray")
    .define_constructor(Constructor<CppArray<int>>());

Riceを使うメリットととしては、非常に簡単にC++でのRuby拡張を作ることができる点ですね。C++のライブラリなどをRubyで使えるようにするラッパーなどは、Riceを使って実装するといいかもしれません。

デメリットとしては、日本語のドキュメントもあまりないことと、開発自体があまり活発でない印象があることですね。

日本語で書かれた記事はあまり(Rice以外での実装とかはあったりする)なく、IBMRice を使用して Ruby の拡張機能を C++ で作成するが日本語で唯一詳しく書かれたRiceのチュートリアルになりそうです、

英語が読める方であれば、こちらのドキュメントを読み解けばよいかと思います。

GitHubリポジトリでのコミットログなどを見るた印象ではあまり開発が活発な印象はないです。最近、いくつかPull Requestが取り込まれてはいるようですが……。 そのため、Rice側の開発が打ち切られると辛いことになりそうな気配がありますね……。

とはいえ、大きな変更が入る可能性は少ないのでとりあえずC++でのRuby拡張を作る分には良いライブラリだと思います。

実装

それでは、Riceを使ってRuby拡張を実装してみましょう。

なにはともあれ、Riceをインストールしましょう。

gem install rice

インストールが無事終了した後は、extconf.rbというファイルを作成します。これはC++のコードをビルドするMakefileを自動生成するためのファイルになります。CでRuby拡張を作る場合も同様にextconf.rbを作成します。

require 'mkmf-rice'

create_makefile('hello')

mkmf-riceはRiceを使ってかかれたC++のソースをもとにMakefileを作成するためのライブラリになります。ちなみに、Cで拡張機能を実装する場合はmkmfというライブラリを読み込んでMakefileを自動生成していますね。

またcreate_makefileに渡している文字列がビルドされた拡張ライブラリの名前になります。

次に、hello.cppextconf.rbと同じ階層に作成します。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

軽くコードの解説をすると、以下の二行でRiceのヘッダーを読み込んでいます。

#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

RiceではData_Typeを使い、既存のクラスをもとにRuby向けにコンバートしています。

Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")

上記のコードではC++で定義したHelloクラスをRubyで呼び出すHelloというクラスに変換しています。

.define_constructor(Constructor<Hello>())

.define_constructor(Constructor<Hello>())ではC++で定義したHelloクラスのコンストラクタ(Rubyでいうところのinitializeのようなもの)を使って、RubyHelloクラスのインスタンスを作成できるようにしています。 つまり、Rubyのinitializeを実装しています。

最後に.define_method("say", &Hello::say);sayというメソッドをHelloクラスに追加しています。

.define_method("say", &Hello::say);

これでC++側での実装は完了です。

次に、extconf.rbを実行してMakefileを生成します。

ruby extconf.rb
# => Makefileを自動生成

あとは、makeコマンドでビルドすればhello.ohello.soが生成されていると思います。

make
# => hello.o と hello.so が生成される

最後に、作成したRuby拡張を実際に動かしてみましょう。hello.rbを以下のように作成して実行してみましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

Ext++での実装

Ext++とは?

Ext++はRice同様にC++を使って、Ruby拡張を作成できるライブラリです。

Ext++もRubyGemsで配布されているのでgemとしてインストールできます。

gem install extpp

Ext++での実装は以下のようになります。

#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではC++ラムダ式を引数に渡して実装することができる点が特徴的です。そのためラムダ式をうまく使うことでRubyのメソッドとC++の実装を一度に書くことができます。

また、Ext++ではruby.hppをインクルードするだけで良いところも便利です。Riceの場合、必要なヘッダーを個別に読み込まなければならず

Riceではラムダ式を使ってメソッドの定義などはできないため、ラムダ式でメソッドを定義したい人はExt++を使うと良いかもしれません

Ext++を使うメリットとしては、実装が一か所で済む点かなと思います。また開発者が日本の方(というか @kou さん)ですので開発者本人にあって話が聴けるという点もメリットかもしれません。

デメリットとしては、サンプルのコードが一つしかなく、人によってはどのように実装を進めていけばいいのかが分かりにくい時がある点でしょうか?その点に関しては今後Pull Requestなどでサンプルコードを投げれたらと思っていますね。 また開発バージョンであり、今後のバージョンアップでは大きな変更も入る可能性もありそうです。

しかしながら、開発者本人に直接話を聞くことができそう(日本人からすると)なので採用するメリットはかなり大きいと思います。

またRiceと違い、Rubyの実装自体に近い実装コードを書くのでCRubyの実装を学んでみたいという人にもオススメかもしれませんね。

実装

それでは、Ext++を使ってRuby拡張を実装していきましょう。

まずはExt++をインストールします。

gem install extpp

インストール完了後、Riceでの実装の時と同じようにextconf.rbを作成します。

require 'extpp'

create_makefile('hello')

Riceの時とおおよそ同じコードですね。違う点としてはmkmf-riceではなく、extppを読み込んでいます。

次に、hello.cppextconf.rbと同じ階層に作成します。

#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではrb::Classで新しいクラスを作成します。また、作成したklassdefine_methodを使うことで必要なメソッドを新しく定義しています。

QnilRubyでのnilを返しています。CRubyのメソッドなどでnilが返ってきているメソッドでは、子のようにreturn Qnil;と書かれています。興味のある方はRuby Hack Challenge Holidayに参加したり、GitHubruby/rubyのコードを読んでみると良いかもしれません。

あとは。extconf.rbを実行し、Makefileを生成します。

ruby extconf.rb
# => Makefileが生成される

その後、makeで作成したRuby拡張をビルドします。

make
# => hello.o と hello.soが生成される

最後にhello.rbを以下のように作成し、実行してみましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

C++での実装

実装

最後にC++でのみで作成するRuby拡張について紹介します。

まずはextconf.rbを作成します。

require "mkmf"

create_makefile("hello")

mkmfはCRubyに添付されているRuby拡張のためのMakefile作成ライブラリですね。

次に、hello.cppを以下のように作成します。

#include <ruby.h>
#include <iostream>

class Hello {
    public:
        Hello() {};
        ~Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

static Hello* get_hello(VALUE self) {
    Hello *ptr;
    Data_Get_Struct(self, Hello, ptr);
    return ptr;
}

static void wrap_hello_free(Hello *ptr) {
    ptr->~Hello();
    ruby_xfree(ptr);
}

static VALUE wrap_hello_alloc(VALUE klass) {
    void *ptr = ruby_xmalloc(sizeof(Hello));
    ptr = std::move(new(Hello));
    return Data_Wrap_Struct(klass, NULL, wrap_hello_free, ptr);
}

static VALUE wrap_hello_init(VALUE self) {
    return Qnil;
}

static VALUE wrap_hello_say(VALUE self) {
    get_hello(self)->say();
    return Qnil;
}

extern "C" {
    void Init_hello() {
        VALUE rb_cHello = rb_define_class("Hello", rb_cObject);

        rb_define_alloc_func(rb_cHello, wrap_hello_alloc);
        rb_define_private_method(rb_cHello, "initialize", RUBY_METHOD_FUNC(wrap_hello_init), 0);
        rb_define_method(rb_cHello, "say", RUBY_METHOD_FUNC(wrap_hello_say), 0);
    }
}

ポイントとしては#include <ruby.h>Ruby拡張の実装で使用するマクロや関数などを呼び出している点ですね。これがないとRuby拡張を実装することができません。

またget_hello関数はRubyインスタンスを引数に受け取って、C++インスタンスのポインタを返しています。この関数を使うことでC++のクラスのメソッドをラップ関数から呼び出して使うことができるようになります。

wrap_hello_free関数はRubyGCが呼び出された際にメモリから解放する際の処理がかかれた関数になります。

wrap_hello_allocインスタンスを作成する際のアロケータになります。wrap_hello_initRubyでのinitializeになりますね。

あとは、extconf.rbを実行し、makeを実行してビルドしてみましょう

ruby extconf.rb
# => Makfileが生成される

make
# => hello.o と hello.so が生成される

最後に、hello.rbを以下のように作成して実行しましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

おわりに

C++でのRuby拡張についてRice、Ext++、C++それぞれでのでの実装を紹介しました。意外と簡単そうと思っていただければ幸いです。

あと今回の記事ではC++をベースに紹介しましたが、もちろんCでの実装を行う方法もあります。むしろ、そちらのほうが参考になる記事が多いので、Ruby拡張を作る際にはCで作ると良いかもしれません。

あと、この記事でRubyの実装に興味を持たれた方はRuby Hack Challenge Holidayなどに参加してみると良いかもしれません。

意外と簡単にC++でもRuby拡張機能を作ることができるので、今後もC++の良さげなライブラリなどをRuby向けに実装していきたいと思います。

参考記事

ref: Rice ref: Ext++ ref: Improve extension API: C++ as better language for extension ref: Rice を使用して Ruby の拡張機能を C++ で作成する ref: Rice - Ruby Interface for C++ Extensions ref: ko1/rubyhackchallenge ref: ruby/ruby ref: C++言語で簡単なRuby拡張ライブラリを書いてみた ref: Rubyの拡張ライブラリの作り方 ref: Rubyソースコード完全解説 ref: TataraというRubyで型を使えるライブラリを作ってみた