en ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja en nb ja en ja ja ja ja ja ja ja ja en es en ja ja ja ja ja ja ja ja ja en ja de ru ja ja ja ja ja ja ja ru ja ja ja ja ja ja ja de ru ja en ja ja ja ja ja ja en ja ja ja en ja ja ja ja ja ja ja en ja ja ja ja ja en ja ja ja ja ja en ja en es ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja pl ja ja ja ja ja ja ja en ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
RubyKaigi 2016:How to create bindings 2016 #rubykaigi - 2016-09-14 - ククログ
class Hello def to_s "Hello" end end
この拡張ライブラリーを作るときに用意するものは次の2つで、どちらもこの発表のスライドを管理しているリポジトリーに入っています。
hello.cは実装で次のようになります。コメントで解説を書いています。
/* Rubyが提供する拡張ライブラリーを作るためのAPIを使うため。 */ #include   /* Hello#to_sの実体。 */ /* VALUEはRubyのオブジェクトのCでの表現。 すべてのメソッドはRubyのオブジェクトを返すので、Cでの実装ではVALUEを返す。 */ static VALUE hello_to_s(VALUE self) { /* rb_str_new_cstr()はCの文字列からRubyのStringオブジェクトを作るAPI。 */ return rb_str_new_cstr("Hello"); } /* 初期化関数。 */ /* 「hello.so」を「require」すると、Rubyは「Init_hello」を呼ぶ。 */ void Init_hello(void) { VALUE hello; /* 「class Hello」に対応。 */ hello = rb_define_class("Hello", rb_cObject); /* 「def to_s」に対応。最後の「0」は引数が「0」という意味。 */ rb_define_method(hello, "to_s", hello_to_s, 0); }
拡張ライブラリーを作ったことがない人でもCがわかればなんとなく読めますよね。
これをビルドするためのMakefileを作るのがextconf.rbです。
# Makefileを作るための便利ライブラリーを使う。 require "mkmf" # 「hello.so」を作るMakefileを生成。 create_makefile("hello")
このhello.cとextconf.rbを用意すると次のようにビルドできます。
% ruby extconf.rb % make
これを使うには次のようにします。
% irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。このときにInit_hello()が呼ばれる。 => true >> hello = Hello.new => #  >> hello.to_s # Cで実装したhello_to_s()が呼ばれる。 => "Hello"
Rubyで書いた実装と同じように使えますね。
それでは拡張ライブラリーとしてバインディングを実装する方法を説明します。
次のhello.hのようなAPIのCライブラリーのバインディングを作成することにします。
#pragma once typedef struct hello_t Hello; /* コンストラクター */ Hello *hello_new (void); /* デストラクター */ void hello_free (Hello *hello); /* メソッド */ const char *hello_message(Hello *hello);
このライブラリーのバインディングを拡張ライブラリーとして作るときに用意するものは次の2つで、どちらもリポジトリーに入っています。
このバインディングはRubyで書くと次のようなクラスを実現します。
class Hello def initialize @message = "Hello" end def message @message end end
hello.cは実装で次のようになります。コメントで解説を書いています。前述の「バインディングではない拡張ライブラリー」との大きな違いはライブラリー対象のHello構造体をラップしているかどうかです。増えているコードの多くはこのためのコードです。
/* Rubyが提供する拡張ライブラリーを作るためのAPIを使うため。 */ #include   /* バインディング対象のライブラリーのAPIを使うため。 */ #include   /* ラップしている`Hello`構造体を開放する関数。 */ static void rb_hello_free(void *data) { Hello *hello = data; /* ライブラリーが提供しているデストラクターを呼ぶ。 */ hello_free(hello); } /* Rubyにどのような構造体をラップしているかを伝えるための情報。 */ static const rb_data_type_t rb_hello_type = { "Hello", { NULL, rb_hello_free, /* ラップしている構造体を開放する関数。↑で定義。 */ NULL, }, NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY, }; /* 構造体をラップするオブジェクトを新しく作る関数。 */ /* Hello.newの中で暗黙的に呼ばれる。 */ static VALUE rb_hello_alloc(VALUE klass) { /* ↑の「どのような構造体をラップしているか」情報を使ってオブジェクトを生成。 */ /* この時点ではラップ対象の構造体はまだ存在しないため最後の引数はNULL。 */ return TypedData_Wrap_Struct(klass, &rb_hello_type, NULL); } /* Hello#initializeの実装。 */ static VALUE rb_hello_initialize(VALUE self) { Hello *hello; /* ラップ対象の構造体をコンストラクターを使って生成。 */ hello = hello_new(); /* ラップ対象の構造体を設定。 */ DATA_PTR(self) = hello; return Qnil; } /* Hello#messageの実装。 */ static VALUE rb_hello_message(VALUE self) { Hello *hello; const char *message; /* ↑の「DATA_PTR(self) = hello」で指定した「hello」を取得。 */ /* ↑の「どのような構造体をラップしているか」情報は引数チェックのために利用。 */ TypedData_Get_Struct(self, Hello, &rb_hello_type, hello); /* ライブラリーが提供しているメソッド用の関数を呼び出す。 */ message = hello_message(hello); /* 結果(Cの文字列)をRubyのオブジェクトに変換して返す。 */ return rb_str_new_cstr(message); } /* 初期化関数。 */ /* 「hello.so」を「require」すると、Rubyは「Init_hello」を呼ぶ。 */ void Init_hello(void) { VALUE hello; /* 「class Hello」に対応。 */ /* 親クラスが「rb_cData」にすることがポイント。 */ hello = rb_define_class("Hello", rb_cData); /* 構造体をラップするオブジェクトを新しく作る関数を Hello.newの中で暗黙的に呼ばれるようにする。 */ rb_define_alloc_func(hello, rb_hello_alloc); /* 「def initialize」に対応。最後の「0」は引数が「0」という意味。 */ rb_define_method(hello, "initialize", rb_hello_initialize, 0); /* 「def message」に対応。最後の「0」は引数が「0」という意味。 */ rb_define_method(hello, "message", rb_hello_message, 0); }
量が増えているのは構造体をラップするためです。この状態から新しくメソッドを追加する場合に増えるコード量は「バインディングではない拡張ライブラリー」とほとんど変わりません。そのため、最初だけコードが多くなりますが、その後はそれほど多くなりません。
これをビルドするためのMakefileを作るextconf.rbは次のようになります。バインディング対象のlibhello.soを検出するためのコードが増えています。
# Makefileを作るための便利ライブラリーを使う。 require "mkmf" # libhello.soとhello.hを見つけるための引数を受けつける。 # --with-libhello-XXXという引数を指定できるようになる。 dir_config("libhello") # hello.hを探す。 exit(false) unless have_header("hello.h") # libhello.soを探す。「hello_new」があるかもチェックする。 exit(false) unless have_library("hello", "hello_new") # 「hello.so」を作るMakefileを生成。 create_makefile("hello")
このhello.cとextconf.rbを用意すると次のようにビルドできます。バインディング対象のlibhello.soの実装はlibhelloとしてリポジトリーに入っています。hello.hは/tmp/local/include/hello.hにインストールされ、ビルドされたlibhello.soは/tmp/local/lib/libhello.soにインストールされているとします。
% ruby extconf.rb --with-libhello-dir=/tmp/local % make
これを使うには次のようにします。
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。このときにInit_hello()が呼ばれる。 => true >> hello = Hello.new # rb_hello_initialize()経由でhello_new()を呼ぶ。 => #  >> hello.message # rb_hello_message()経由でhello_message()を呼ぶ。 => "Hello"
Rubyのリポジトリーには拡張ライブラリーを作り方を説明したdoc/extension.ja.rdocが入っているので、このドキュメントも参照すると捗るでしょう。
SWIGはラッパーを生成するツールです。Ruby専用のツールではありませんが、拡張ライブラリーとして実装されたRubyのバインディングを生成することができます。
SWIGは.iファイル(「i」は「interface」の「i」)を入力としてCのソースを出力します。.iファイルの中にはどのようにバインディングを生成するかという情報を記述します。
前述のhello.hのようなAPIのCライブラリーのバインディングを作成する場合は次のようなhello.iになります。
/* Helloモジュール以下にクラスやメソッドを定義する。 */ %module hello %{ /* 出力したCのソースでhello.hをinlcudeする。 */ #include   %} /* SWIGがhello.hをパースして関数を検出してバインディングを生成する。 */ /* 「#」includeではなく「%」includeなことに注意。 */ %include
これでhello_wrap.cという名前の2000行強のCプログラムが生成されます。このhello_wrap.cをビルドするためのMakefileを作るextconf.rbは次のようになります。hello.iからhello_wrap.cを生成するためのコードが増えています。
# Makefileを作るための便利ライブラリーを使う。 require "mkmf" # libhello.soとhello.hを見つけるための引数を受けつける。 # --with-libhello-XXXという引数を指定できるようになる。 # swigコマンドにヘッダーファイルがある場所を指定するので # dir_configの戻り値にinclude_dirと名前を付けている。 include_dir, library_dir = dir_config("libhello") # hello.hを探す。 exit(false) unless have_header("hello.h") # libhello.soを探す。「hello_new」があるかもチェックする。 exit(false) unless have_library("hello", "hello_new") # この時点ではまだhello_wrap.cが存在しないため、 # mkmfはソースとして認識できない。そのため、手動で設定する。 $srcs = ["hello_wrap.c"] # 「make clean」時に削除される設定も手動で行う。 $cleanfiles << "hello_wrap.c" # 「hello.so」を作るMakefileを生成。 create_makefile("hello") # Makefileにはhello.iからhello_wrap.cを生成するルールがないので追記する。 File.open("Makefile", "a") do |makefile| makefile.puts(<<-MAKEFILE) hello_wrap.c: hello.i # -rubyを指定してRuby用のコードを生成する。 swig -Wall -I"#{include_dir}" -ruby -o $@ $< MAKEFILE end
このhello.iとextconf.rbとSWIGを用意すると次のようにビルドできます。バインディング対象のlibhello.soの実装はlibhelloとしてリポジトリーに入っています。hello.hは/tmp/local/include/hello.hにインストールされ、ビルドされたlibhello.soは/tmp/local/lib/libhello.soにインストールされているとします。
% ruby extconf.rb --with-libhello-dir=/tmp/local % make
これを使うには次のようにします。
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。 => true >> hello = Hello.hello_new # hello_new()を呼ぶ。 => #  >> Hello.hello_message(hello) # hello_message()を呼ぶ。 => "Hello" >> Hello.hello_free(hello) # hello_free()を呼ぶ。 => nil
Cの関数をそのまま呼べるようなバインディングが自動生成されています。自動生成なので関数が多くても手間がかかりませんが、使い勝手はよくありません。hello.messageのように書けなかったり、GCにメモリーの解放を任せるのではなく手動でHello.hello_free(hello)を実行しないといけないからです。
使いやすくする方法は2つあります。
1つはRubyで使いやすいラッパーを作る方法です。たとえば次のようなコードを用意します。ObjectSpace.define_finalizerを使って自動的に解放するようにしているところとWrappedHello#messageでラップしているところがポイントです。
require "hello.so" class WrappedHello class << self def finalizer(hello) lambda do |id| Hello.hello_free(hello) end end end def initialize @hello = Hello.hello_new ObjectSpace.define_finalizer(self, self.class.finalizer(@hello)) end def message Hello.hello_message(@hello) end end
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。 => true >> hello = WrappedHello.new # ラップしたバージョンを使う。 => # > >> hello.message # ラップしたバージョンを使う。 => "Hello"
もう1つの方法は.iファイルでオブジェクト指向なAPIにする方法です。たとえば、次のようなhello.iにします。
/* Helloモジュール以下にクラスやメソッドを定義する。 */ %module hello %{ /* 出力したCのソースでhello.hをinlcudeする。 */ #include   %} /* Hello::Helloクラスを定義する。 */ typedef struct hello_t { %extend { /* Hello::Hello#initializeで呼ぶCの関数を定義する。 */ /* C++のコンストラクターと同じ構文。 */ hello_t() {return hello_new();} /* Hello::HelloオブジェクトがGCされたときに呼び出す関数を定義する。 */ /* C++のデストラクターと同じ構文。 */ ~hello_t() {hello_free($self);} /* Hello::Hello#messageで呼ぶCの関数を定義する。 */ const char *message() { return hello_message($self); } } } Hello;
extconf.rbは前述のものと同じです。そのためビルド方法も同じです。
% ruby extconf.rb --with-libhello-dir=/tmp/local % make
使い勝手は次のようになります。オブジェクト指向なAPIになっています。
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" => true >> hello = Hello::Hello.new => #  >> hello.message => "Hello"
Ruby FFIはRubyでバインディングを作れるようにするライブラリーです。libffiを使って実現しています。
拡張ライブラリーでバインディングを実装するよりも非常に短い記述になります。前述のhello.hのようなAPIのCライブラリーのバインディングは次のhello.rbのようになります。
# Ruby FFIを読み込む。 require "ffi" module LibHello # このモジュールでRuby FFIを使う。 extend FFI::Library # バインディング対象の共有ライブラリーを指定する。 ffi_lib "/tmp/local/lib/libhello.so" # 「Hello *hello_new(void)」に対応。 attach_function :hello_new, [], :pointer # 「const char *hello_message(Hello *hello)」に対応。 attach_function :hello_message, [:pointer], :string # 「void hello_free(Hello *hello)」に対応。 attach_function :hello_free, [:pointer], :void end
拡張ライブラリーと違ってビルドする必要はないのですぐに使えます。使い勝手は次のようになります。
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。 => true >> hello = LibHello.hello_new # hello_new()を呼ぶ。 => #  >> LibHello.hello_message(hello) # hello_message()を呼ぶ。 => "Hello" >> LibHello.hello_free(hello) # hello_free()を呼ぶ。 => nil
単純にSWIGを使った場合と同じくCの関数をそのまま呼べるだけなので使い勝手はよくありません。
使い勝手をよくするには次のようなコードを使ってラップします。SWIG用のラッパーと同じことをしています。
class Hello def initialize # GC時に自動で解放してくれる便利オブジェクトでラップ。 # 内部ではObjectSpece.define_finalizerを使っている。 @hello = FFI::AutoPointer.new(LibHello.hello_new, LibHello.method(:hello_free)) end def message LibHello.hello_message(@hello) end end
このラッパーを使うと次のような使い勝手になります。
% LD_LIBRARY_PATH=/tmp/local/lib irb -I . --simple-prompt >> require "hello" # 「hello.so」を読み込む。 => true >> hello = Hello.new # ラップしたバージョンを使う。 => # > >> hello.message # ラップしたバージョンを使う。 => "Hello"
オブジェクト指向なAPIになりました。
GObject Introspection
GObject IntrospectionはCで実装されたライブラリーの言語バインディングを簡単に実装できるようにするライブラリー・ツール群です。SWIGと同じように様々な言語に対応したソフトウェアです。「Ruby専用」ではなく「Rubyでも使える」ソフトウェアです。
バインディング対象のライブラリーがGObject Introspectionに対応している場合(たとえばWebブラウザーライブラリーであるWebKitGTK+)はgobject-introspection gemを使うと次のようにすればバインディングができあがります。(このAPIは次回リリースの3.1.0から使えるようになる予定です。そのため、最新リリースの3.0.9では動きません。)
# gobject-introspection gemを読み込む書き方の1つ。短いバージョン。 require "gi" # GObject Introspectionを使ってWebKitGTK+のバインディングを生成する。 # Moduleが返ってくるので「WebKit」という定数に設定。 WebKit = GI.load("WebKit2") # ↑は以下の書き方のシンタックスシュガー。 # module WebKit # end # loader = GObjectIntrospection::Loader.new(WebKit) # loader.load("WebKit2")
次のように使えます。自動生成なのに自然に使えるようになっていることがわかります。
% irb --simple-prompt >> require "gi" # gobject-introspection gemを読み込み。 => true >> WebKit = GI.load("WebKit2") # WebKitGTK+のバインディングを作成。 => WebKit # WebKitGTK+のバインディングの動作を確認するためにGTK+ 3を利用。 # GTK+ 3はGUIを提供するライブラリー。 >> require "gtk3" => true # GTK+ 3のウィジェットを表示するにはイベントループを回す必要がある。 # irb上でブロックせずにイベントループを回すために別スレッドでイベントループを実行。 >> Thread.new {c = GLib::MainContext.default; loop {sleep 0.01 unless c.iteration(false)}} => #  # ウィンドウを作成。 >> w = Gtk::Window.new => #  # ウィンドウを表示。 >> w.show => #  # Webブラウザーウィジェットを作成。 # GI.load("WebKit2")で作成したバインディング。 >> web_view = WebKit::WebView.new => #  # ウィンドウにWebブラウザーウィジェットを追加。 >> w   #  # Webブラウザーウィジェットを表示。 >> web_view.show => #  # Webブラウザーウィジェットでページを表示。 # GI.loadが引数の処理を自動で実装していることを確認できる。 >> web_view.load_uri("http://rubykaigi.org/") => #  # 現在表示しているページのスクリーンショットを取得。 # 非同期で取得するのでブロックで処理完了時の処理を指定。 # GI.loadがenumの値を名前(:full_documentと:none)で指定できるような # バインディングを生成している。1や2のように数値で指定しなくてもよい。 # GI.loadがブロックに対応したバインディングを生成している。 >> snapshot = nil; web_view.get_snapshot(:full_document, :none) {|_, result| snapshot = web_view.get_snapshot_finish(result)} => #  # スクリーンショットとして返ってきたオブジェクト(Cairo::ImageSurface)は # GI.loadで作成されていないクラスのオブジェクトだが連携できている。 >> snapshot => #  # ↑で:full_documentと指定していたものは # WebKit::SnapshotRegion::FULL_DOCUMENTに相当する。 >> WebKit::SnapshotRegion::FULL_DOCUMENT => #
このようなオブジェクト指向なAPI、名前でenumの値を指定する、ブロック対応(、この例では出てきていない例外)などに対応するためにはGObjectというCで実装されたオブジェクトシステムを利用する必要があります。(WebKitGTK+は利用しています。)バインディングを作りたいライブラリーがGObjectを利用していない場合はGObjectを使うようにしたラッパーを作成すればGObject Introspectionを使えるようになります。たとえば、GroongaはGObjectを使っていませんが、GObjectを使ったラッパーであるGroonga GObjectを使うと、GObject IntrospectionでGroongaのバインディングを使えます。
GObject Introspectionの重要なポイントは、共有ライブラリーだけでなく、共有ライブラリーのメタデータも利用するところです。メタデータとはたとえば関数のシグネチャー(名前・引数・戻り値の情報)です。Ruby FFIでは共有ライブラリーしか利用していないため関数のシグネチャーをバインディング開発者が指定(attach_functionで指定)していましたが、GObject Introspectionの場合はそれを利用できるのでバインディング開発者は指定する必要はありません。このため、自動でいい感じのバインディングを生成できるのです。
GObjectがGTK+用(もう少し言うとGIMP用)で利用するために開発されているライブラリーのため、GObjectを活用しているGObject IntrospectionもGUI用(GTK+はGUI用のライブラリー)の仕組みと勘違いする人がいます。しかし、GObjectはGUIに関係ない下回りの機能を提供するライブラリーなため、GUIを使わないライブラリーでも利用されています。たとえば、PDFを処理するPoppler、PNGやJPEGなどを含む様々な画像を統一的に処理する機能を提供するGdkPibuf、マルチメディアを処理するGStreamerなどはGUIを使わない(必須ではない)ライブラリーですが、GObjectを利用しています。つまり、GObject IntrospectionはGUIを使わないサーバー上でも活用できる仕組みです。
GObject Introspectionに対応したライブラリーの作り方はGObject Introspectionに対応したライブラリーの作り方を参考にしてください。今回の発表のためにlibgobject-helloとlibgobject-calcという実装も用意しました。これらも参考になるはずです。
実際に作ろうとしてつまづいた人はruby-gnome2/ruby-gnome2のissuesで相談してください。日本語で構いません。
RubyKaigi 2016でバインディングの作り方を紹介し、バインディング開発者を増やそうとしました。バインディング開発者が増えるとRubyを活用できる場面が広がると考えているからです。
発表中ではバインディングの作り方の詳細を省略したのでここで補足しました。
Windowsでのバインディングのインストールを簡単にするための仕組みとしてfat gemというものがあり、fat gemの説明資料も用意していたのですが、今回のRubyKaigi中では説明する機会がありませんでした。いつか、機会があれば。。。Rubyの拡張ライブラリーやバインディングに特化したイベントを開催するときはぜひ声をかけてください。
発表中に参加者に「バンディングという概念を知っていたか?」と聞いたところ、参加者の半分以上は知らないということでした。「拡張ライブラリー」と合わせてバインディングの情報を提供していった方がRubyを活用できる場面が広がりそうです。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
RubyKaigi 2016にスポンサーとして参加 #rubykaigi #oss_gate
RubyKaigi2016: Ruby Reference Manual 2016 Autumn #rubykaigi
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。