Last Updated: February 17, 2023
·
13.38K
· 13k

Accessing Cocoa (Objective-C) from Go with cgo

It's pretty easy to use go's FFI capabilities via cgo. Go provides extensive and well written documentation about it:

http://golang.org/cmd/cgo/

https://github.com/golang/go/wiki/cgo

Now, while using exported C symbols is pretty straightforward, using Objective-C adds another level of indirection when accessing references in both languages.

First of all, your cgo code should be able to convert from Obj-C data (objects, method calls, etc) to Go data. The easiest way is by wrapping everything Obj-C in C code. When you have the converted C data, it's a matter of converting it to Go.

So what one needs to figure out is how to convert from Objective-C to C. I'm not experienced with Obj-C, so I had to do some research first, but anyone familiar with it should be able to easily do this.

The second issue is how to compile and link Obj-C with the Go code. Fortunately, this is pretty easy since you can set the compiler cgo will be using by setting the CC environment variable. By simply using CC=clang and setting CFLAGS: -x objective-c within the Go files, you're all set to compile Obj-C code.

A very simple "hello world" example that should work is:

compile with CC=clang go build

package main

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void hello() {
    NSLog(@"Hello World");
}
*/
import "C"

func main() {
    C.hello()
}

This should output "Hello World" using Foundation Framework's NSLog.

In my exercise, I tried to dynamically get the user's "Application Support" directory using Foundation Framework's NSFileManager.

In Objective-C, this would be done like this:

// get the default file manager
NSFileManager *manager = [NSFileManager defaultManager];
// get all possible app support directories in the user domain
NSArray* urls = [manager URLsForDirectory: NSApplicationSupportDirectory inDomains: NSUserDomainMask];

Note that in the above case, urls is a NSArray containing NSURL objects.

So far so good. I'd have to wrap that call in a regular C function and then use the data.

Now I think is the tricky part: how to convert from Obj-C objects to Go objects. What I came up with was this:

main.go

package main

import (
    "fmt"
    "net/url"
    "strconv"
    "unsafe"
)

//#cgo CFLAGS: -x objective-c
//#cgo LDFLAGS: -framework Foundation
//#include "foundation.h"
import "C"

// NSString -> C string
func cstring(s *C.NSString) *C.char { return C.nsstring2cstring(s) }

// NSString -> Go string
func gostring(s *C.NSString) string { return C.GoString(cstring(s)) }

// NSNumber -> Go int
func goint(i *C.NSNumber) int { return int(C.nsnumber2int(i)) }

// NSArray length
func nsarraylen(arr *C.NSArray) uint { return uint(C.nsarraylen(arr)) }

// NSArray item
func nsarrayitem(arr *C.NSArray, i uint) unsafe.Pointer {
    return C.nsarrayitem(arr, C.ulong(i))
}

// NSURL -> Go url.URL
func gourl(nsurlptr *C.NSURL) *url.URL {
    nsurl := *C.nsurldata(nsurlptr)

    userInfo := url.UserPassword(
        gostring(nsurl.user),
        gostring(nsurl.password),
    )

    host := gostring(nsurl.host)

    if nsurl.port != nil {
        port := goint(nsurl.port)
        host = host + ":" + strconv.FormatInt(int64(port), 10)
    }

    return &url.URL{
        Scheme:   gostring(nsurl.scheme),
        User:     userInfo, // username and password information
        Host:     host,     // host or host:port
        Path:     gostring(nsurl.path),
        RawQuery: gostring(nsurl.query),    // encoded query values, without '?'
        Fragment: gostring(nsurl.fragment), // fragment for references, without '#'
    }
}

// NSArray<NSURL> -> Go []url.URL
func gourls(arr *C.NSArray) []url.URL {
    var result []url.URL
    length := nsarraylen(arr)

    for i := uint(0); i < length; i++ {
        nsurl := (*C.NSURL)(nsarrayitem(arr, i))
        u := gourl(nsurl)
        result = append(result, *u)
    }

    return result
}

func UserApplicationSupportDirectories() []url.URL {
    return gourls(C.UserApplicationSupportDirectories())
}

func main() {
    fmt.Printf("%#+v\n", UserApplicationSupportDirectories())
}

foundation.h

#import <Foundation/Foundation.h>

typedef struct _NSURLdata {
    NSString *scheme;
    NSString *user;
    NSString *password;
    NSString *host;
    NSNumber *port;
    NSString *path;
    NSString *query;
    NSString *fragment;
} NSURLdata;

const char* nsstring2cstring(NSString*);
int nsnumber2int(NSNumber*);
unsigned long nsarraylen(NSArray*);
const void* nsarrayitem(NSArray*, unsigned long);
const NSURLdata* nsurldata(NSURL*);
const NSArray* UserApplicationSupportDirectories();

foundation.m

#import "foundation.h"

const char*
nsstring2cstring(NSString *s) {
    if (s == NULL) { return NULL; }

    const char *cstr = [s UTF8String];
    return cstr;
}

int
nsnumber2int(NSNumber *i) {
    if (i == NULL) { return 0; }
    return i.intValue;
}

unsigned long
nsarraylen(NSArray *arr) {
    if (arr == NULL) { return 0; }
    return arr.count;
}

const void*
nsarrayitem(NSArray *arr, unsigned long i) {
    if (arr == NULL) { return NULL; }
    return [arr objectAtIndex:i];
}

const NSURLdata*
nsurldata(NSURL *url) {
    NSURLdata *urldata = malloc(sizeof(NSURLdata));
    urldata->scheme = url.scheme;
    urldata->user = url.user;
    urldata->password = url.password;
    urldata->host = url.host;
    urldata->port = url.port;
    urldata->path = url.path;
    urldata->query = url.query;
    urldata->fragment = url.fragment;
    return urldata;
}

const NSArray*
UserApplicationSupportDirectories() {
    NSFileManager *manager = [NSFileManager defaultManager];
    return [manager URLsForDirectory: NSApplicationSupportDirectory
                           inDomains: NSUserDomainMask];
}

So you see, there's a lot of boilerplate code only to convert NS* objects to Go objects, ranging from simple strings (NSString* -> char* -> Go string) to arrays and other objects (NSURL*, for example). For NSURL in this case, I choose a very poor solution in creating a C struct to get its data and avoid writing a C function for each NSURL property. I'm pretty sure there's a better solution, like trying to implement in C a function to call any Obj-C object property, but my lack of knowledge in Obj-C prevented me from doing so. Maybe even creating an url.URL Go object in C.

Also I'm not sure how to handle memory management yet and I'm almost sure the malloc memory is leaked. Also I'd be wanting to check out how Obj-C's memory management would fair alongside Go's garbage collection. My guess is the safe option would be to copy over all data to Go when creating Go objects (like C.GoString does) and let Obj-C handle its own memory. That way both keep separate memory management.

And if you are maybe intrigued if would be possible to write Cocoa applications in Go, you're right, it is possible.

With this initial and simplistic integration, it's right to think it would be perfectly possible to have an entire language binding between Obj-C/Cocoa and Go. It would be a pretty big job, but doable.

1 Response
Add your response

Nice Article. I wanted to compile hello code from Ubuntu machine with cgo for MacOS. Is it possible? If possible can you guide me?

over 1 year ago ·