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

(2018.5.13)
[(その4)まとめ](https://qiita.com/ki073/items/2e5ee4b29aece93bf12b)を公開しました。ベンチマーク結果もありますのでご参考に。

##はじめに
以前[C++を使ってRubyのクラスメソッドを書く](https://qiita.com/ki073/items/b35e1e0ecc4862ea3c14)を投稿しましたが、大きな配列を扱うことが多いので、C++でデバッグするのはしんどいところがあります。
比較的デバッグがやり易く、速度も出やすいHaskellで書いた整数、浮動小数点、文字列、配列を使う関数をRubyから呼び出す方法を紹介いたします。

GHCのインストールなどでstackを利用している方が多いと思いので、それを使います。
buildなどは[PythonからHaskell関数を呼ぶ](https://qiita.com/A_kirisaki/items/3f69e6add075795b02fa)を参考にさせていただきました。

以下
$ stack new testFFI
で作成したプロジェクトを前提としてます。

##ファイルの変更
app/Main.hsとsrc/Lib.hsにプログラムを書き込みます。

```haskell:Main.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 ()
```

```Haskell:Lib.hs
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を一部変更してください。

```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..

##テスト用プログラム

```ruby:test.rb
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