Ruby

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


拡張モジュールの作り方


オブジェクト指向スクリプト言語 Ruby

 Ruby は、オブジェクト指向のスクリプト言語です。コンセプトとして「プログラムを組んでいて楽しいと感じることができる」というものがあり、実際その通りで、プログラミングがしやすくなるように、いろいろな工夫がなされています。速度性能という面では Perl などに及びませんが、再利用性などの面では、手続き型のスクリプト言語よりも遥かに優れています。

インストール

 Ruby のオフィシャルサイト からダウンロードすることができます。
 Windows など、コンパイル済みのバイナリが必要な場合は、トップページにある「ダウンロード」から Windows 用のバイナリがある場所を探さなければなりません。しかし、Ruby はもともと UNIX 環境を前提に作られているので、使うのであれば Cygwin 上でソースからコンパイルして構築する方が良いと思います。

TIPS

 Ruby における細かい話題を提供します。

ガベージコレクトに引っかからないオブジェクト カテゴリ: 拡張モジュール
レベル: 中〜高
 Ruby のガベージコレクタはとても優秀 (とプログラミング言語 Ruby に書いてある) なので、スタックフレームの内まで Ruby オブジェクトが無いかと探してくれます。しかし、コンパイラの挙動によっては望む動作をしてくれない場合もあるのです。
 たとえば次のような場合。
VALUE foo ( VALUE self )
{
    VALUE v;
    struct bar *a;
    int n;

    v = Data_Make_Struct( rb_cData, struct bar, NULL, NULL, a );
    a->ptr = ALLOC_N( int, BUFFER_SIZE );
    n = (int)a->ptr & 0xff;
    my_function( n );

    return self;
}
関数中において v を生成した後でアクセスを行わず、C 言語における実体である a のみを操作するような場合があります。
 このようなとき、優秀なコンパイラ (gcc とか) は v が使われなくなってしまったものと思いこんでしまい、別の値を割り当ててしまいます。この例では、n が v の領域に割り当てられてしまうでしょう。そうなると、v が指していたオブジェクトが、n への書き込みが行われた時点で見えなくなってしまいます。
 この状態でガベージコレクトが行われたとすると、元々 v が指していたオブジェクトはガベージコレクタによって回収され、メモリが無効なものとなってしまいます。C 言語で無効なメモリを指し示すと Bus Error や Segmentation Fault が発生する原因となりますので、とても危険な状態と言えます。
 これを回避するには、v を必ずスタックに割り当てるよう、volatile で宣言すればよいわけです。
VALUE volatile v;
これで不測のガベージコレクトが行われる事態を避けることができます。気をつけなければ発見できない問題なので、拡張モジュールで Ruby オブジェクトを生成する場合は、十分に気をつけましょう。

スクリプトの置かれたディレクトリをライブラリパスに加える カテゴリ: Ruby スクリプト
レベル: 低
 通常、require などのメソッドで検索されるパスは Ruby のシステムライブラリ (コンパイル時に指定) とカレントディレクトリ、-I オプションで指定したディレクトリ、そして環境変数 RUBYLIB で指定したディレクトリとなっています。
 しかし、違うディレクトリにあるスクリプトを呼び出し、そのディレクトリに置かれたライブラリを require したいというような場合は多々あります。そこで、require を行うスクリプトの先頭に次の行を書き加えておきます。
$: << File::dirname(__FILE__)
これで、どこから実行してもライブラリを読み込んでくれます。
 もしくは、直接にディレクトリを埋め込む方法もあります。
require File.dirname(File.expand_path(__FILE__)) + "/lib.rb"
この場合、ライブラリパスを汚染しません。

 また、カレントディレクトリに標準ライブラリなどと同名のライブラリファイルがあり、それを優先的に読み込みたいという場合は、次のように検索パスの先頭にデータを加えるという方法もあります。
$:.unshift('.')
カレントディレクトリによって使われるライブラリを変えることができるので、ライブラリのテストをするときなどに便利かもしれません。

alias の意味は? カテゴリ: Ruby スクリプト
レベル: 低〜中
 alias は、あるメソッドに別名を与えるための命令です。が、def でも同様の事を実現することができます。たとえば、
def foo ()
  bar()
end
と書くのも、
alias bar foo
と書くのも同じ意味です。
 では何が違うのか? といいますと、効率 が違うのです。
 自分のマシンで実行時間を測定してみると、alias を使った方が二倍ほど早くなりました。これは、メソッド呼び出しを行うと特別なグローバル変数の待避 ($_ など) やメソッドの検索に時間がかかるためと思われます。一度 foo() が呼ばれてから bar() が呼ばれるので、オーバーヘッドができてしまうわけです。
 というわけで、ほかに何もしていない、というのならば alias を使う意義は十分にあるというわけです。

 では、特異メソッド (クラスメソッド) の場合はどうすればいいでしょうか?
class Sample
  def self.foo ()
  end
  alias Sample::bar Sample::foo
end
とはできません。alias では、Sample 中のメソッドしか扱うことができないからです。
 それではどうするか? クラスそのものを操作すれば良いので、次のようにします。
class << Sample
  alias bar foo
end
これで Sample::bar がエイリアスとなります。

CGI でファイル転送するとき カテゴリ: CGI
レベル: 低
 Ruby 標準添付の CGI ライブラリ、cgi.rb はブラウザからのファイル転送もサポートしています。これにより、クライアントが CGI に対してローカルのファイル内容を転送することができるようになります。
 しかし、このファイル転送を使ったとき、CGI オブジェクトのフォームパラメータが若干変わってくるので注意しなければなりません。

 論より証拠、ファイル転送をしたときに CGI#paramsinspect してみると、次のような結果が得られます。
[["command", [#<File:0x402d5000>]], ["source", [#<File:0x402b51d8>]]]
 これを見ればわかるように、各データはファイル (実際にはテンポラリファイル) オブジェクトになっています。このままでは文字列としてアクセスできないので、データ内容が必要な場合は適宜 read() メソッドを呼び出さなければなりません。
 なお、クラス名が File となっていますが、実際には === 演算子で File オブジェクトであるかどうかを判定しても、偽が返されてしまいます (テンポラリファイルクラスは File を継承していないため)。
 そのため、文字列でもファイルでも同じように扱うためには、String 型で判定するといいでしょう。
command = $cgi['command'].first || ''
command = command.read unless String === command
 パラメータが空の場合は値が nil になってしまい、次の文にある判定でテンポラリファイルと間違えられてしまうので、空文字列で保護しているのに注意してください。

Ruby を自分のプログラムに組み込むとき カテゴリ: ビルド
レベル: 高
 自分のプログラムに Ruby の実行系を組み込もうと考えたとき、どのようにすればよいでしょうか。UNIX 系であれば、Makefile を参考にして、比較的楽に実現することができます。しかし Windows 系、とくに Visual C++ の環境では、そう容易なことではありません。ここでは、VC++ のプロジェクトにおいて、Ruby を組み込む方法について書いていきます。

 まず、Ruby のライブラリが必要になります。これはビルドするほかに無いので、まずは Ruby のアーカイブをダウンロードし、コンパイルします。
> cd win32
> configure.bat
> nmake
 これで、
  • msvcrt-ruby17.dll
  • msvcrt-ruby17.lib
  • libruby.lib
というファイル (他多数) が生成されます。ここでは Ruby 1.7 を使用したので、そのバージョンの識別子がファイル名にくっついています。数字の部分は適宜読みかえてください。
 Ruby のソースを展開したディレクトリをそのまま使っても構いませんが、どうにも気持ちが悪いのであれば、ホームディレクトリのライブラリ置き場へこれらのファイルをコピーします。また、その場合はヘッダファイルもコピーしなければなりません。
  • config.h (win32 の下にあるものをコピー)
  • defines.h
  • intern.h
  • missing.h
  • ruby.h
  • win32/win32.h
  • vms/vms.h
なお、Ruby のソースを展開したディレクトリをそのまま使う場合は、config.h が Ruby のソースディレクトリにコピーされないようなので、自分でコピーするなり移動するなりしておく必要があります。
 これで下準備は完了です。

 次は統合環境の方を準備します。
 まず、インクルードディレクトリとライブラリディレクトリに、それぞれヘッダファイルとライブラリファイルを置いた位置を追加しておきます。これはプロジェクトそのものの設定に書いてもいいですし、統合環境の「オプション」で設定してもいいでしょう。
 必要となるライブラリも書いておかなければなりません。これらはプロジェクトの設定にある「リンク」の「インプット」において指定することができます。
  • wsock32.lib
  • libruby.lib
  • msvcrt-ruby17.dll
また、「無視するライブラリ」に次のライブラリを加えます。
  • msvcrt
これで完了です。
 あとは、Ruby のコードを使う部分でヘッダファイルをインクルードするのですが…、どうにも、MFC と組み合わせるとうまく動かない場合があります。また、winsock.h をインクルードしなければならなかったり…。この辺りは、詳しいことがわかったら、また書き足していきます。

クラスメソッドの呼び出し カテゴリ: Ruby スクリプト
レベル: 低
 クラスメソッドは次のように定義されます。
class Foo
  def Foo.bar ()
    ...
  end
end
このメソッドを使用しようとしたとき、次のように書くことはできません。
Foo.new.bar() # → undefined method `bar' for #<Foo:...>
C++ などでは、インスタンスからクラスメソッドを呼び出すことができるのですが、Ruby では、できないことになっています。わたしの勝手な予想ですが、理由として二つあると思います。ひとつは、言語的にスッキリしないためで、もうひとつは、実行時のメソッド検索におけるパフォーマンスが低下するためです。言語的にスッキリしないのは、「クラスメソッドはクラスに割り当てられたメソッドであって、インスタンスに割り当てられたメソッドではない。」と言えばいいでしょうか。
 クラスメソッドは、インスタンスではなく、クラスオブジェクトを指定して書きます。
Foo::bar() # or Foo.bar()
 この話題は [ruby-list:36371] が発端となっています。

CVS コミット用スクリプト カテゴリ: Ruby スクリプト
レベル: 低
CVS を使っていると、コミットをしたくなることが多いと思います。わたしは、いつも (対象ファイル).changelog というファイルを作っておき、そこに変更点を書いておきます。たとえば、
- Some changes.
- Fixed bugs.
を hogehoge.c.changelog に書いておくわけです。で、コミットを行うときは次のようにします。
$ cvs commit -F hogehoge.c.changelog hogehoge.c
しかし、毎回毎回、これを行うのは面倒です。
 そこで、カレントディレクトリに存在する .changelog ファイルを検索し、自動的にコミットするというスクリプトを書いてみました。てきと〜に作ってあるので、細かいところは自分で修正してください…。改良点などありましたら、掲示板なりメールなりで教えてください。お願いします。

ダウンロード (commit, 912 byte)

#!/usr/local/bin/ruby

input = ARGV
if ARGV.empty? then
  input = Dir.glob("*.changelog")
  input.each do |file|
    file.sub!(/.changelog$/, '')
  end
  
  $stderr << "commit files:\n"
  input.each do |file|
    $stderr << "  #{file}\n"
  end
  $stderr << "perform commit, OK? (y/N) "
  ans = $stdin.readline
  unless /^y$/i === ans then
    $stderr << "canceled.\n"
    exit
  end
end

while file = input.shift do
  if /^(.+)\./ === file then
    base = $1
  else
    base = file
  end
  changelog = file + ".changelog"
  unless File::exist?( changelog ) then
    changelog = base + ".changelog"
    unless File::exist?( changelog ) then
      $stderr.print "#{changelog} is not found.\n"
      exit
    end
  end
  comline = "cvs commit -F #{changelog} #{file}"
  $stderr.print comline + "\n"
  if system( comline ) then
    $stderr.print "removing #{changelog}...\n"
    File::unlink( changelog )
  end
end
ソースも掲載してきます。

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