HaskellからRubyを呼んでみる(2)

id:ha-tan:20061031:1162240689の続き。拡張ライブラリをロードできるようにしてみました。とりあえずopen-uriをrequireしてWebページをダウンロードしてみます。昨日は静的ライブラリをリンクしていましたが、今日はDLLをリンクしています(そうしないと.soの拡張ライブラリがロードできません)。
ファイル: ruby_test.hs

module Main (main) where

import Foreign.C.String (CString, newCString, withCString)
import Foreign.Marshal.Alloc (free)
import Foreign.Marshal.Array (withArrayLen)
import Foreign.Marshal.Utils (new)
import Foreign.Ptr (Ptr)

foreign import ccall unsafe "NtInitialize" cNtInitialize :: 
    Ptr Int -> Ptr (Ptr CString) -> IO ()

foreign import ccall unsafe "ruby_init" rubyInit :: IO ()

foreign import ccall unsafe "ruby_options" cRubyOptions ::
    Int -> Ptr CString -> IO ()

foreign import ccall unsafe "rb_eval_string" cRubyEvalString ::
    CString -> IO ()

withCStringArrayLen :: [String] -> (Int -> Ptr CString -> IO a) -> IO a
withCStringArrayLen ss f = do
  css <- mapM newCString ss
  a <- withArrayLen css f
  mapM_ free css
  return a

ntInitialize :: [String] -> IO ()
ntInitialize ss = do
  withCStringArrayLen ss $ \ len css -> do
    len' <- new len
    css' <- new css
    cNtInitialize len' css'
    free css'
    free len'

rubyInitOptions :: [String] -> IO ()
rubyInitOptions ss = do
  ntInitialize ss
  rubyInit
  rubyOptions ss

rubyOptions :: [String] -> IO ()
rubyOptions = flip withCStringArrayLen cRubyOptions

rubyEvalString :: String -> IO ()
rubyEvalString = flip withCString cRubyEvalString

main :: IO ()
main = do
  rubyInitOptions ["<unknown>", "-I./lib", "-I./lib/i386-mingw32", "-e", ""]
  rubyEvalString "require 'open-uri'"
  rubyEvalString "open('http://d.hatena.ne.jp/ha-tan/', 'r') {|http| print http.read }"

実行手順
1. ruby-1.8.4-i386-mingw32.tar.gzから必要なファイルを抜き出します。

$ tar zxvf ruby-1.8.4-i386-mingw32.tar.gz
$ cp usr/local/lib/libmsvcrt-ruby18.dll.a .
$ cp usr/local/bin/msvcrt-ruby18.dll .
$ cp -r usr/local/lib/ruby/1.8/ ./lib
$ rm -rf usr

2. 以下のコマンドでコンパイルします。

$ ghc -W -fno-warn-unused-matches -ffi -o ruby_test ruby_test.hs libmsvcrt-ruby18.dll.a

3. できあがったプログラムを実行します。

$ ./ruby_test.exe
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-jp">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript"
...

現在の技術的な課題

  • 例外が発生すると[BUG] Segmentation faultとかいやな感じで終了するので何とかする必要があります。rb_eval_stringを直に使ってるのがまずいんじゃないかなと予想しています。rb_eval_string_protectあたりを使えば何とかなるかと。でもそうするとスタックトレースをどう出すのかが問題になりそうです。
  • Ruby側で標準出力をhackしているみたいで、HaskellRubyの両方で標準出力に出力すると出力が混ざります。Ruby側の出力オブジェクトを変更して、Haskell側に返すとかした方がよさそう。
  • 上の問題にもからむのですが、Ruby側からHaskell側に値を返す方法がありません。それ用の拡張ライブラリをCで書いて、それ経由でRuby側から値を返すのがよさそう。
  • GCの動きがよくわかりません(特にHaskell)。GCが走ったときにホントに大丈夫なんだろうか…

参照: 第4回 Rubyインタプリタの組み込み(2)