Writing Apps in Go and Swift

Go makes it easy to create safe, reliable and efficient software. Concurrency is part of the language, making otherwise complicated code more intuitive to write. It can compile binaries for any non-obscure platform and has a quite capable standard library with a lively developer community.

Although Swift is cross platform, it’s perhaps most commonly used to develop apps for Apple’s platforms. Maybe I’m just not very clever, but even after years of using Grand Central Dispatch (“GCD”), I still find it hard to write maintainable multi-threaded code1 for macOS or iOS. Although GCD offers a great improvement over how asynchronous code was written before Snow Leopard,2 I couldn’t help but wonder what it would be like if I could focus on creating and designing APIs without having to worry about the minutiae of parallelism (threads, semaphores, locks, barriers, etc.).

All that to say, when I discovered a straight-forward and performant way to call Go code from Swift, it felt like I unlocked new developer super-powers!

As a demonstration, let’s build a library to escape/unescape HTML tags in Go and call it from Swift. This technique should work regardless of the platform (iOS, macOS, Linux, …), but for simplicity, this post will target macOS.

If you’re curious to see an example of such a hybrid app, check out Emporter on the Mac App Store.

There’s a complementary project hosted on GitHub if you’re a “hands on” learner.

Writing a Go library

Background

It’s a pretty well-known feature that Go can call C code, but since Go 1.5, it’s also possible to call Go code from C. The go build command has a buildmode flag to indicate what type of object should be built.

From go help buildmode:

-buildmode=c-archive
   Build the listed main package, plus all packages it imports,
   into a C archive file. The only callable symbols will be those
   functions exported using a cgo //export comment. Requires
   exactly one main package to be listed.

So what does this mean exactly? Well, if we can compile Go to C, and embed C libraries in our Mac app… well, I think we just found our golden ticket!

Write a C archive

To write a C library in Go, we need to use cgo, the bridge between C and Go. For now, it’s enough just to know that the C package can convert Go values to and from C types, and vice-versa. If you want to dive-in a little deeper, the Go authors have written an excellent post about cgo on the Go Blog.

As mentioned previously, to build a C archive, we need to create a main package and mark each method we want to export with a preceding //export cgo comment.

The entire library would look something like this:

package main

import (
	"C"
	"html"
)

//export escape_html
func escape_html(input *C.char) *C.char {
	s := html.EscapeString(C.GoString(input))
	return C.CString(s)
}

//export unescape_html
func unescape_html(input *C.char) *C.char {
	s := html.UnescapeString(C.GoString(input))
	return C.CString(s)
}

// We need an entry point; it's ok for this to be empty
func main() {}

Notice that we also had to convert between C and Go strings using cgo, and only exposed C types in the method signatures.

Compile the archive

Assuming you’re in the same directory as the Go source, the library can be compiled using the following command:

go build --buildmode=c-archive -o libhtmlescaper.a

We’ve specified an explicit name and extension to use for our library, which helps makes it a little easier to bundle for use in Xcode. The build will also output a generated header3 libhtmlescaper.h which exposes all of the exported functions / types available when linking the archive.

Calling Go from Swift

Create a module map

The easiest way to use our compiled library from Swift is to create a module map. Once setup correctly (which honestly, can be painful), our library will be automatically linked, with its headers included, when imported.

Here’s what part of our module.modulemap might look like:

module HTMLEscaper {
    header "libhtmlescaper.h"
    link "htmlescaper"
    export *
}

If you don’t already have module maps setup for your project, you should save your module map in your Xcode project’s $(SRCROOT) (the same directory as your .xcodeproj file). Afterwards, you’ll need to update your target’s build settings: set LIBRARY_SEARCH_PATHS and SWIFT_INCLUDE_PATHS to $(SRCROOT).

I’ll admit, there can be little bit of friction here, but no more than if you were to use other third-party libraries in Swift.

Create a wrapper

If we’ve setup our module correctly and Xcode is on its best behavior, all we need to do is import it.

Here’s what it might look like if we wrote a String extension to escapes HTML using our library:

import HTMLEscaper

extension String {
    public func escapedHTMLString() -> String? {
        return self.withCString() {
            guard let v = escape_html(UnsafeMutablePointer(mutating: $0)) else { return nil }
            return String(bytesNoCopy: v, length: strlen(v), encoding: .utf8, freeWhenDone: true)
        }
    }

    public func unescapedHTMLString() -> String? {
        return self.withCString() {
            guard let v = unescape_html(UnsafeMutablePointer(mutating: $0)) else { return nil }
            return String(bytesNoCopy: v, length: strlen(v), encoding: .utf8, freeWhenDone: true)
        }
    }
}

And that’s it! Our Go library is now just an implementation detail, and the Swift API feels right at home.

Was that really worth it?

Really, it depends on your project.

For Emporter, its backend services are written in Go. By writing the client in Go, I have an easy way to run tests, without mocks, instantaneously. I seriously can’t imagine having written it differently as a one-person project, based on the amount of time I’ve saved by keeping all of the networking code in a single repo (then exporting the client as a C library).

And if I ever grow enough to hire, expand to a different platform, or license the service, I’m ready: its core can be developed independently and works cross-platform.

Give Emporter a try and let me know how it compares to an Electron app. 😉

Conclusion

In this article, we’ve written a simple Go library which was embedded in a native Mac app. Although we’ve focused on macOS, this technique will work for any platform that Go supports with C bindings.

You can download an example project on GitHub.


  1. The subtleties required to implement coordination between routines and access to shared variables, especially after periods of inactivity in the codebase, is hard. ↩︎︎
  2. Back in my day, we called macOS “OS X”. And we managed NSThread and NSRunLoop instances ourselves. Get off my lawn! 👴 ↩︎︎
  3. The generated header is not very easy to read. In real projects, I tend to write my own headers for well-documented code. ↩︎︎