Both Swift and Go are modern compiled languages. They have their own typical use cases, strengths and weaknesses. Would it not be great to combine strengths of both languages in a single project?
One possible use case is when you want to use Cocoa API's (native to macOS) such as NSUserNotificationCenter
in an existing Golang code base. This can be done via Objective-C as intermediate language. But I very much prefer Swift over Objective-C.
This "best of both worlds approach" has some caveats of course, but we'll come to that later. First the fun part.
A library as a bridge
There is no direct interoperability between Swift and Go. But what we can do, is using a generic way to link binaries written in arbitrary languages: shared libraries. Or more specific in this case: dynamic libraries.
Prerequisite is that both languages support the same calling convention. Both Golang and Swift support creating and linking to libraries that use the cdecl
calling convention, which originates from the C programming language. If we build a Swift library that exports functions with cdecl calling convention, we should be able to call those functions from Go.
We have to make sure to explicitly export functions with the cdecl convention in Swift. Calling Swift functions in a "normal" Swift library from Go is not possible, because Swift and Golang have their own (and different) calling conventions.
But even if both languages would use the same calling convention, we would need some way of converting argument and return types from one language to the other. For example, a string
in Golang is completely different from a String
in Swift. So we would not be able to simply pass strings between Swift and Golang without some kind of layer in between.
In our case that layer is a dynamic C library. Thus, we'll use C as lingua franca, which is common when bridging programming languages.
Now, let's look at the steps needed create a C library from Swift code that can be called from Go.
Creating a Swift library
Let's create a very simple Swift library, that exports a single function that prints a string. First step is initialise our project using Swift Package manager:
$ swift package init --type library
Now we can create a Swift file that contains our function: ./Sources/go-swift/go_swift.swift
.
In this file we declare one function: sayHello
. This function
accepts a pointer to a C string as the only argument:
@_cdecl("sayHello") // export to C as `sayHello`
public func sayHello(namePtr: UnsafePointer<CChar>?) {
// Creates a new string by copying the null-terminated UTF-8 data (C String)
// referenced by the given pointer.
let name = String(cString: namePtr!)
print("Welcome, \(name)!")
}
To make this Swift function accessible from C, we have to add a @ _cdecl
attribute to it.
@_cdecl
is an undocumented and unsupported attribute (so might not be available anymore in future Swift versions), which gives a Swift function a "C name" in the resulting library. It tells the compiler not to mangle the function name (as it does by default), ensuring our function name is exported as if it had been written in C.
Also notice that we can't use the build-in Swift String
as type for our argument, because that is not a C compatible type. Therefore we're dealing with raw pointers to C-style null-terminated strings: UnsafePointer<CChar>?
.
Call library from Go
After building the swift library via $ swift build
, we can try to call the sayHello
function in that library.
To call our function from Go, we need cgo. Cgo acts as a bridge between C and Go.
But before we can link the Swift library to our Go program using cgo, we need to create a C header file: ./include/go_swift.h
. Otherwise our Go program is not aware of the C function signature of sayHello
.
Since we only have a single function, our header file can consist of a single line:
void sayHello(char* name);
With $ swiftc -emit-objc-header <swift file>
you can generate an Objective-C header automatically. But as far as I'm aware, there is no automatic generation for plain C header. So we'll have to create the C
header file manually.
For simple functions this is relatively straightforward. For more complex functions you might need (lots of) trial and error. I didn't go as far for example to test if it's possible to pass a pointer to a function as parameter.
After defining the header, it's time to write a small Go program:
package main
/*
#cgo CFLAGS: -I./include
#cgo LDFLAGS: -L.build/debug/ -lgo_swift
#include <stdlib.h>
#include "go_swift.h"
*/
import "C"
import "unsafe"
func main() {
// Copy Go string to C string
cstr := C.CString("Bob")
// Call Swift function `sayHello`
C.sayHello(cstr)
// The C string is allocated in the C heap using malloc.
// Therefore memory must be freed when we're done
C.free(unsafe.Pointer(cstr))
}
In our Go file, we import the C package and declare a preamble. From the cgo documentation:
To use cgo write normal Go code that imports a pseudo-package "C". The Go code can then refer to types such as C.size_t, variables such as C.stdout, or functions such as C.putchar.
If the import of "C" is immediately preceded by a comment, that comment, called the preamble, is used as a header when compiling the C parts of the package. For example:
In the preamble, we need to set some linker flags:
-
cgo CFLAGS: -I./include
: set path to header files -
cgo LDFLAGS: -L.build/debug/ -lgo_swift
sets the location in which to look for library files, and which libraries to link to.go_swift
is library the name fromPackage.swift
. The library location is path that contains the output from the Swift build process.
Finally, we can build an run our Go program:
$ go build -o hello
$ ./hello
Welcome, Bob!
This works fine! But, as mentioned before, there are several issues with this approach, compared to "normal" development in either Golang or Swift.
Memory management
Both Swift and Go have automatic memory management, such as memory allocation and garbage collection. Therefore, normally when writing code in Swift or Go we don't need to worry about things like memory leaks and buffer overflows.
In order for automatic memory management to work, a runtime needs to be fully in charge of all memory allocated and to know the location of all pointers. That means Swift's runtime can't manage pointers created by Go's runtime, and visa versa.
Because of this, there are restrictions when passing pointers via a shared library. Go pointers passed to C code must be pinned for the duration of the C call, and C code must stop storing the Go pointer when the C function returns. If you don't strictly adhere to these and other rules, things can become messy quickly.
Bottom line is you must be careful and cautious about memory management. That's a big disadvantage, because you'll lose the convenience and safety of automatic memory management when using functions from a shared library. The type name UnsafePointer
we have to use in the Swift code says it all.
Performance
Using C as an intermediate language means we need to convert Go types to C types, C types to Swift types, and back again. This, amongst other things, causes overhead that will impact application performance. For simple programs this might not be a problem at all. But when performance matters, there is always a better solution than bridging languages via a shared library.
Cross-platform support
While Go is available on many platforms, Swift is not. By linking a Go program to a Swift library you'll loose the broad platform support that Golang offers.
So, is this a good idea?
"Yes", because it's fun (!) and bridges a gap between Golang and native MacOS frameworks. But also "No", because, because calling Swift from Go creates issues that would not be there when using a single language.
In other words, whether or not choosing the Golang/Swift route is for you, depends on your project and if there is another way to achieve what you want. For example, MacOS APIs can be called using Objective-C as intermediate language instead.
But, that aside, it is something definitely worth experimenting with.
Repository with example code available on Github.
Read more
- The other way around:
Top comments (3)
Hello, I tried to reproduce what is described in this blog post.
I'm using swift 5 and go1.12.4
When I try to compile, I got:
(mac is replacing the go_swift name)
Here is the preamble:
I did swift build before.
Look like it doesn't find the lib.
Here is what swift build is generating:
Do you have an idea of what the problem could be?
I had a similar error in a different project, after upgrading MacOS (and Swift to Swift 5?). Solved it by deleting
.build
directory, and re-building the project.Did you try the example project github.com/onderweg/swift-from-go ?
I've tried your solution but for some reason it doesn't seem to work
all the time it reports error image missing or something like that
then i've stumbled upon this article
ardanlabs.com/blog/2013/08/using-c...
figured out .dylib is missing
and then added it like this:
or packing it into folder like this
It did the trick for me