Compile GoLang as a Mobile Library
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" {
#endifextern 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 moduleoutputfile.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.h
header 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!