Aug 3, 2008

Ruby 1.9.0-3 : インタプリタ DLL を C から呼ぶ例

(2010-08-29 追記 : 以下の記事は古くなっています。こちらが参考になるかもしれません。 http://sites.google.com/site/ltsevenscore/ruby/tips/interpreter_dll )


  • ruby_options が値を返すようになっている
  • ruby_run という関数がない
など、Rubyの初期化・実行に関する README.EXT の内容は現状に追いついていないようです。
そのようなわけで、Ruby 1.9 を外部から利用する方法を少し調べてみました。
  • アプリケーションに Ruby を組み込みたい
  • rubyw.exe に代わる前処理プログラムを作りたい
といったときに役立つかもしれません。
利用したのは Ruby 1.9.0-3です。 これを VC++ 2008 でビルドした msvcr90-ruby190.dll を C プログラムから利用してみます。
こちらがアプリケーションのソースコード。必要最小限のエントリーポイント を取得した後、インタプリタの初期化と実行に移っています。
UseRubyDLL.c:
#define WIN32_LEAN_AND_MEAN
#include 

#define APP_NAME                "UseRubyDLL.exe"
#define RUBY_DLL_NAME           "d:\\ruby19\\bin\\msvcr90-ruby190.dll"
#define RUBY_SCRIPT_NAME        "script.rb"

HINSTANCE ruby_dll;

/* from include/ruby/ruby.h */
void  (*ruby_init)( void );
void  (*ruby_sysinit)( int*, char*** );
void* (*ruby_options)( int, char** );
/* from include/ruby/intern.h */
void  (*ruby_init_loadpath)( void );
int   (*ruby_run_node)( void* );

void
WinMainCRTStartup()
{
    /* Dummy arguments for ruby_sysinit(). */
    int         sysinit_argc = 0;
    char**      sysinit_argv = NULL;

    /* Manually construct interpreter arguments. */
    int         app_argc = 3;
    char*       app_args[3];
    char**      app_argv = app_args;
    app_args[0] = APP_NAME;
    app_args[1] = RUBY_SCRIPT_NAME;
    app_args[2] = "See ya.";

    ruby_dll = LoadLibrary( RUBY_DLL_NAME );
    if ( ruby_dll )
    {
        ruby_sysinit       = (void (*)( int*, char*** ))GetProcAddress( ruby_dll, "ruby_sysinit" );
        ruby_init          = (void (*)( void ))GetProcAddress( ruby_dll, "ruby_init" );
        ruby_init_loadpath = (void (*)( void ))GetProcAddress( ruby_dll, "ruby_init_loadpath" );
        ruby_options       = (void* (*)( int, char** ))GetProcAddress( ruby_dll, "ruby_options" );
        ruby_run_node      = (int (*)( void* ))GetProcAddress( ruby_dll, "ruby_run_node" );

        ruby_sysinit( &sysinit_argc, &sysinit_argv );
        ruby_init();
        ruby_init_loadpath();

        ruby_run_node( ruby_options( app_argc, app_argv ) );

        /* NOTE: After the execution, +ruby_run_node+ automatically
           calls +ruby_cleanup+. So there's no need to explicitly call
           Ruby's finalizer here(otherwise cause disaster).
         */

        FreeLibrary( ruby_dll );
    }
}
上記の ruby_*() 系関数の利用方法は Ruby のソースコード(winmain.c, eval.c, etc.)から把握したものです。 当然ですがインタプリタ DLL の場所など、ハードコードした部分は適宜変更する必要があります。
アプリケーションから呼び出すスクリプトはこちら。インタプリタ DLL の場所 次第では、標準ライブラリ(ここでは matrix.rb)jの require に失敗しますの でご注意を。
script.rb:
require 'matrix'
File.open( 'result.txt', 'w' ) do |f|
  f.puts Time.now
  f.puts Vector[rand,100*rand,10000*rand]
  f << ARGV
end
ビルドによって生成された UseRubyDLL.exe を以下のように実行します。 script.rb は同じ場所にあるものとします。
$ ./UseRubyDLL
実行すると同じ場所に result.txt ができているはず。以下が結果の一例です。
result.txt(例)
2008-08-03 18:52:16 +0900
Vector[0.434646515113465, 27.44515575993, 9534.51425972162]
["See ya."]
ちなみにビルド用の Makefile はこちら。実行ファイルを小さくするため、あえて Cygwin gcc (gcc -mno-cygwin -mwindows) を利用してみました。
Makefile:
TARGET = UseRubyDLL.exe
SRC = $(TARGET:exe=c)
OBJ = $(TARGET:exe=o)

CC = gcc -mno-cygwin -mwindows
CFLAGS = -Wall -Os
LDFLAGS = -lkernel32 -nostdlib

all: $(TARGET)

$(TARGET): $(OBJ)
 $(CC) -o $(TARGET) $< $(LDFLAGS)
 strip $(TARGET)

clean:
 rm $(OBJ) $(TARGET)

.c.o:
 $(CC) $(CFLAGS) -c $<
手元での結果はこちら:
$ gcc --version
gcc (GCC) 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


kazuwe@kazuwe-PC ~/UseRubyDLL
$ ls -l UseRubyDLL.exe
-rwxr-xr-x 1 kazuwe None 2560 Aug  3 18:58 UseRubyDLL.exe
ちょっと心配になるくらい小さくなるものですね :-)

参考

  • embedding Ruby 1.9.0 inside pthread
  • BDS2006(C++Builder)からRubyを使う (山本隆の開発日誌)