[Guide] Reusing Golang components in a SailfishOS app

Recently I wanted to use the Spliit api in both a server daemon and a client for SFOS, so I decided to give CGO (fancy name for a FFI in Golang) a try. This guide is focused more on the building side than the actual code.

First thing you obviously need to write the Go code you want to use. Second thing you need to create the C wrappers in Go.

If you’re entirely unkissed by Go and CGO, here’s an example:

package main

func Hello(name string) string {
  return "Hello, " + name
}

And here a CGO wrapper:

package main

import "C"

//export HelloC
func HelloC(name *C.char) *C.char {
  goStrName := C.GoString(name)
  result := Hello(goStrName)
  resultAsCString := C.CString(goStrName) // todo free this string

  return resultAsCString
}

This is quite a simplistic example and I can promise real code gets much more fun, especially when it gets to error handling, custom structs, freeing allocated memory and so on. If you want to take a look at how I did it in Spliit, here you go (I think I generally made some sane architecture choices, on the other hand I’m not really a C guy, but it works and it works well).

Building it

I tried building it in the Sailfish Build Engine VM but every binary produced by it simply causes Segmentation Fault, I don’t have the time nor the knowledge to fix this.

If you want to give it a shot, you can install go from my OpenRepos page.

This is the actual hard part. Or not if you know how to do it and are not discovering the joys yourself.

You need a cross-compilation toolchains for compiling C code for all architectures you’re interested in - x86 (emulator, tablet), arm64 (Xperia 10 III and newer), arm32 (older than Xperia 10 III).

The limiting factor here is the ancient version of glibc on SailfishOS - 2.30, released in 2019. Long story short, glibc is generally forward-compatible to the extreme, but backwards compatibility is not promised nor guaranteed. That means that software built on 2.30 will happily run on anything newer, but building it with newer glibc means it simply won’t run on SailfishOS.

And your PC (if it’s running Linux) is very likely running something newer (check by ldd –version), mine for example runs 2.42 released in 2025. As is any CI or anything you want to throw at it.

Nix to the rescue

Nix is a great and unique package manager. I won’t bother you with the details, but one of its inherent properties is that every software definition is described as a function and that function is committed to a git repository. Meaning with a bit of hunting you can find a commit which contains software for glibc 2.30, for example.

And the best part is you can put all that config into a single file that can be shipped with your repository and using a single command (nix develop) you’ll get into a shell with the exact dependencies.

If you want to install Nix on a non-NixOS computer, you can follow my guide for Steam Deck - it’s 99% equal to any other computer.

Then you can pull software from that commit, namely pkgsCross.gnu32, pkgsCross.armv7l-hf-multiplatform and pkgsCross.aarch64-multiplatform.

You can see a nix file with the exact config I’m talking about here: SpliitApi/flake.nix at master · RikudouSage/SpliitApi · GitHub

So download that flake.nix file in a directory and run nix develop in that directory and you’ll have all that’s needed configured and ready to go. Once you exit the terminal, everything’s back to normal (well, the software will still be on your disk, but it won’t clutter your $PATH until you run nix develop again).

It basically install all needed dependencies and then exports the paths to the compilers to three env vars:

  • CC_386
  • CC_ARMV7
  • CC_ARM64

Which then makes it possible to build using this command:

GOOS=linux GOARCH=386 CGO_ENABLED=1 CC=$CC_386 go build -buildmode=c-shared -o libwhatever.so path/to/file.go

You just replace the $CC_386 with $CC_ARMV7 or $CC_ARM64 and set GOARCH to arm or arm64. You might also need to set GOARM to 7 for the armv7 build.

This will compile both the .so and .h files, the header file can be directly included in a C++ project.

A note on the x86 toolchain: Nix basically contains instructions on how to build each software and the nix command knows how to turn these instructions into actual software. That would be long if you had to basically compile everything yourself, that’s why there’s a binary cache where you download the software from if it exists.

Sadly, the x86 toolchain is not fully in the Nix cache and when you run nix develop, it will download and compile it from sources which does take a long even on powerful computers. At the time of writing, arm32 and arm64 toolchains are still in the binary cache and thus a simple download is sufficient.

If you don’t need to build for x86, just delete all lines referencing 386 from the flake.nix file. If you need it (e.g. an emulator), just be prepare that the first time might take long because the toolchain itself needs to be compiled.

Integrating with C++

The integration itself is not that hard - the .h file can be included directly in a SailfishOS Qt project.

An example generated header can be downloaded from here (clicking the link will initiate download).

And then you simply call the functions exported from Go - using a trivial example from the beginning, it would look like this:

#include "hello.h"

#include <QDebug>

int main(int argc, char *argv[]) {
  auto result = HelloC("Dominik");

  qDebug() << result; // prints "Hello, Dominik"

  return 0;
}

If you want to see a real example, you can check my spliitapi.cpp and search for anything starting with Spliit_ prefix.


Hope I made a thing or two clearer for anyone going down that road. It’s especially powerful because a ton of useful stuff has been created in Go and it’s just a matter of importing any library/project you like, creating the C wrappers and importing them into your app.

4 Likes