A Haskell Cross Compiler for Android

Over the last two weeks we saw how to build a Haskell Cross Compiler for Raspberry Pi, set up Cabal for Cross Compilation, and how to Cross Compile Template Haskell. Building a Haskell cross compiler for Android is almost identical, with only minor differences.

For the Raspbian Haskell cross compiler we had a single architecture only. Android runs on a plethora of architectures. We will focus on arm processors, specifically the 32bit armv7 and 64bit aarch64.

The Android NDK & LLVM

Google provides the NDK for Android, which provides a similar set of tools as the Raspbian Cross Compilation SDK does; it contains the Android toolchain and sysroot.

For GHC we need opt and llc from the llvm 4, which can be obtained from their release download website.

Toolchain Wrapping

To keep our PATH tidy, and abstract about the Android NDK a bit, we’ll use a wrapper script that wraps the toolchain and embeds the sysroot.

#!/bin/bash
source android-toolchain.config
name=${0##*/}
cmd=${name##*-}
target=${name%-*}
case $name in
*-cabal)
fcommon="--builddir=dist/${target}"
fcompile=" --with-ghc=${target}-ghc"
fcompile+=" --with-ghc-pkg=${target}-ghc-pkg"
fcompile+=" --with-gcc=${target}-clang"
fcompile+=" --with-ld=${target}-ld"
fcompile+=" --hsc2hs-options=--cross-compile"
fconfig="--disable-shared --configure-option=--host=${target}"
case $1 in
configure|install) flags="${fcommon} ${fcompile} ${fconfig}" ;;
build) flags="${fcommon} ${fcompile}" ;;
list|info|update) flags="" ;;
"") flags="" ;;
*) flags=$fcommon ;;
esac
;;
# android (armv7)
armv7-linux-androideabi-clang)
flags=" --target=${target}"
flags+=" --sysroot=${ADR32_SYSROOT}"
flags+=" -isysroot ${ADR32_SYSROOT}"
;;
armv7-linux-androideabi-ld|armv7-linux-androideabi-ld.gold)
flags=" --sysroot=${ADR32_SYSROOT}"
flags+=" -L${ADR32_TOOLCHAIN_LIB}"
;;
# android (aarch64)
aarch64-linux-android-clang)
flags=" --target=${target}"
flags+=" --sysroot=${ADR64_SYSROOT}"
flags+=" -isysroot ${ADR64_SYSROOT}"
;;
aarch64-linux-android-ld|aarch64-linux-android-ld.gold)
flags=" --sysroot=${ADR64_SYSROOT}"
flags+=" -L${ADR64_TOOLCHAIN_LIB}"
;;
# default
*-nm|*-ar|*-ranlib) ;;
*) echo "Unknown command: ${0##*/}" >&2; exit 1;;
esac
case $target in
armv7-linux-android*)
exec env PATH="${ADR32_PATH}:${PATH}" $cmd $flags "$@" ;;
aarch64-linux-android*)
exec env PATH="${ADR64_PATH}:${PATH}" $cmd $flags "$@" ;;
*) exec $cmd $flags "$@" ;;
esac

The wrapper depends on android-toolchain.config which can be obtained from the zw3rk/toolchain-wrapper repository. The android-toolchain.config will likely need minor modifications, encoding the location of the NDK.

Next, we will create symbolic links to the wrapper script:

for target in "armv7-linux-androideabi aarch64-linux-android"; do
for command in "clang ld ld.gold nm ar ranlib cabal"; do
ln -s wrapper $target-$command
done
done

This will produce 14 files (e.g. armv7-linux-androideabi-clang), which will point to the wrapper. The wrapper in turn will build up the necessary flags to pass to the command, based on the name of the file. Note: we assume that ld.bfd and ld.gold accept the same flags.

Prerequisites

As Android does not ship with iconv by default, and GHC depends on iconv, we will need to build it as laid out in building iconv for android. Note that you do want to build for both targets: armv7 and aarch64 and you want to build static libraries (pass --enable-shared=no --enable-static=yes to the configure script). This will ease integrating the library into android studio.

To build GHC, we need ghc and cabal, as well as alex and happy. A recent GHC version from downloads.haskell.org should provide ghc and cabal. alex and happy can then be installed via cabal:

cabal install alex happy

As with the Haskell cross compiler for Raspberry Pi, we need to build a newer libffi from source, due to an incompatibility between the latest release version of libffi (from 2014), and recent llvm versions. With the wrapped toolchain in PATH, building libffi should be as simple as:

git clone https://github.com/libffi/libffi.git
cd libffi
./autogen.sh
CC="armv7-linux-androideabi-clang" \
CXX="armv7-linux-androideabi-clang" \
./configure \
--prefix=/path/to/libffi/armv7-linux-androideabi \
--host=armv7-linux-androideabi \
--enable-static=yes --enable-shared=yes
make && make install
git clean -f -x -d
./autogen.sh
CC="aarch64-linux-android-clang" \
CXX="aarch64-linux-android-clang" \
./configure \
--prefix=/path/to/libffi/aarch64-linux-android \
--host=aarch64-linux-android \
--enable-static=yes --enable-shared=yes
make && make install

This will build and place the libffi header and libraries for armv7 and aarch64 into /path/to/libffi/armv7-linux-androideabi and /path/to/libffi/aarch64-linux-android.

As we will also be using GHCs -staticlib flag. GHC uses libtool for -staticlib. As the NDK does not ship libtool, we need a thin wrapper. libtool-lite from the zw3rk/toolchain-wrapper repository can be used instead; it uses ar and ranlib under the hood. We only need to create symbolic links pointing to it:

ln -s libtool-lite armv7-linux-androideabi-libtool
ln -s libtool-lite aarch64-linux-android-libtool

Building GHC

We need to build GHC for both targets: armv7 and aarch64. With ghc, alex, happy, and cabal in PATH, as well as our wrapped toolchain:

export PATH=$HOME/.cabal/bin:$PATH
export PATH=/path/to/bin/ghc:$PATH
export PATH=/path/to/wrapped-toolchain:$PATH

And a copy of the patched GHC:

git clone --recursive git://git.haskell.org/ghc.git
cd ghc
git remote add zw3rk https://github.com/zw3rk/ghc.git
git fetch zw3rk
git checkout zw3rk/my-ghc -b my-ghc
git submodule update --init --recursive

Building GHC for armv7-linux-androideabi and aarch64-linux-android should require nothing more than:

# set paths
export PREFIX=/my/prefix
export LIBFFI=/path/to/libffi
export LIBICONV=/path/to/libiconv
for target in "armv7-linux-androideabi aarch64-linux-android"; do
# Clean up the build tree
git clean -x -f -d
  # Boot up the build system
./boot
  # Configure a GHC that targets $target
./configure --target=$target \
--prefix=$PREFIX \
--disable-large-address-space \
--with-iconv-includes=$LIBICONV/$target/include \
--with-iconv-libraries=$LIBICONV/$target/lib \
--with-system-libffi \
--with-ffi-includes=$LIBFFI/$target/include \
--with-ffi-libraries=$LIBFFI/$target/lib
  # Create a mk/build.mk and set the BuildFlavour to quick-cross
sed -E "s/^#(BuildFlavour[ ]+= quick-cross)$/\1/" \
mk/build.mk.sample > mk/build.mk
  # Compile and install ghc
make -j && make install
done

As this builds two cross compilers (for armv7 and aarch64), this will take approximately 60–120 minutes, depending on your hardware. Once done, it should have installed armv7-linux-androideabi-ghc and aarch64-linux-android-ghc into /my/prefix/bin.

Compiling Hello World

For Android we need to produce a hello world library, and call the native code from an Android app.

The library Lib.hs contains a thin wrapper around hello, and exposes a c function: char* hello().

module Lib where
import Foreign.C (CString, newCString)
-- | export haskell function @chello@ as @hello@.
foreign export ccall "hello" chello :: IO CString
-- | Tiny wrapper to return a CString
chello = newCString hello
-- | Pristine haskell function.
hello = "Hello from Haskell"

Assuming our Android application lives in /path/to/HelloWorld, we create /path/to/HelloWorld/app/hs-libs/armeabi-v7a and /path/to/HelloWorld/app/hs-libs/arm64-v8a.

We will make use of GHCs -staticlib flag has to produce a static library that contains the Lib.o as well as all dependencies in a single .a archive.

aarch64-linux-android-ghc -odir arm64-v8a -hidir arm64-v8a \
-staticlib -liconv -lcharset \
-L/path/to/libffi/aarch64-linux-android/lib -lffi \
-o /path/to/HelloWorld/app/hs-libs/arm64-v8a/libhs.a \
Lib.hs
armv7-linux-androideabi-ghc -odir armeabi-v7a -hidir armeabi-v7a \
-staticlib -liconv -lcharset \
-L/path/to/libffi/armv7-linux-androideabi/lib -lffi \
-o /path/to/HelloWorld/app/hs-libs/armeabi-v7a/libhs.a \
Lib.hs

Note: The-liconv -lcharset and-L/path/to/libffi... -lffi arguments are currently necessary, because ghc does not pass them properly. The libffi arguments are needed only if GHC is configured with --with-system-libffi.

We will start out with a fresh new android application with including C++ and Kotlin support (you can also use Java, the example code will be in Kotlin though), with an Empty Activity named MainActivity. For C++ use the Default Toolchain, and neither support for exceptions nor rtti is needed.

In the CMakeLists.txt file, we need to tell CMake about our new libhs.a and that we want to link against libc.

Adding the following two find_library statements:

# find libc
find_library( c-lib
c )
# find libhs in /path/to/HelloWorld/app/hs-libs/<abi>,
# outside of cmakes root search path.
find_library( hs-lib
hs
PATHS ${PROJECT_SOURCE_DIR}/hs-libs/${ANDROID_ABI}
NO_CMAKE_FIND_ROOT_PATH )

and including the found libraries in the final target_link_library statement

target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib}
${c-lib}
${hs-lib}
)

will instruct CMake to link the native-lib which contains our JNI bridge to link against the libhs as well as libc.

Setting the abiFilters in the app/build.gradle file to

android {
...
defaultConfig
...
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
...
}

will tell Android Studio that we only have armv7 and aarch64 native libraries. Adjusting the native-lib.cpp to read

#include <jni.h>
#include <string>

#ifdef __cplusplus
extern "C" {
#endif
extern void hs_init(int * argc, char ** argv[]);
extern char* hello(void);
JNIEXPORT void
JNICALL
Java_com_zw3rk_helloworld_MainActivityKt_initHS(
JNIEnv *env,
jclass /* klass */) {
hs_init(NULL,NULL);
}
JNIEXPORT jstring
JNICALL
Java_com_zw3rk_helloworld_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
return env->NewStringUTF(hello());
}
#ifdef __cplusplus
}
#endif

will provide the hello prototype, which we can use in the stringFromJNI method to call our hello() function. We also have the hs_init prototype. This one will initialize the haskell runtime and should be called only once, prior to calling any haskell function.

The MainActivity class from the MainActivity.kt then looks like this:

external fun initHS()
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Example of a call to a native method
initHS()
val tv = findViewById(R.id.sample_text) as TextView
tv.text = stringFromJNI()
}

/**
* A native method that is implemented by the 'native-lib'
* native library, which is packaged with this application.
*/

external fun stringFromJNI(): String

companion object {

// Used to load the 'native-lib' library on
// application startup.
init {
System.loadLibrary("native-lib")
initHS()
}
}
}

Note: as pointed out by /u/gergoerdi on /r/haskell onCreate can be called multiple times. The example code has been updated to move initHS from onCreate to right after the loadLibrary.

This is all that is all the source that is needed for our Hello World Haskell Android app. The source can be found at zw3rk/hs-android-helloworld.

Haskell running on an Android device

Hello from Haskell

Finally launching and running the application on the device, we are greeted with Hello from Haskell.

While the utility of this application is certainly questionable it illustrates the essential steps required to build, link and run an android application calling a native haskell function.

With this we should be well equipped to build the GHCSlave application for android next and be able to also cross compile Template Haskell to Android.