(2018.5.13)
(その4)まとめを公開しました。ベンチマーク結果もありますのでご参考に。
##はじめに
以前C++を使ってRubyのクラスメソッドを書くを投稿しましたが、大きな配列を扱うことが多いので、C++でデバッグするのはしんどいところがあります。
比較的デバッグがやり易く、速度も出やすいHaskellで書いた整数、浮動小数点、文字列、配列を使う関数をRubyから呼び出す方法を紹介いたします。
GHCのインストールなどでstackを利用している方が多いと思いので、それを使います。
buildなどはPythonからHaskell関数を呼ぶを参考にさせていただきました。
以下
$ stack new testFFI
で作成したプロジェクトを前提としてます。
##ファイルの変更
app/Main.hsとsrc/Lib.hsにプログラムを書き込みます。
module Main where
import Foreign.C.Types
import Foreign.C.String
import Foreign.Marshal.Array (peekArray, newArray)
import Foreign.Marshal.Alloc (free)
import GHC.Ptr
import Lib
main = undefined
convertStringBy :: (String -> String) -> CString -> IO CString
convertStringBy func cstring = do
str <- peekCString cstring
newCString $ func str
convertArrayBy func n ptr = do
lst <- peekArray (fromIntegral n) ptr
newArray $ func lst
foreign export ccall "twicefold_hi" twicefold :: CLong -> CLong
foreign export ccall "twicefold_hd" twicefold :: CDouble -> CDouble
upcase_hs str = convertStringBy upcase str
foreign export ccall upcase_hs :: CString -> IO CString
twicefold_had n darray_ptr = convertArrayBy twicefold_list n darray_ptr
foreign export ccall twicefold_had :: CLong -> Ptr CDouble -> IO (Ptr CDouble)
foreign export ccall "free_ptr" free :: Ptr a -> IO ()
module Lib
(twicefold, twicefold_list, upcase) where
import Data.List
import Data.Char (toUpper)
twicefold n = n*2
twicefold_list lst = map (*2) lst
upcase str = map toUpper str
foreign export〜
はどのファイルに書いても問題ないようですが、ghciでエラーが出てデバッグがやりにくいので、分けた方が良いように思います。
package.yamlを一部変更してください。
(省略)
dependencies:
- base >= 4.7 && < 5
ghc-options:
- -shared
- -dynamic
library:
source-dirs: src
executables:
testFFI.so:
main: Main.hs
source-dirs: app
extra-libraries:
- HSrts-ghc8.2.2
ghc-options:
- -shared
dependencies:
- testFFI
(省略)
HSrts-ghc8.2.2はGHCのバージョンに合わせてください。
$ stack build
でtestFFI.soが作成されます。
最後の方に次のようなメッセージのが出ますので、パスをffi_libのあとに書き込んでください。
Installing executable testFFI.so in /path..
##テスト用プログラム
require "ffi"
module Test
extend FFI::Library
ffi_lib "./.stack-work/install/x86_64-osx/lts-11.5/8.2.2/bin/testFFI.so"
attach_function :c_hs_init, :hs_init, [:pointer, :pointer], :void
attach_function :hs_exit, [], :void
attach_function :hs_free, :free_ptr, [:pointer], :void
attach_function :twicefold_hi, [:int64], :int64
attach_function :twicefold_hd, [:double], :double
attach_function :twicefold_had, [:int64, :pointer], :pointer
attach_function :upcase_hs, [:string], :pointer
def self.hs_init() c_hs_init(nil, nil) end
def self.upcase(str)
ptr=Test.upcase_hs str
read_str=ptr.read_string
self.hs_free(ptr)
read_str
end
def self.twicefold_array(array)
arg_ptr=FFI::MemoryPointer.new(:double, array.size)
arg_ptr.write_array_of_double(array)
ret_ptr=Test.twicefold_had(array.size, arg_ptr)
ret_array=ret_ptr.read_array_of_double(array.size)
self.hs_free(ret_ptr)
ret_array
end
end
Test.hs_init
puts Test.twicefold_hi(100001)
puts Test.twicefold_hd(10000.0)
puts Test.upcase "abcdEF"
p Test.twicefold_array([*10..20])
Test.hs_exit
(2018.4.17 test.rbの使っていないメソッドを削除)
それとhs_initの実行を忘れないように
##ffiがSegmentation faultで落ちる時の対処法
私の環境では最初、attach_function命令のところでSegmentation faultが発生して動かない事態になっていました。調べてみるとlibffiが2つ存在し、どうも読んでるffi.hとリンクしているlibffiの実体が違うようでした。
ffiをgemでインストールするときに、--with-ffi_c-includeや--with-ffi_c-libで指定することで解決できました。
sudo gem install ffi -- --with-ffi_c-include=xxxx
(xxxxはffi.hのあるパス名)
余談ですが、--with-yyy-includeのyyyに何を書くかは、次のようにして探せば良いようです。
ffiのextconf.rbファイルの中にdir_config("ffi_c")があり、その少し下にhave_library("ffi",...
があります。この場合は
--with-ffi_c-include=xxxx
とします。
OS MacOS 10.12.6 or Scientific Linux 7.3
GHC 8.2.2
Ruby 2.5.1p57
stack 1.6.5