DEV Community

gerwert
gerwert

Posted on • Edited on

Calling Swift from Go

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
Enter fullscreen mode Exit fullscreen mode

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)!")
}

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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 from Package.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!
Enter fullscreen mode Exit fullscreen mode

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

Top comments (3)

Collapse
 
maxencecharriere profile image
Maxence Charriere

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:

▶ go build .
# github.com/maxence-charriere/murlok/internal/mac
ld: library not found for -lmac
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Enter fullscreen mode Exit fullscreen mode

(mac is replacing the go_swift name)

Here is the preamble:

/*
#cgo CFLAGS: -I./include
#cgo LDFLAGS: -L.build/debug/ -lmac
#include <stdlib.h>
#include "include/mac.h"
*/
import "C"
Enter fullscreen mode Exit fullscreen mode

I did swift build before.
Look like it doesn't find the lib.

Here is what swift build is generating:

murlok/internal/mac  swift ✗                                                                                  23m ⚑ ◒
▶ ls -l .build/debug/
total 32
drwxr-x---   4 maxence  staff    128 Apr 20 10:05 ModuleCache
drwxr-x---   3 maxence  staff     96 Apr 20 10:05 index
drwxr-xr-x  10 maxence  staff    320 Apr 20 10:05 mac.build
-rw-r--r--   1 maxence  staff    372 Apr 20 10:05 mac.swiftdoc
-rw-r--r--   1 maxence  staff  10844 Apr 20 10:05 mac.swiftmodule
drwxr-x---   3 maxence  staff     96 Apr 20 10:05 macPackageTests.product
Enter fullscreen mode Exit fullscreen mode

Do you have an idea of what the problem could be?

Collapse
 
gerwert profile image
gerwert

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 ?

Collapse
 
vjerci profile image
Vjeran • Edited

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:

go run -exec "env LD_LIBRARY_PATH=/Users/vjeranfistric/go/src/github.com/vjerci/swift-from-go" main.go
Enter fullscreen mode Exit fullscreen mode

or packing it into folder like this

swift-from-go.app/Contents/Frameworks/libgo_swift.dylib
Enter fullscreen mode Exit fullscreen mode

It did the trick for me