In this tutorial we’ll be building a simple Go package that you can run from an iOS application (Swift) and also an Android application (Kotlin).

This tutorial does NOT use the Go Mobile framework; instead it uses Cgo to build the raw static (iOS) and shared (Android) C library that can be imported into your mobile project (which is what the Go Mobile framework does under-the-hood).

Setup

For this tutorial we’ll create a simple monorepo with the following structure:

.
├── android/
├── go/
│   ├── cmd/
│   │   └── libfoo/
│   │       └── main.go
│   ├── foo/
│   │   └── foo.go
│   ├── go.mod
│   └── go.sum
└── ios/
$ mkdir -p android ios go/cmd/libfoo go/foo

We’ll start with the Go code and come back to creating the iOS and Android projects later.

$ cd go
$ go mod init rogchap.com/libfoo

Foo package

// go/foo/foo.go
package foo

// Reverse reverses the given string by each utf8 character
func Reverse(in string) string {
    n := 0
    rune := make([]rune, len(in))
    for _, r := range in { 
        rune[n] = r
        n++
    } 
    rune = rune[0:n]
    for i := 0; i < n/2; i++ { 
        rune[i], rune[n-1-i] = rune[n-1-i], rune[i] 
    } 
    return string(rune)
}

Our foo package has a single function Reverse that has a single string argument in and a single string output.

Export for C

In order for our C library to call our foo package we need to export all the functions that we want to expose to C with the special export comment. This wrapper needs to be in the main package:

// go/cmd/libfoo/main.go
pacakge main

import "C"

// other imports should be seperate from the special Cgo import
import (
    "rogchap.com/libfoo/foo"
)

//export reverse
func reverse(in *C.char) *C.char {
    return C.CString(foo.Reverse(C.GoString(in)))
}

func main() {}

We’re using the special C.GoString() and C.CString() functions to convert between Go string and a C string.

Note: The function that we are exporting does not need to be an exported Go function (ie. starts with a Captial letter). Also note the empty main function; this is required for the Go code to compile otherwise you’ll get a function main is undeclared in the main package error.

Let’s test our build by creating a static C library using the Go -buildmode flag:

go build -buildmode=c-archive -o foo.a ./cmd/libfoo

This should have outputed the C library: foo.a and the header file: foo.h. You should see our exported function at the bottom of our header file:

extern char* reverse(char* in);

Building for iOS

Our goal is to create a fat binary that can be used on iOS devices and the iOS simulator.

The Go standard library includes a script for building for iOS: $GOROOT/misc/ios/clangwrap.sh, however this script only builds for arm64, and we need x86_64 too for the iOS Simulator. So, we’re going to create our own clangwrap.sh:

#!/bin/sh

# go/clangwrap.sh

SDK_PATH=`xcrun --sdk $SDK --show-sdk-path`
CLANG=`xcrun --sdk $SDK --find clang`

if [ "$GOARCH" == "amd64" ]; then
    CARCH="x86_64"
elif [ "$GOARCH" == "arm64" ]; then
    CARCH="arm64"
fi

exec $CLANG -arch $CARCH -isysroot $SDK_PATH -mios-version-min=10.0 "$@"

Don’t forget to make it executable:

chmod +x clangwrap.sh

Now we can build our library for each architecture and combine into a fat binary using the lipo tool (via a Makefile):

# go/Makefile

ios-arm64:
	CGO_ENABLED=1 \
	GOOS=darwin \
	GOARCH=arm64 \
	SDK=iphoneos \
	CC=$(PWD)/clangwrap.sh \
	CGO_CFLAGS="-fembed-bitcode" \
	go build -buildmode=c-archive -tags ios -o $(IOS_OUT)/arm64.a ./cmd/libfoo

ios-x86_64:
	CGO_ENABLED=1 \
	GOOS=darwin \
	GOARCH=amd64 \
	SDK=iphonesimulator \
	CC=$(PWD)/clangwrap.sh \
	go build -buildmode=c-archive -tags ios -o $(IOS_OUT)/x86_64.a ./cmd/libfoo

ios: ios-arm64 ios-x86_64
	lipo $(IOS_OUT)/x86_64.a $(IOS_OUT)/arm64.a -create -output $(IOS_OUT)/foo.a
	cp $(IOS_OUT)/arm64.h $(IOS_OUT)/foo.h

Create our iOS Application

Using XCode we can create a simple single page application. I’m going to use Swift UI, but it is just as easy to do with UIKit:

// ios/foobar/ContentView.swift

struct ContentView: View {

    @State private var txt: String = ""

    var body: some View {
        VStack{
            TextField("", text: $txt)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            Button("Reverse"){
                // Reverse text here
            }
            Spacer()
        }
        .padding(.all, 15)
    }
}

In XCode we can drag-and-drop our newly generated foo.a and foo.h into our project. For our Swift code to interop with our library we need to create a bridging header:

// ios/foobar/foobar-Bridging-Header.h

#import "foo.h"

In Xcode Build Settings, under Swift Compiler - General set the Objective-C Bridging Header to the file we just created: foobar/foobar-Bridging-Header.h.

We also need to set the Library Search Paths to include the directory of our generated header file foo.h. (Xcode may have done this for you when you drag-and-drop the files into the project).

We can now call our function from Swift, then build and run:

// ios/foobar/ContentView.swift

Button("Reverse"){
    let str = reverse(UnsafeMutablePointer<Int8>(mutating: (self.txt as NSString).utf8String))
    self.txt = String.init(cString: str!, encoding: .utf8)!
    // don't forget to release the memory to the C String
    str?.deallocate()
}

libfoo ios app

Creating the Android application

Using Android Studio, we will create a new Android project. From the Project Templates select Native C++, which will create a project with an Empty Activity that is configured to use the Java Native Interface (JNI). We will still select Kotlin as our language of choice for the project.

After creating a simple Activity with a EditText and a Button we create the basic functionality for our app:

// android/app/src/main/java/com/rogchap/foobar/MainActivity.kt

class MainActivity : AppCompatActivity() {

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

        btn.setOnClickListener {
            txt.setText(reverse(txt.text.toString()))
        }
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    private external fun reverse(str: String): String

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

We created (and called) an external function reverse that we need to implement in the JNI (C++):

// android/app/src/main/cpp/native-lib.cpp

extern "C" {
    jstring
    Java_com_rogchap_foobar_MainActivity_reverse(JNIEnv* env, jobject, jstring str) {
        // Reverse text here 
        return str;
    }
}

The JNI code has to follow the conventions to interop correctly between the Native C++ and Kotlin (JVM).

Build for Android

The way the JNI works with external libraries has changed over the many releases of Android and the NDK. The current (and easiest) is to place our outputted library into a special jniLibs folder that is copied into our final APK file.

Rather than creating a Fat binary (as we did for iOS) we are going to place each architecture in the correct folder. Again, for the JNI, conventions matter.

// go/Makefile

ANDROID_OUT=../android/app/src/main/jniLibs
ANDROID_SDK=$(HOME)/Library/Android/sdk
NDK_BIN=$(ANDROID_SDK)/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin

android-armv7a:
	CGO_ENABLED=1 \
	GOOS=android \
	GOARCH=arm \
	GOARM=7 \
	CC=$(NDK_BIN)/armv7a-linux-androideabi21-clang \
	go build -buildmode=c-shared -o $(ANDROID_OUT)/armeabi-v7a/libfoo.so ./cmd/libfoo

android-arm64:
	CGO_ENABLED=1 \
	GOOS=android \
	GOARCH=arm64 \
	CC=$(NDK_BIN)/aarch64-linux-android21-clang \
	go build -buildmode=c-shared -o $(ANDROID_OUT)/arm64-v8a/libfoo.so ./cmd/libfoo

android-x86:
	CGO_ENABLED=1 \
	GOOS=android \
	GOARCH=386 \
	CC=$(NDK_BIN)/i686-linux-android21-clang \
	go build -buildmode=c-shared -o $(ANDROID_OUT)/x86/libfoo.so ./cmd/libfoo

android-x86_64:
	CGO_ENABLED=1 \
	GOOS=android \
	GOARCH=amd64 \
	CC=$(NDK_BIN)/x86_64-linux-android21-clang \
	go build -buildmode=c-shared -o $(ANDROID_OUT)/x86_64/libfoo.so ./cmd/libfoo

android: android-armv7a android-arm64 android-x86 android-x86_64

Note Make sure you set the correct location for your Android SDK and the version of the NDK you have downloaded.

Running make android will now build all the shared libraries we need into the correct folder. We now need to add our library to CMake:

// android/app/src/main/cpp/CMakeLists.txt

// ...

add_library(lib_foo SHARED IMPORTED)
set_property(TARGET lib_foo PROPERTY IMPORTED_NO_SONAME 1)
set_target_properties(lib_foo PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libfoo.so)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/)

// ...

target_link_libraries(native-lib lib_foo ${log-lib})

It took me a while to figure out these settings, once again, naming matters so was important to name the library with lib_xxxx and also set the property IMPORTED_NO_SONAME 1 otherwise your apk will be looking for your library in the wrong place.

We can now hookup our JNI code to our Go library, cross our fingers, and run our app:

// android/app/src/main/cpp/native-lib.cpp

#include "libfoo.h"

extern "C" {
    jstring
    Java_com_rogchap_foobar_MainActivity_reverse(JNIEnv* env, jobject, jstring str) {
        const char* cstr = env->GetStringUTFChars(str, 0);
        char* cout = reverse(const_cast<char*>(cstr));
        jstring out = env->NewStringUTF(cout);
        env->ReleaseStringUTFChars(str, cstr);
        free(cout);
        return out;
    }
}

libfoo android app

Conclusion

One of Go’s strengths is that it’s cross-platform; but that doesn’t just mean Window, Mac and Linux, Go can target many other architectures including iOS and Android. Now you have another option in your toolbelt to create shared libraries that run on server, your mobile apps and maybe even web (via web assembly).

All the code for this tutorial, is available on GitHub: rogchap/libfoo

Looking forward to hearing about the new killer app that you build with Go.

comments powered by Disqus