iOSアプリでHaskellで作った関数を使う

Haskellで解くのが楽な問題があってHaskellで書いたけど、その後iOSアプリから使いたくなったので調べてみた。

GHC iOS の導入

まず、GHCやcabal-installを最新にしておく。

brew install ghc cabal-install
cabal update && cabal install cabal-install

~/.cabal/config に $ncups という定義があったら消す必要があるらしいが、うちにはなかった。

ghc-ios-scriptsをcloneしてパスを通す。

git clone git@github.com:ghc-ios/ghc-ios-scripts.git ~/bin/ghc-ios-scripts
echo -e "\nPATH=~/bin/ghc-ios-scripts:"'$PATH' >> ~/.profile
PATH=~/bin/ghc-ios-scripts:$PATH

installGHCiOS.shを実行。クロスコンパイル用のGHCとかを入れてくれる。

installGHCiOS.sh

が、コケたので調べてみたら、curlがリダイレクト処理してなかったので修正。再度実行したらいけた。

diff --git a/installGHCiOS.sh b/installGHCiOS.sh
index e13087e..780c326 100755
--- a/installGHCiOS.sh
+++ b/installGHCiOS.sh
@@ -13,7 +13,7 @@ fi
 
 echo "Downloading GHC for iOS devices..."
 
-curl -O https://www.haskell.org/ghc/dist/7.8.3/ghc-7.8.3-arm-apple-ios.tar.xz
+curl -OL https://www.haskell.org/ghc/dist/7.8.3/ghc-7.8.3-arm-apple-ios.tar.xz
 tar xvf ghc-7.8.3-arm-apple-ios.tar.xz && mv ghc-7.8.3 ghc-7.8.3-arm
 rm ghc-7.8.3-arm-apple-ios.tar.xz
 cd ghc-7.8.3-arm
@@ -35,7 +35,7 @@ rm -r ghc-7.8.3-arm
 
 echo "Downloading GHC for the iOS simulator..."
 cd /tmp
-curl -O https://www.haskell.org/ghc/dist/7.8.3/ghc-7.8.3-i386-apple-ios.tar.xz
+curl -OL https://www.haskell.org/ghc/dist/7.8.3/ghc-7.8.3-i386-apple-ios.tar.xz
 tar xvf ghc-7.8.3-i386-apple-ios.tar.xz && mv ghc-7.8.3 ghc-7.8.3-i386
 rm ghc-7.8.3-i386-apple-ios.tar.xz
 cd ghc-7.8.3-i386

iOSプリプロジェクト作成

Xcodeで新規プロジェクトを作る。

CocoaPodsと協調利用できるか確認したかったからとりあえず適当に1つ入れておく。

platform :ios, '8.0'
pod 'SDWebImage'
use_frameworks!

HaskelliOS.xcconfigをダウンロードしてプロジェクトに追加。これとCocoaPodsのxcconfigをマージするxcconfigを作成して、向け先もCocoaPodsのものからこちらに向ける。

#include "Pods/Target Support Files/Pods/Pods.debug.xcconfig"
#include "HaskelliOS.xcconfig"
#include "Pods/Target Support Files/Pods/Pods.release.xcconfig"
#include "HaskelliOS.xcconfig"

HaskelliOS.xcconfigのARCHS = "$(ARCHS_STANDARD_32_BIT)"コメントアウトを外す(まだ64bitで動かない)。また、HEADER_SEARCH_PATHSも自分の環境に合わせて書き直す。うちの環境ではバージョンだけなおした。

ソースコードの配置

アプリのコードの中にHaskellというフォルダを作って、haskell.hsを追加。試しに以下のようなコードを書く。戻り値はIOでくるむ必要がある様子。

{-# LANGUAGE ForeignFunctionInterface #-}

module HTestApp where 
import Foreign

foreign export ccall x3Int :: Int -> IO Int

x3Int :: Int -> IO Int
x3Int x = return $ x * 3!

この状態で、ghc-ios haskell.hsをビルドすると.aとか.hとか出てくるので、Haskellフォルダごとまるっとプロジェクトに追加。

スクリーンショット 2015-03-13 11.50.40.png

Haskell.hsはターゲットに含めないようにTarget Membershipのチェックを外しておく。

HaskellのコードをiOSアプリビルド時にビルドされるようにする

次に、ビルドしたときにHaskellのコードがビルドされるようにする。ターゲットのBuild PhasesにRun Scriptsを追加して、Compile Sourcesの前に、以下のように追加する

cd $SRCROOT/App/Haskell
PATH=$PATH:/usr/local/bin:$HOME/bin/ghc-ios-scripts/ ghc-ios haskell.hs

スクリーンショット 2015-03-13 11.59.56.png

Haskell部分の初期化と解放

Haskell部分を呼ぶときはhs_initで初期化して、hs_exitで解放する必要がある。ブリッジオブジェクトを作り、こいつの初期化・解放とHaskellの状態が一致するようにした。

#import <Foundation/Foundation.h>

@interface HaskellBridge : NSObject

- (int)x3Int:(int)x;

@end
#import "HaskellBridge.h"
#import "HTestApp_stub.h"

@implementation HaskellBridge

- (instancetype)init
{
    self = [super init];
    if (self) {
        hs_init(NULL, NULL);
    }
    return self;
}

- (void)dealloc
{
    hs_exit();
}

- (int)x3Int:(int)x
{
    return (int)x3Int(x);
}

@end

サンプルアプリの作成

あとはこういう画面を作って

スクリーンショット 2015-03-13 12.13.54.png

こんな感じで使う。

#import "ViewController.h"
#import "Haskell/HaskellBridge.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (nonatomic) HaskellBridge *bridge;

@end

@implementation ViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    self.bridge = [[HaskellBridge alloc] init];
}

- (IBAction)x3Pushed:(id)sender {
    self.textField.text = [NSString stringWithFormat:@"%d",
                           [self.bridge x3Int:[self.textField.text integerValue]]];
}

@end

文字列を渡す

文字列を渡すには、Haskell側でCStringを使って定義しておく。

foreign export ccall repeatString :: Int -> CString -> IO CString

repeatString :: Int -> CString -> IO CString
repeatString x cstr = do
  str <- peekCString cstr 
  newCString . foldl (++) "" $ replicate x str 

NSStringをcStringUsingEncodingでCの文字列ポインタをとってきてそれを渡せばOK。

- (NSString *)repeatString:(NSString *)str count:(int)count
{
    const char *inCstr = [str cStringUsingEncoding:NSUTF8StringEncoding];
    const char *outCstr = (const char *) repeatString(count, (HsPtr)inCstr);
    return [[NSString alloc] initWithCString:outCstr encoding:NSUTF8StringEncoding];
}

問題点

最初の方にも書いたけど、64bit未対応なので、現時点ではストアにリリースできなくなってしまっていること。