(2018.5.13)
(その4)まとめを公開しました。ベンチマーク結果もありますのでご参考に。
##はじめに
その1の続きです。
直接FFIで数値やポインタでやり取りするとどうしてもC言語の制限が残ってしまいます。RubyやHaskellで使える大きな整数や複雑な構造の変数を気楽に扱えるJSONを経由させる方法を紹介します。
基本的には、RubyとHaskellの間でJSON形式のテキストを介してやり取りしているシンプルなものです。
ここではフィボナッチ数を1つだけ、それと配列にして求める2つを示します。
##Haskell
まずHaskellの関数を用意します。
module Fib (fib) where
-- フィボナッチ数の計算
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib i = fibs !! i
Lib.hsはそのまま使ってください。
{-# LANGUAGE OverloadedStrings #-}
module Lib where
import Foreign.C.String
import Data.ByteString.Char8 (unpack, packCString, pack)
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Aeson
-- CStringをAesonが解釈できるData.ByteString.Lazy.Char8に変換し、funcに渡しその結果を、CStringに戻す
convertCStringViaJSON :: (BL8.ByteString -> BL8.ByteString) -> CString -> IO CString
convertCStringViaJSON func cstr = do
b8s <- packCString cstr
newCString . unpack . BL8.toStrict $ func $ BL8.fromStrict b8s
-- JSON(Data.ByteString.Lazy.Char8)をdecodeし、funcで変換後にencodeする
convertJSONBy :: (ToJSON a, FromJSON t) =>
(t -> a) -> BL8.ByteString -> BL8.ByteString
convertJSONBy func json_str = encode $ func d
where d = case eitherDecode json_str of
Right a -> a
Left s -> errorWithoutStackTrace s -- 型が違うとエラーに
-- debug用
toBL8 :: String -> BL8.ByteString
toBL8 string = BL8.fromStrict $ pack string
上のconvertCStringViaJSONはCStringとData.ByteString.Lazy.Char8との変換、convertJSONByはJSONテキストとデータとの変換をしています。
module Main where
import Foreign.C.String
import Foreign.Marshal.Alloc (free)
import GHC.Ptr
import Lib
import Fib
main = undefined
-- 整数値のJSONを受け取って、fibを計算してJSONで返す
fib_h cstr = convertCStringViaJSON (convertJSONBy (fib :: Int -> Integer)) cstr
foreign export ccall fib_h :: CString -> IO CString
-- 配列形式のJSONを受け取って、fibを計算してJSONで返す
fibs_h cstr = convertCStringViaJSON (convertJSONBy (map fib :: [Int] -> [Integer])) cstr
foreign export ccall fibs_h :: CString -> IO CString
foreign export ccall "free_ptr" free :: Ptr a -> IO ()
(fib :: Int -> Integer)と(map fib :: [Int] -> [Integer]))の違いだけでデータ型の違いを吸収しています。この部分だけを書き換えることで色々な関数やデータ構造に対応できます。
使われている関数(ここではfib)の型注釈は、型推論する上で重要ですので忘れないようにしてください。
decode(eitherDecode)やencodeは型推論が正常にできないと簡単にエラーになってしまいます。debug用にtoBL8を追加していますので、エラーが出るようでした、色々と試してみてください。
$ stack ghci
> :l Lib
*Lib> decode $ toBL8 "[1,2,3]" :: Maybe [Int]
Just [1,2,3]
*Lib> decode $ toBL8 "[1,2,\"a\"]" :: Maybe (Int, Double, String)
Just (1,2.0,"a")
最後の例にありますようにタプルへの変換もできます。(Ruby)Array<->(Haskell)List or Tuple間の変換がなされます。
コンパイル等はその1を参考に
今回はData.ByteStringやData.Aesonを使っていますので、package.yamlにdependenciesを追加しておいてください。
その1からの変更点
(省略)
library:
source-dirs: src
dependencies:
- aeson
- bytestring
(省略)
##テスト用プログラム
require "ffi"
require "json"
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
def self.hs_init() c_hs_init(nil, nil) end
attach_function :fib_h, [:string], :pointer
attach_function :fibs_h, [:string], :pointer
def self.ffi_via_JSON(y)
ptr= yield y
read_str=ptr.read_string
self.hs_free(ptr)
JSON.parse(read_str)
end
def self.fib(v)
self.ffi_via_JSON(v){|a| self.fib_h JSON.generate(a)}
end
def self.fibs(v)
self.ffi_via_JSON(v){|a| self.fibs_h JSON.generate(a)}
end
end
Test.hs_init
p Test.fib(20)
p Test.fibs([*100..110])
Test.hs_exit