Ruby.EXT.2

ENTRANCEINFORMATION | DIARY | LABORATORY | LINK
CAGE(一般掲示板)Developper's Nest(開発掲示板)こころ宙


目次


拡張モジュールの作り方とクラスの宣言

 さて、ここからは Ruby の拡張モジュールをどのように作成していくか、その具体的な方法について説明していこうと思います。いろいろな話があるのですけれども、まず最初は クラスの作り方 から始めていこうと思います。いくつか、まだ意味や役割を説明していないようなものが多く現れると思いますが、とりあえずは「そういうものなのか」と思いながら先へ読み進めてください。

 クラスは、インスタンスを生成するときに使われる雛形のようなものです。Ruby はオブジェクト指向のスクリプト言語ですから、このクラスの扱いというものが重要な位置を占めています。
 さて、Ruby スクリプトにおいて、クラスを宣言する場合はどのようにすればよいでしょうか?

class SampleClass
end

 上のように書きますね。とりあえず、中身は無いままにしておきましょう。
 今は拡張モジュールについての話をしているので、次に考えることは「これをどのように拡張モジュールで実現するか?」ということになります。そもそも「可能なのか?」という疑問があるかもしれませんが、拡張モジュールは Ruby の一部であるようなものですので、できなければ話になりません。拡張モジュール内で定義されたクラスも、他の Ruby スクリプトで定義されたかのように、普通に扱うことができます。これに関しては、また後で実例も掲載することにしましょう。

 コードを示す前に、最初ですので、まず拡張モジュールの作り方から説明していきます。
 まず、適当な空のディレクトリを作りましょう。

$ mkdir ext-test-1
$ cd ext-test-1

 ここに extconf.rb というスクリプトファイルを作成します。名前は何でも良いのですが、拡張モジュール (ext) の設定 (conf) を行う Ruby スクリプト (rb) ということで、この名前にしています。

require 'mkmf'
create_makefile('test')

 最初に mkmf というライブラリを読み込んでいますが、これは拡張モジュール用の Makefile を生成するためのユーティリティライブラリです。このライブラリによって提供されるメソッド create_makefile() を使って Makefile の生成を行います。引数で渡している文字列は、拡張モジュールの識別子です。ファイル名にも使われます。
 これで下準備は完了です。拡張モジュールの識別子は、この時に決めておきましょう。後で使うことになりますからね。

 さて、次は拡張モジュールのソースコードを記述していきます。
 まず、何はともあれ Ruby のヘッダファイルを読み込みます。インクルードディレクトリは extconf.rb が生成する Makefile に書き込まれているので、とくに気にする必要はありません。ただし、Ruby をソースレベルでインストールしていない場合は問題になりますので、そのような場合は今のうちにインストール作業を行っておきましょう。

#include "ruby.h"

 次に、この拡張モジュールのエントリポイントを作成します。エントリポイントとは、この拡張モジュールがディスクからロードされたときに呼び出される関数のことです。C 言語では main() 関数が最初に呼び出されますが、それと同じようなものです。
 このエントリポイントの名前には、先ほど決めた拡張モジュールの識別子が使われます。たとえば、上の例では "test" を識別子にしています。そのため、エントリポイントは Init_test() という関数になります。一般には、Init_拡張モジュールの識別子() という関数名になります。

void Init_test ( void )
{
}

 中身は後で書くので、今はここまでにしておきましょう。

 ここまでが、Ruby の拡張モジュールを作成する下準備です。どんなものを作成するときでも、ここまでの作業はだいたい同じものになるでしょう。ここから後では、これらの説明を省略して突然エントリポイントから書き始めたりするので、よく憶えておいてください。

 では、クラスの定義を行います。ファイル名は何でも構いませんが、test.c とでもしておきましょう。

#include "ruby.h"

VALUE cls_SampleClass;

void Init_test ( void )
{
    cls_SampleClass = rb_define_class("SampleClass", rb_cObject);
}

 最初の VALUE 型は、Ruby と C 言語の間でデータをやり取りするためのものです。グローバル変数にしていますが、他の場所で使わないのであれば Init_test() 内のローカル変数にしてしまっても構いません。それに関する話は、また後で…。
 さて、Ruby クラスの宣言は RubyAPI の rb_define_class() で行います。この関数は引数で渡されたオブジェクトを継承し、指定された名前のクラスを宣言します。そして、作成されたクラスオブジェクトを返値として戻してきます。Ruby は「ちゃんとした」オブジェクト指向の言語ですから、クラスそのものもオブジェクトとして扱われるのです。

VALUE rb_define_class (const char * classname, VALUE ancestor)
classname 宣言するクラスの名前。
ancestor 親クラスとなるオブジェクト。(これもクラスでなければならない)
よく使われるものとして、
  • 自分で定義したクラス
  • rb_cObject (組み込みの Object クラス)
  • rb_cData (組み込みの Data クラス)
などがあります。
rb_cObject などはグローバル変数で、ruby.h で宣言されています。
返値 宣言されたクラスオブジェクト。

 他にもクラスを宣言する API はあるのですが、今はこのコードを実際に動作させるところから行います。後でまた取りあげるので、その時に。
 では、extconf.rb を動かして、make します。

$ ruby extconf.rb
creating Makefile
$ make
gcc ...

 構築に成功すると、test.so というファイルが作成されているはずです。これを Ruby で利用するには、require() メソッドを用います。

$ ruby -e 'require "test"; p SampleClass.type'
Class

 簡単に動作を確認するため、Ruby にコマンドラインで直接スクリプトを渡して実行しています。まず、最初の require() で拡張モジュール test が読み込まれます。ただし、test.rb というファイルもある場合、そちらが先に読み込まれてしまいます。必ず拡張モジュールを読み込む、と決まっている場合は、require "test.so" のように、.so を加えます。このようにすれば、必ず拡張モジュールが読み込まれます。
 次の p() では、拡張モジュールで宣言されたクラス SampleClass が使用されています。SampleClassObject を継承しているので、そこで定義されているメソッド type() を使用することができます。メソッド type() はそのオブジェクトのクラスを返すので、SampleClass オブジェクトのクラス、すなわち Class が返されて表示されているわけです。

 クラスの宣言に関しては、ひとまずここで留めておきます。次は、宣言されたクラスに対してメソッドを追加していく方法について解説します。

(2002/10/26)

メソッドの定義

 クラスだけを宣言しても、仕方がありません。クラスにはメソッドが必要です。
 というわけで、次はクラスにメソッドを追加していく方法について説明します。

 そもそも、メソッドとは何でしょうか?
 メソッドは関数に似ていますが、関数とはちょっと違います。それは、あるオブジェクトの コンテキスト で動作するという点です。あるメソッドが実行されると、その内部での self 変数がメソッドを保有しているオブジェクトに設定され、インスタンス変数はメソッドを保有しているオブジェクトのものを示すようになります。
 しかし、ちょっと考えてみると、これをどのようにして拡張モジュールで実現するのだろう? ということになります。Ruby はオブジェクト指向言語なので、メソッドが実行されているコンテキストを特別に意識する必要はないのですが、拡張モジュールの記述言語である C 言語は手続き型の言語なので、オブジェクト指向のようなカラクリはどこにもありません。
 それでは、Ruby の拡張モジュールはどのようにしてメソッドを実現しているのでしょうか? そして、そもそも、一体どのような形で実現されているのでしょうか?(以前の説明を読んでいれば、大体の予想はついていると思いますけれども) 関数へのポインタ列など、データとして記述されているのでしょうか。それとも、関数そのものに対応づけられているのでしょうか。

 Ruby の拡張モジュールは、関数の形に制限をかけることによって、Ruby のメソッドを C 言語で定義された関数に対応づけています。そのため、Ruby において拡張モジュールで定義されたメソッドの呼び出しが発生すると、C 言語で書かれた関数の実行が開始します。
 では、制限と書きましたが、一体どのような制限があるのでしょうか。
 まず、メソッドに必要なものは 引数 です。しかしこの引数、数が固定されている場合もあれば、自由長であるという場合もあります。もう一つ必要なものは、コンテキストとなるオブジェクト です。C 言語には C++ や Java にあるような this ポインタが存在しませんから、それを補ってやらなければなりません。
 考えても仕方がないので (もう決定しているものですからね) 記述する方法を示してしまいますが、上で述べたことに注意しながら見ていってみてください。

 ある Ruby のメソッドと、拡張モジュールにおける C 言語の関数は、次のような対応になります。
 まずは固定長引数の場合。

def foo ( arg1, arg2 )
  ...
end
VALUE foo ( VALUE self, VALUE arg1, VALUE arg2 )
{
  ...
}

 次は自由長引数の場合です。

def bar ( *arg )
{
  ...
}
VALUE bar ( int argc, VALUE* argv, VALUE self )
{
  ...
}

 まず、どちらの場合でも、拡張モジュールの方が self という引数を受け取っていることに注目してください。まあ、名前は何でも構わないのですが、この引数によって「現在このメソッドがどのオブジェクトのコンテキストで動作しているのか?」というのが指定されます。VALUE という型については、また後で解説します。とりあえず現在は「Ruby と C の間を橋渡しするための型」と考えておいてください。
 次に、固定長引数の場合は、Ruby での引数が C の関数における引数に対応していることに気がつくと思います。自由長引数の場合は、C 言語の main() などと同じような感じがしますね。

 さて、次の問題は「引数の数をどこで指定するのか?」ということです。Ruby が C 言語のプロトタイプ宣言などを読み取って、自動的に調整してくれる…、というような事はありません。というより、関数に引数の情報が全く含まれない C 言語では、そのようなテクニックを使うことができません。
(デバッグ情報なども駆使すれば可能かもしれませんけれども、全プラットフォームで共通というわけにはいかないでしょう…。)
 そこで、拡張モジュール (というより Ruby) では、C 言語の方から Ruby にメソッドの仕様を通知するという方法をとっています。上に示した二つの関数を SampleClass のメソッドとして定義するコードを示します。

rb_define_method(cls_SampleClass, "foo", 2);
rb_define_method(cls_SampleClass, "bar", -1);

 これによって、クラス SampleClass にメソッド foo()bar() が追加されます。

void rb_define_method (VALUE classobj, const char * methodname, int argnum)
classobj メソッドを追加するクラス (Ruby オブジェクト)。
methodname メソッド名。ヌル文字で終わる文字列です。
argnum 引数の数を指定します。

正の数の場合、固定長引数と解釈されます。この数に満たない引数でメソッド呼び出しが行われた場合は例外が発生します。対応する関数は次のように定義します。
VALUE method ( VALUE self, VALUE arg1, VALUE arg2, 〜 )

-1 が指定された場合は、自由長引数と解釈されます。引数は「引数の数」と「引数の配列」となり、対応する関数は次のように定義します。
VALUE method ( int argc, VALUE* argv, VALUE self )

-2 が指定された場合も自由長引数になりますが、引数は Ruby の配列として渡されます。対応する関数は、次のように定義します。
VALUE method ( VALUE self, VALUE arg )
返値 宣言されたクラスオブジェクト。

 ここまででひとまず、必要最低限の知識は仕入れられたと思います。
 クラスの宣言や、メソッドの定義に関しては、他にもまだ多くのことがあるのですが、現時点ではまだ必要ないでしょう。それよりも、VALUE 型の扱いに関して知っておく方が先です。
 というわけで、クラスとメソッドに関する話は、ここで一度、終わりにします。どちらにしても、VALUE に関する解説が終わってから、また、この周辺の話が必要になりますしね。

(2002/11/1), (2002/12/20)


ENTRANCEINFORMATION | DIARY | LABORATORY | LINK
CAGE(一般掲示板)Developper's Nest(開発掲示板)こころ宙