euphonictechnologies’s diary

Haskell超初心者の日記です。OCamlが好きです。

follow us in feedly

Haskell - GHC for iOS : iOSアプリをHaskellで開発する

ここ数週間ずっとswiftを書いてきたのですが、swiftは

  • 書いていてつまらない
  • 言語仕様がちょっと頭おかしい
  • オフィシャル言語なのにIDEが糞
  • 未来が見えない

と、ぶっちゃけ飽きてしまったので、HaskellでiOSアプリを書くという茨の道に行ってみたいと思いました。 HaskellでiOSアプリがかければ共通部分の処理はHaskellのままAndroidで動かすとか色々できそうです。ぶっちゃけHaskellからCに落としちゃえばどこでも動かせるはずです。というわけで色々探していたところ

HaskellがiOSをサポート、性能を改善

というわけで、動くのは動くらしい。少し触ってみて感じを掴んでみることにしました。まずは環境をつくって"Hello, world."です。

今回の参考資料

今回は基本的にこれに従って進めていきます。

ghc-ios/ghc-ios-scripts · GitHub

ロゴマークがカッコいいね。

まずはcabal updateとgit cloneから

最初は参考資料通りでok. cabal updateしましょう。

cabal update && cabal install cabal-install

で、~/.cabal/configの中のjobs: $ncpusとそのサブセクションを全部消しましょう。

消したら、git cloneしましょう。

git clone git@github.com:ghc-ios/ghc-ios-scripts.git ~/bin/ghc-ios-scripts

以下では~/bin/ghc-ios-scripts/にクローンしたものとして話を進めます。パスを通しておいてください。

最初の鬼門

次のステップは簡単。インストール作業はシェルスクリプトにまとまっているので、スクリプトを実行するだけです。もしかするとsudoが必要です。

installGHCiOS.sh

ただ、これ素直には動かないのでこうしましょう:

$ git diff installGHCiOS.sh
diff --git a/installGHCiOS.sh b/installGHCiOS.sh
index e13087e..353b7b2 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 -O https://downloads.haskell.org/~ghc/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 -O https://downloads.haskell.org/~ghc/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

URL変わってるやんけ! URL修正したらインストール作業が綺麗にできるはずです。このインストーラは複数のghcをインストールします。具体的には

  • ARM用(実機用)ghc
  • iOS シミュレータ用(i386の)ghc

の2つです。その時それぞれのghcが違うsettingsを見ることで複数のアーキテクチャ向けのghcを共存させることができます。それぞれのghcは実行バイナリの名前が違うので全部ちゃんと違うコンパイラとしてインストールできます。既存の環境を毀損しません(ここが高度なダジャレになっている)。

Xcodeのワークスペースを作る

Using GHC iOSに書いてあるとおりです。まずはiOS->Application->Single View Applicationを作りましょう。その後の画面では言語はObjective-Cを選びます。

f:id:euphonictechnologies:20150126204724p:plain

f:id:euphonictechnologies:20150126204730p:plain

できたらHaskelliOS.xcconfigファイルを~/bin/ghc-ios-scripts/の中からコピーしてプロジェクトに追加しましょう。

ここでひとまずXcodeから離れてコマンドライン作業に戻ります。

コマンドライン作業でHaskellソースをコンパイルする

iOSで実行するHaskellのコードをコンパイルしましょう。Haskellのコードは.a形式にしてObjective-Cのコードから呼び出せるようにします。

つまり、iOS + HaskellはiOSのObjective-Cのコードの骨組みの上にHaskellのコードで味付けをするという感じです。メインコードはHaskellでObjective-Cをグルーとして使うようなイメージですね。今回実行するコードはこちら

{-# LANGUAGE ForeignFunctionInterface #-}
module Counter where
import Control.Concurrent
import Control.Monad
foreign export ccall startCounter :: Int -> IO ()
startCounter :: Int -> IO ()
startCounter = void . forkIO . void . loop
    where loop i = do
            putStrLn (replicate i 'o')
            threadDelay (10^6)
            loop (i + 1)

まんま参考資料通りです。見ての通りUI要素を全く使わないアプリになっています。これをCounter.hsとして保存します。 欲しいのはCounter_stub.hCounter.aという2つのファイルです。インクルードファイルとそのメソッドが入ったバイナリです。ファットバイナリなので実機用とシミュレータ用のバイナリが両方ともにCounter.aに入っています。

ghc-iosでコンパイルする

ghc-ios Counter.hs

でコンパイルしましょう。多分うまく行きません。

なぜghc-iosが通らない?

通らない場合はエラーメッセージをよく見ましょう。例えば

ghc: could not execute: arm-apple-darwin10-clang

の場合、arm-apple-darwin1-clang(GHC for iOSが用意したシェルスクリプト)があるかどうか確認しましょう。。ある場合は無理矢理settingsの中を編集しましょう。settingsは普通は/usr/local/lib/arm-apple-darwin10-ghc-7.8.3の中にあるはずです。

...
 ("C compiler command", "/Users/<user name>/bin/ghc-ios-scripts/arm-apple-darwin10-clang"),
...

て感じで無理くりフルパスにしちゃいましょう。

ghc: could not execute: libtool-quiet

の場合は、libtool-quietがghc-iosのシェルスクリプト内で指定されているので

arm-apple-darwin10-ghc  $ghcargs -threaded -staticlib -outputdir build/arm  -o build/arm/$baseFileName  -pgmlibtool /Users/<user name>/bin/ghc-ios-scripts/libtool-quiet $targetFile -stubdir .

て感じでまた無理くりフルパスにしちゃいましょう。i386-apple-darwin11-XXXの方も同じように設定すればおそらく通るはず。通らない場合は自力で解決する他ありません。エラーとにらめっこしましょう。

うまくコンパイルできた

できたらCounter_stub.hCounter.aが生成されているはずなので、プロジェクトに追加しましょう。これでプロジェクトに追加するファイルは以上です。全部終わるとこんな感じ:

f:id:euphonictechnologies:20150126205131p:plain

main.mがあるかもしれませんが追加するとシンボルがduplicateとか言われるので追加しないでおきましょう。

Objective-C側のコード変更

AppDelegate.mにすべての変更を適用します。

  • #import "HsFFI.h"#import "Counter_stub.h"を追加
  • didFinishLaunchingWithOptionsを変更
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    hs_init(NULL, NULL);
    startCounter(3);
    // Override point for customization after application launch.
    return YES;
}

これでコードは準備完了です。

プロジェクトの設定を整える

PROJECT -> Build Settings -> Architectures -> Build Active Architecture OnlyをすべてNO

f:id:euphonictechnologies:20150126205617p:plain

Info -> Configurations を両方HaskelliOSに変更

f:id:euphonictechnologies:20150126205627p:plain

これで基本大丈夫なはずです。

うまくいくとCommand-Rで実機かシミュレータ上で実行できて真っ白な画面とデバッグ表示のところにoが増えていくのが見えるはずです。

f:id:euphonictechnologies:20150126205741p:plain

次は

UI要素を使えるようにするかcabal-iosを使えるようにするかに挑戦してみたいです。