on
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.
Figure 1: 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.