Compile GoLang as a Mobile Library

Adonis Gaitatzis
6 min readOct 28, 2021

If you are reading this, you are working on some edge-case!

You have a GoLang project that needs to run inside an iOS program. You could re-write everything in Swift but you are hoping it will be easier to just compile GoLang as a module.

Good news: It is possible!

The go program allows you to not only cross-compile for different platforms, but it also lets you export methods and generate C header files.

You’ll have to know how to:

  • Export functions
  • Enable mobile-specific features
  • Cross compile
  • Build the module and C-header
  • Trim the Fat

Doing this will generate a dynamic library and header file. If you are building for iOS, you’ll also need to create an XCFramework package.

Note: You might be able to use gomobile for this, but I wasn’t able to get it to work to my satisfaction.

Export Functions

When compiling GoLang as a module, you will lose access to the main() function. Instead, you’ll be calling specific methods from the host environment.

Define Exported Functions

You can do this by creating a special comment which reads //export <functionName> :

//export functionName
func functionName () {
// do stuff
}

Bridge to C

Now that you can export functions, you need expose those functions through a C bridge, which you can do by importing the “C” module:

import "C"

Let’s create a simple GoLang project that returns a string when a function is called from an external library. We can call this file sayHelloLib.go:

package mainimport "C"func main() {
// main() won't be called, but it is required for compilation
}
//export sayHello
func sayHello() *C.char {
return C.CString("Hello")
}

GoLang developers will notice here that we are returning a pointer to a C char and wrapping a string inside a C.CString. This is to prevent returning a pointer to a GolangString, as it’s called in C. Since we want to be able to communicate with C-compatible programming languages, we will need to use the C utilities to interact with Strings.

Enable Mobile-Specific Features

There are a couple considerations when exporting to mobile. In particular, we want to make sure we use cgo, which allows Go programs to interpolate with C libraries. This is essential for using being able to run GoLang programs on Android or iOS.

We do this by setting the environment variable CGO_ENABLED=1

The other consideration is binary size. Since mobile generally has less storage and memory than a normal computer, Apple introduced something called Bitcode, which uses some clever computer engineering tricks to optimize binaries, for example by packing multiple data types into a single 64-bit memory frame.

Apple requires Bitcode-enabled binaries in their programs, so let’s turn that on by setting the CGO-CFLAGS="-fembed-bitcode" environment variable:

Putting that together, we get this:

$ export CGO_ENABLED=1
$ CGO_CFLAGS="-fembed-bitcode"

Compiling

The basic command to create a binary from a GoLang project is this:

$ go build

If you are compiling a specific file or files in a folder, you’ll tell go which file or folder you are compiling:

$ go build /path/to/gofile/or/folder

Change The Output File Name

You can tell GoLang what to name the output file by passing the -o outputfilename parameter to go build. It is common to give mobile binaries a .a extension:

$ go build -o outputfilename.a /path/to/gofile/or/folder

Cross Compiling

To cross-compile, you need to tell go which OS (GOOS), architecture (GOARCH), and depending on your target platform, your SDK (SDK) by setting the appropriate GoLang environment variables.

For example, if you are compiling for an Android device, you’ll be setting the GOOS and GOARCH environment variables for Android/arm64

$ export GOOS=android
$ export GOARCH=arm64

If you are compiling to an iOS device, you’ll be setting the GOOS, GOARCH and SDK environment variables for iOS/arm64:

$ export GOOS=ios
$ export GOARCH=arm64
$ export SDK=iphoneos

For iOS Simulator, you’ll change the SDK environment variable for the iPhone simulator. Also the iPhone simulator will be running on your computer’s CPU, which might be an amd64 or an M1.

$ export GOOS=ios
$ export GOARCH=amd64
$ export SDK=iphonesimulator

A full list of supported GOOS and GOARCH flags is available on this Go (Golang) GOOS and GOARCH Gist.

Here’s a table of environment variables for various common platforms:

If you are compiling for iOS, you’ll also have to tell Xcode which SDK and which C architecture to compile for.

$ export SDK_PATH=`xcrun --sdk $SDK --show-sdk-path`
$ export CLANG=`xcrun --sdk $SDK --find clang`
$ export CARCH="x86_64" # if compiling for iPhone simulator
$ export CARCH="arm64" # if compiling for iPhone
$ exec $CLANG -arch $CARCH -isysroot $SDK_PATH -mios-version-min=10.0 "$@"

Then you can run the build command:

$ go build -o outputfilename.a /path/to/gofile/or/folder

You should see a new file, outputfilename.a in the working folder.

Build the Module and C Header

Cgo lets you export C Header files with your binary. This file is important because it allows you to call functions from your GoLang project as a module. You can do this by passing a -buildmode=c-archive parameter to the go build program:

$ go build -buildmode c-archive -o outputfilename.a /path/to/file

You’ll notice two new files:

  • outputfilename.a, the compiled binary.
  • outputfilename.h, the C header file.

This C-header file will include your exported function:

// ...
#ifdef __cplusplus
extern "C" {
#endif
extern char* sayHello();#ifdef __cplusplus
}
#endif

Trim the Fat

Finally, you can get rid of some unused symbols by passing -trimpath to the go build function. This will remove the project’s path information from the final binary:

$ go build -buildmode c-archive -trimpath -o outputfilename.a /path/to/file

You can read about this in the go help build documentation, under the -trimpath section:

 -trimpath
remove all file system paths from the resulting executable.
Instead of absolute file system paths, the recorded file names
will begin with either "go" (for the standard library),
or a module path@version (when using modules),
or a plain import path (when using GOPATH).

Putting It All Together

Now that you’ve read all that, this should make sense:

$ export GOOS=ios   # ios: ios, android: android
$ export GOARCH=arm64 # iPhone simulator: amd64
$ export SDK=iphoneos # iPhone simulator: iphonesimulator
$ export CGO_ENABLED=1
$ export CGO_CFLAGS="-fembed-bitcode"
$ go build -buildmode c-archive -trimpath -o outputfilename.a /path/to/file

This will produce two files that you will use as a module in your project:

  • outputfilename.a, the binary module
  • outputfile.h, which tells your compiler how to use the binary

Create an XCFramework

If you are building for an Apple product such as iOS, MacOS, iTV, or iWatch, you’ll want to create an XCFramework.

Since XCode 12 in 2019, XCFrameworks are the way that Apple organizes cross-platform binary frameworks. Essentially, an XCFramework is a folder containing binaries for each target platform, plus an Info.plist that describes which platform each binary is designed for.

You will want to cross-compile for one or more target platforms. For example if you are developing for iOS, you will compile for iPhone (arm64) and iPhone Simulator.

Supporting Intel and M1 Silicon on iOS Simulator

If you intend to deploy for iPhone Simulator on both Intel and M1 Silicon, you must make a fat binary using the lipo command. This will create a single binary with support for multiple architectures. Otherwise you can just use a single architecture and skip this step.

$ lipo \
outputfile_amd64_simulator.a \
outputfile_arm64_simulator.a \
--create \
--output outputfile_fat.a

Creating XCFramework

Let’s say you’ve created these two binaries and named them as outputfile_amd64.a and outputfile_arm64.a . You need to run xcodebuild -create-xcframework to bundle the different target binaries into a single framework. xcodebuild will auto-detect the architecture and platform to organize the framework.

$ xcodebuild -create-xcframework \
-output outputfile.xcframework \
-library outputfile_arm64.a \
-headers outputfile_amd64.h \
-library outputfile_amd64.a \
-headers outputfile_amd64.

Now you will have a single outputfile.xcframework and the outputfile.hheader file use in your XCode project.

If one of the libraries you included in the XCFramework is the fat library, it should support iOS Simulator on both Intel and Silicon.

Using your New Module

You will be able to include these files in a project, and then call the sayHello() method, for example:

#include <stdio.h>
#include "outputfile.h"
// ...
void main() {
printf(sayHello())
}

Or, if you are using XCode/Swift drag-and-drop your module’s outputfile.xcframework and outputfile.h files into your XCode project and interact with it like this:

// ... object or View code
sayHello()
// ... object or View code

Good luck!

--

--

Adonis Gaitatzis

Is a technology and branding nerd who can synthesize any business need into a technology solution. Fun projects have included brain imaging tech & mesh routers.