LoginSignup
6
1

More than 5 years have passed since last update.

Haskellを使ってRubyのクラスメソッドを書く(その2) FFI+JSON

Last updated at Posted at 2018-04-18

(2018.5.13)
(その4)まとめを公開しました。ベンチマーク結果もありますのでご参考に。

はじめに

その1の続きです。
直接FFIで数値やポインタでやり取りするとどうしてもC言語の制限が残ってしまいます。RubyやHaskellで使える大きな整数や複雑な構造の変数を気楽に扱えるJSONを経由させる方法を紹介します。
基本的には、RubyとHaskellの間でJSON形式のテキストを介してやり取りしているシンプルなものです。

ここではフィボナッチ数を1つだけ、それと配列にして求める2つを示します。

Haskell

まずHaskellの関数を用意します。

src/Fib.hs
module Fib (fib) where

-- フィボナッチ数の計算
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib i = fibs !! i

Lib.hsはそのまま使ってください。

src/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テキストとデータとの変換をしています。

app/Main.hs
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を追加しておいてください。

package.yaml
その1からの変更点
(省略)
library:
  source-dirs: src
  dependencies:
  - aeson
  - bytestring
(省略)

テスト用プログラム

test.rb
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
6
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1