Writing Android Apps in Haskell Part 1

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

  1. Firstly, we need a cross compiler (don’t worry! Binaries are now provided!)
  2. Next we need to build libiconv.a and libcharset.a for the platform.
  3. 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