Why would you even want to do this!?
Haskell is objectively one of the best languages in the world. Audrey Tang famously said “Haskell is faster than C++, more concise than Perl, more regular than Python, more flexible than Ruby, more typeful than C#, more robust than Java, and has absolutely nothing in common with PHP.” What more could you want? Well… Quite a lot actually. Haskell is a very strange language because it is a specification written up by a committee of academics as a research language and usefullness has never been a goal. This has resulted in many breaking changes (for example, suddenly requiring all monads to have applicative and functor definitions), initial slowness (not anymore) and a very slow adoption outside of academia. But I think all these are well worth putting up with. Lack of adoption doesn’t affect us building apps, breaking changes happen less than in most web frameworks and haskell is way faster than JVM languages (when used properly). So lets write an android app in it!
Challenges
- Firstly, we need a cross compiler (don’t worry! Binaries are now provided!)
- Next we need to build
libiconv.a
andlibcharset.a
for the platform. - Then we need to build an android app and hook them up!
This is going to be a two part article. In this part we are going to set everything up in linux and make an android app that can talk to haskell. In part 2, I will show you how to build a wrapper so that the UI can be described in haskell and callbacks set up in haskell. If that all goes well (and I get a mac) I will show you in a third part how to build one haskell app, two wrappers (one on Kotlin, one in Objective C), so that all your app code can be instantly run on both iOS and android without any changes to the app code.
Setting Up
I am going to assume that you have installed Android Studio, you have GHC, gcc etc. People tend not to look at haskell tutorials unprepared (twice in their life). Make sure to have the NDK set up with android studio and have LLVM 5 installed!
Getting the compilers
Cross compilers for Mac OS and Linux have been built already! Head here to download them. Notice that they only
work on 64 bit linux and mac os. (I haven’t tested them on the WSL, maybe that would work on windows?). Download, unpack and put the bin folders
into your path in your ~/.bashrc
file. This, however, won’t work straight away. You need some wrappers for clang. Clone this
repo into a folder and stick it into your path. Work out where your NDK path is (mine is ~/Android/Sdk/Ndk-Bundle
) and change the values in android-toolchain.config
and linux-android-toolchain.config
. Also change the HOST_ARCH
setting (I set mine to linux-x86_64
). Once this has been done, run the bootstrap
script. If all goes
to plan, the folder should be filled with links for each android (and raspberry pi and iOS) version of clang and other tools.
Getting the Libraries
Haskell needs iconv and charset to work. iOS has these available in a dynamic library by default, sadly android doesn’t, so we are going to have to build it ourselves.
For me, getting this to work can only be described as a hacky pain! Clone this repo. We now need to edit it to point
to the correct places. At the time of writing, line 67 specifies the NDK directory, edit it to point to your directory. Line 50 specifies which platforms
are going to be targeted, add x86_64-linux-android
to that list. You could try building it… but it isn’t going to work! It will think the c compiler is broken
(because it can’t find stdio.h
or asm/types.h
). But don’t worry, we can fix this! Head to ndk-bundle/platforms/android-24/
. Now there should
be 4 folders of platforms. In each is a usr
folder and in there should be an ‘include’ folder? If not, go to ndk-bundle/sysroot/usr/
and copy the include
folder to ndk-bundle/platforms/android-24/%PLATFORM%/usr/
. For each platform, go into the include
folder. There should be another folder withe the name of the platform. Copy the content into the include folder. For example,
copy ndk-bundle/platforms/android-24/arch-arm/usr/include/arm-linux-androideabi/*
to ndk-bundle/platforms/android-24/arch-arm/usr/include/
,
and ndk-bundle/platforms/android-24/arch-arm64/usr/include/aarch64-linux-android/*
to ndk-bundle/platforms/android-24/arch-arm64/usr/include/
.
Hope that was clear! Now return to the build folder and run ./build-libiconv --host=linux-x86_64 --prefix=$PWD/libiconv
. You
might have to create a libiconv directory. If all has gone to plan, there should now we a libiconv directory with the three platforms
in! In those folders there will be a lib(64) directory with our static libraries!
To Android Studio!
Now it’s time to make a new project in android studio! Create a new project, making sure C++ support is enabled; I also use Kotlin instead of java. The default project it will create for you does two things, it loads an XML file (describing the UI) and sets the text to the result of a function. This function is in a .cpp file and returns a string saying “Hello from C++”. Our goal for today will to be to change this to ‘Hello from Haskell!’
We will make a libhs.hs file. I suggest you go to your project dir and put it in a folder: $(project)/app/src/main/haskell
Add this code:
module Lib where
import Foreign.C
foreign export ccall "hello" chello :: IO CString
chello = newCString "Hello from Haskell"
In the $(project)/app
folder, we will make a file called Makefile. Fill it with this code:
DIR='src/main/haskell'
SOURCE='$(DIR)/libhs.hs'
all: x86 aarch64 armv7
x86:
x86_64-linux-android-ghc -fllvmng -staticlib -fPIC \
-odir build/hs/x86 -hidir build/hs/x86 \
-isrc/main/haskell $(SOURCE) \
-o libs/x86_64/libhs.a
aarch64:
aarch64-linux-android-ghc -fllvmng -staticlib -fPIC \
-odir build/hs/aarch64 -hidir build/hs/aarch64 \
-isrc/main/haskell $(SOURCE) \
-o libs/arm64-v8a/libhs.a
armv7:
armv7-linux-androideabi-ghc -fllvmng -staticlib -fPIC \
-odir build/hs/armv7 -hidir build/hs/armv7 \
-isrc/main/haskell $(SOURCE) \
-o libs/armeabi-v7a/libhs.a
clean:
rm -fr build/hs/* && \
rm libs/*/libhs.a
If you go into the app folder with a command line and type make, the three different GHCs will be called. I shall quickly
explain the arguments. -fllvmng
specifies that we should use the llvm backend of GHC. -staticlib
specifies that
we should build a static library -fPIC
means ‘position independent code’, which basically makes the shared library work,
-odir
and -hidir
tells GHC where to put the intermediate complication files, -isrc
tells GHC where other includes could be
and -o
gives us the output location.
Notice that our haskell library is being put into a platform specific folder. Go into the libiconv folder, find libiconv.a
and
libcharset.a
for each platform (will be in either the lib folder or lib64 folder) and place it next to the haskell library in
the platform specific folder ( move the x86_64 libcharset.a to $(project)/app/libs/x86_64/libcharset.a
for example). Now we need
to tell Android Studio where our haskell library and libs are!
Find CMakeLists.txt
and add the following:
find_library(
hs-lib
hs
PATHS ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}
NO_CMAKE_FIND_ROOT_PATH )
find_library(
iconv
iconv
PATHS ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}
NO_CMAKE_FIND_ROOT_PATH )
find_library( \
charset
charset
PATHS ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}
NO_CMAKE_FIND_ROOT_PATH )
Modify the target bit (and add to the bottom) to look like this:
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib}
${hs-lib}
${iconv}
${charset})
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-fatal-warnings")
This tells CMake where our static libraries are and to link them to our app. The last line just stops clang from erroring hard…
Next go into the buildgradle file for the app and add under defaultConfig
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
If God is smiling, the project should still build… Now it’s time for some finishing touches.
Accessing Haskell
I am going to assume knowledge of C and C++ for this next bit. The file wants C++ but the Java Native
Interface wants C. For this reason, in our C++ file (the only one in the project) all function
exports begin with extern "C"
. I would suggest you take them out and stick a big extern “C” thingy
around everything. In side our C block, lets deference two haskell functions:
extern char* hello();
extern void hs_init(int *argc, char **argv[]);
Modify the stringFromJNI
function to call hello
instead of making a string and create a new
function (call it hsInit(env*, jobject)
, I’ll come back to the name). It will return void and call hs_init(NULL,NULL)
.
We are so almost there! Go into your MainActivity.kt file (assuming Kotlin). Before the class definition,
add this line: external fun hsInit()
. And in the companion object, call that function after loading in
the system library. Now, the name. Notice that external fun stringFromJNI(): String
is inside the
class. for this reason its method name in C++ is Java_com_bla_bla_MainActivity_stringFromJNI
.
We can’t reference the hsInit()
function inside the class because it is accessed by the object,
not any instance members. So what do we call it if it isn’t in the MainActivity class? It is
in the MainActivity.Kt file, so we add Kt to its name in C++! The final name of the function is:
Java_com_bla_bla_MainActivityKt_hsInit
. Now, make sure the haskell has been compiled, hit build and
hopefully the app should work! Next time, I’ll talk about building the UI of the app from hasekll.
James