Mobile Haskell (iOS)

Christian Kjær in a casual setting :)
Christian Kjær
13 min read

·

7. February 2018

·

, ,

A lot of progress has been going on to make Haskell work on mobile natively, instead of e.g. generating JavaScript via GHCJS and using that. Unfortunately, not much documentation exists yet on how to build a project using these tools all together.

This post will be an attempt to piece together the tools and various attempts into a coherent step-by-step guide. We will start by setting up the tools needed, and then build an iOS app that runs in both the simulator and on the device itself (i.e. a x86 build and an arm build).

For the impatient and brave, simply,

  • clone down the MobileHaskellFun repository,
  • run ./setup-tools.sh to set up the tools,
  • cd into Offie/hs-src/
    • build the package index ./call x86_64-apple-ios-cabal new-update --allow-newer,
    • run ./call make iOS to compile the program for iOS,
  • and finally launch Xcode and start the simulator.

Otherwise, let's go through the steps in detail:

Setting up the Tools

A bunch of tools are needed, so we will set these up first. You might have some of these, but I will go through them anyways, for good measure. The steps will assume that we are on macOS for some parts, but it should not be part to adapt these to your system (all steps using brew).

If you don't have stack installed already, set it up with,

$ curl -sSL https://get.haskellstack.org/ | sh

We will collect all our tools and GHC versions in a folder in $HOME—for convenience—so first we are a going to create that directory,

$ mkdir -p ~/.mobile-haskell

Next step is cloning down cabal and building cabal-install. This is necessary until new-update lands.

$ cd ~/.mobile-haskell
$ git clone [email protected]:haskell/cabal.git
$ cd cabal-install && stack exec --no-ghc-package-path -- ./bootstrap.sh

If you have cabal-install and a system GHC already, then you can try and install it via cabal new-build cabal-install instead, which is less brittle. I wanted to remove the need to setup these though, so I went with the ./bootstrap.sh approach.

NOTE: If you are having trouble with e.g. errors on packages being shadowed, try the good ol' cabal-hell fix, and nuke ~/.ghc and ~/.cabal/.

Install LLVM version 5,

$ brew install llvm

This should set up LLVM in /usr/local/opt/llvm@5/bin (or just /usr/local/opt/llvm/bin), remember this path for later.

We'll now set up the tools from http://hackage.mobilehaskell.org, namely the toolchain-wrapper and the different GHC versions we will use.

Let's start off with getting our GHCs, by downloading ghc-8.4.0.20180109-x86_64-apple-ios.tar.xz and ghc-8.4.0.20180109-aarch64-apple-ios.tar.xz, for the simulator and device respectively. You can download the by cliking their links on the website, or curl them down with (the links are probably outdated soon, so replace the links with the ones on the site),

$ cd ~/.mobile-haskell
$ curl -o ghc-aarch64-apple-ios.tar.xz http://releases.mobilehaskell.org/x86_64-apple-darwin/9824f6e473/ghc-8.4.0.20180109-aarch64-apple-ios.tar.xz
$ curl -o ghc-x86_64-apple-ios.tar.xz http://releases.mobilehaskell.org/x86_64-apple-darwin/9824f6e473/ghc-8.4.0.20180109-x86_64-apple-ios.tar.xz

Now, let's unpack these into their own folders (assuming you're still in ~/.mobile-haskell),

$ mkdir -p ghc-aarch64-apple-ios && xz -d ghc-aarch64-apple-ios.tar.xz && tar -xf ghc-aarch64-apple-ios.tar -C ghc-aarch64-apple-ios
$ mkdir -p ghc-x86_64-apple-ios && xz -d ghc-x86_64-apple-ios.tar.xz && tar -xf ghc-x86_64-apple-ios.tar -C ghc-x86_64-apple-ios

Next up is the toolchain-wrapper, which provides wrappers around cabal and other tools we need,

$ cd ~/.mobile-haskell
$ git clone [email protected]:zw3rk/toolchain-wrapper.git
$ cd toolchain-wrapper && ./bootstrap

And that's it! We have now set up all the tools we need for later. If you want all the steps as a single script, check out the setup script in the MobileHaskellFun repo.

Setting up the Xcode Project

Setting up Xcode is a bit of a visual process, so I'll augment these steps with pictures, to hopefully make it clear what needs to be done.

First, let's set up our Xcode project, by creating a new project.

1. Create Project

Choose Single View Application,

1.1. Create Project - Single View Application

And set the name and location of your project,

1.2. Create Project - Name
1.3. Create Project - Set Location

Now, let's add a folder to keep our Haskell code in and call it hs-src, by right-clicking our project and adding a New Group,

2. Add Source Folder for Haskell Code

Interlude: Set up the Haskell Code

Before we proceed, let's set up the Haskell code. Navigate to the hs-src directory, and add the following files (don't worry, we'll go through their contents),

$ mkdir -p src
$ touch MobileFun.cabal cabal.project Makefile call LICENSE src/Lib.hs

./hs-src/cabal.project

We use the features of cabal.project to set our package repository to use the hackage.mobilehaskell.org overlay.

1packages: .
2
3repository hackage.mobilehaskell
4 url: http://hackage.mobilehaskell.org/
5 secure: True
6 root-keys: 8184c1f23ce05ab836e5ebac3c3a56eecb486df503cc28110e699e24792582da
7 81ff2b6c5707d9af651fdceded5702b9a6950117a1c39461f4e2c8fc07d2e36a
8 8468c561cd02cc7dfe27c56de0da1a5c1a2b1b264fff21f4784f02b8c5a63edd
9 key-threshold: 3

./hs-src/MobileFun.cabal

Just a simple cabal package setup.

1name: MobileFun
2version: 0.1.0.0
3license: BSD3
4license-file: LICENSE
5author: Your Name
6maintainer: email@example.com
7copyright: Your Name
8category: Miscellaneous
9build-type: Simple
10cabal-version: >=1.10
11
12library
13 hs-source-dirs: src
14 exposed-modules: Lib
15 build-depends: base >= 4.7 && < 5
16 , freer-simple
17 default-language: Haskell2010

./hs-src/Makefile

The Makefile simplifies a lot of the compilation process and passes the flags we need to use.

1LIB=MobileFun
2
3ARCHIVE=libHS${LIB}
4
5.PHONY: cabal-build
6cabal-build:
7 $(CABAL) new-configure --disable-shared --enable-static --allow-newer --ghc-option=-fllvmng
8 $(CABAL) new-build --allow-newer --ghc-option=-fllvmng
9
10binaries/iOS/$(ARCHIVE).a:
11 CABAL=x86_64-apple-ios-cabal make cabal-build
12 CABAL=aarch64-apple-ios-cabal make cabal-build
13 mkdir -p $(@D)
14 find . -path "*-ios*" -name "${ARCHIVE}*ghc*.a" -exec lipo -create -output $@ {} +
15
16binaries/macOS/$(ARCHIVE).a:
17 CABAL=cabal make cabal-build
18 mkdir -p $(@D)
19 find . -path "*-osx*" -name "${ARCHIVE}*ghc*.a" -exec lipo -create -output $@ {} +
20
21binaries/android/armv7a/${ARCHIVE}.a:
22 CABAL=armv7-linux-androideabi-cabal make cabal-build
23 mkdir -p $(@D)
24 find . -path "*arm-*-android*" -name "${ARCHIVE}*ghc*.a" -exec cp {} $@ \;
25
26binaries/android/arm64-v8a/${ARCHIVE}.a:
27 CABAL=aarch64-linux-android-cabal make cabal-build
28 mkdir -p $(@D)
29 find . -path "*aarch64-*-android*" -name "${ARCHIVE}*ghc*.a" -exec cp {} $@ \;
30
31.PHONY: iOS
32.PHONY: macOS
33.PHONY: android
34.PHONY: all
35.PHONY: clean
36iOS: binaries/iOS/${ARCHIVE}.a
37macOS: binaries/macOS/${ARCHIVE}.a
38android: binaries/android/armv7a/${ARCHIVE}.a binaries/android/arm64-v8a/${ARCHIVE}.a
39all: iOS macOS android
40clean:
41 rm -R binaries

./hs-src/src/Lib.hs

Our Haskell code for now, is simply some C FFI that sets up a small toy function.

1module Lib where
2
3import Foreign.C (CString, newCString)
4
5-- | export haskell function @chello@ as @hello@.
6foreign export ccall "hello" chello :: IO CString
7
8-- | Tiny wrapper to return a CString
9chello = newCString hello
10
11-- | Pristine haskell function.
12hello = "Hello from Haskell"
13

./hs-src/call

We use the call script to set up the various path variables that point to our tools, so we don't need these polluting our global command space. If you've followed the setup so far, the paths should match out-of-the-box.

1#!/usr/bin/env bash
2# Path to LLVM (this is the default when installing via `brew`)
3export PATH=/usr/local/opt/llvm@5/bin:$PATH
4# Path to Cross-target GHCs
5export PATH=$HOME/.mobile-haskell/ghc-x86_64-apple-ios/bin:$PATH
6export PATH=$HOME/.mobile-haskell/ghc-aarch64-apple-ios/bin:$PATH
7export PATH=$HOME/.mobile-haskell/ghc-x86_64-apple-darwin/bin:$PATH
8# Path to tools.
9export PATH=$HOME/.mobile-haskell/toolchain-wrapper:$PATH
10export PATH=$HOME/.mobile-haskell/head.hackage/scripts:$PATH
11# Path to Cabal HEAD binary.
12export PATH=$HOME/.cabal/bin:$PATH
13
14# Pass everything as the command to call.
15$@

Compiling Our Haskell Code

First off, we need to build our package index, so run (inside hs-src),

$ ./call x86_64-apple-ios-cabal new-update --allow-newer
Downloading the latest package lists from:
- hackage.haskell.org
- hackage.mobilehaskell

Now we can build our project by running make on our target. For now, we have only set up iOS, so this is what we will build.

$ ./call make iOS
CABAL=x86_64-apple-ios-cabal make cabal-build
x86_64-apple-ios-cabal new-configure --disable-shared --enable-static --allow-newer --ghc-option=-fllvmng
'cabal.project.local' file already exists. Now overwriting it.
Resolving dependencies...
Build profile: -w ghc-8.4.0.20180109 -O1
In order, the following would be built (use -v for more details):
 - natural-transformation-0.4 (lib) (requires download & build)
 - transformers-compat-0.5.1.4 (lib) (requires download & build)
 - transformers-base-0.4.4 (requires download & build)
...
find . -path "*-ios*" -name "libHSMobileFun*ghc*.a" -exec lipo -create -output binaries/iOS/libHSMobileFun.a {} +

We should now have our library file at hs-src/binaries/iOS/libHSMobileFun.a. If you change your project, to will probably need to run ./call make clean before running ./call make iOS again.

Back to Xcode

Now we need to tie together the Haskell code with Xcode. Drag-and-drop the newly created files into the hs-src group in Xcode (if it hasn't found it by itself).

And set the name and location of your project,

3. Drag the files to Xcode

Since we are using Swift, we need a bridging header to bring our C prototypes into Swift. We'll do this by adding an Objective-C file to the project, tmp.m, which will make Xcode ask if we want to create a bridging header, Offie-Bridging-Header.h, for which we will answer yes.

4. Create Objective-C File
4.1. Create Objective-C File - Choose Filetype
4.2. Create Objective-C File - Set Name

./Offie-Bridging-Header.h

In our bridging file, Offie-Bridging-Header.h, we add our prototypes that we need to glue in the Haskell code,

1extern void hs_init(int * argc, char ** argv[]);
2extern char * hello();

./AppDelegate.swift

Now let's go into AppDelegate.swift and call hs_init to initialize the Haskell code,

1import UIKit
2
3@UIApplicationMain
4class AppDelegate: UIResponder, UIApplicationDelegate {
5
6 var window: UIWindow?
7
8 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
9 // Override point for customization after application launch.
10 hs_init(nil, nil)
11 return true
12 }
13
14 func applicationWillResignActive(_ application: UIApplication) {}
15 func applicationDidEnterBackground(_ application: UIApplication) {}
16 func applicationWillEnterForeground(_ application: UIApplication) {}
17 func applicationDidBecomeActive(_ application: UIApplication) {}
18 func applicationWillTerminate(_ application: UIApplication) {}
19}

./ViewController.swift

Next, we will set up a label in a view controller. You can either set this up in the story board and connect it via an IBOutlet.

First go into the Main.storyboard and create a label element somewhere on the screen.

7. Add Label

Then enable the Assistant Editor in the top right cornor, and ctrl-click on the label, dragging it over to the ViewController.swift and name helloWorldLabel.

7.1. Add Label - Connect IBOutlet

We can now set the text of the label by calling our Haskell function with cString: hello(), making our ViewController.swift look like,

1import UIKit
2
3class ViewController: UIViewController {
4
5 @IBOutlet var helloWorldLabel: UILabel!
6
7 override func viewDidLoad() {
8 super.viewDidLoad()
9 helloWorldLabel.text = String(cString: hello())
10 }
11
12 override func didReceiveMemoryWarning() {
13 super.didReceiveMemoryWarning()
14 }
15}

Linking in our Haskell Library

The final step we need to do, is linking in our library that we built earlier, hs-src/binaries/iOS/libHSMobileFun.a, so that Xcode can find our C prototype functions.

We do this by going into Build Phases, which is exposed under the Xcode project settings, and click the + to add a new library,

5. Build Phases

Choose Add Other... to locate the library,

5.1. Build Phases - Add New

and finally locate the library file in hs-src/binaries/iOS/libHSMobileFun.a,

5.2. Build Phases - Locate the Library

We also need to set the build to not generate bytecode, because we are using the external GHC library. This is done under Build Settings, locating Enable Bitcode (e.g. via the search) and setting it to No.

6. Build Settings

Run the Code!

Final step, let's run our code in the simulator

9. Run Simulator

Congratulations! You're now calling Haskell code from Swift and running it in an iOS simulator.

8. Add libconv to libraries

Resources

Most of this is gathered from:

If you are interested in following the development of Haskell in the mobile space, I recommend following @zw3rktech and @mobilehaskell.

Finally, let me know if something is not working with the MobileHaskellFun repository. I haven't dealt that much with setting up Xcode projects for sharing, so I'm a bit unclear on what settings follow the repository around.