Saturday, April 24, 2010

Callbacks with cgo

A major limitation of cgo is the lack of support for callbacks or, in more general terms, the ability for C to call a Go function.  This is a major issue for bindings to event driven systems.  As part of impending support for SWIG, however, the Go team has expanded cgo to provide a facility by which Go functions may be exposed to C.

In one's cgo files, exposing a function is a very simple matter: one precedes the function header with a comment starting with the word export and followed by the function name.  For example:

//export Foo
func Foo() {
    // ...
}

When cgo sees this it generates two new C files: _cgo_export.c and _cgo_export.h.  These files provide the wrappers that enable a C program to call Go functions.

Provided with the current distribution of Go is an example program demonstrating this new feature.  It is found in:

$GOBIN/misc/cgo/life

Looking through that example, though, one should notice one problem: it is not an example classic callbacks.  That is, Go is not passing functions to C.  Instead, the functions are merely being exposed so that C can call them.  Thus the problem remains for event driven libraries such as GLUT.  Callbacks are achieved via a bit of indirection.

Let us start by creating a very simple library that adds number via a callback.  It has a single function, call_add, which takes a function pointer and calls it to add two integers.

// calladd.h

typedef int(*adder)(int, int);

extern void call_add(adder);

//callback.c

#include <stdio.h>
#include "calladd.h"

void
call_add(adder func) {
    printf("Calling adder\n");
    int i = func(3, 4);
    printf("Adder returned %d\n", i);
}

Nothing to it.  Compile it and turn it into a shared object:

gcc -g -c -fPIC calladd.c
gcc -shared -o calladd.so calladd.o

Now here's the interesting part: in order to pass a Go function to C we must create a wrapper that passes the wrapper to the Go function.  Wonderful, eh?  Two levels of indirection. . .

// funcwrap.h

extern void pass_GoAdd(void);

// funcwrap.c

#include "_cgo_export.h"
#include "calladd.h"

void
pass_GoAdd(void) {
    call_add(&GoAdd);
}

Our cgo file is rather simple:

//callback.go

package callback

// #include "callgo.h"
// #include "funcwrap.h"
import "C"

func Run() {
    // call the wrapper
    C.pass_GoAdd()
}

//export GoAdd
func GoAdd(a, b int) int {
    return a + b
}

This is still not really a callback, as we are not passing an arbitrary function.  That would require yet another level of indirection (for a total of three), whereby we store a function pointer and call it within GoAdd.  Hopefully in the future one level will be removed.  I suspect that this could be possible if cgo interpreted any passing of an exported function to a C function as passing the wrapper, but I cannot say that I have looked deeply into the matter.

To conclude, here's the Makefile for this project (a modified version of the life example's):

CGO_LDFLAGS=_cgo_export.o calladd.so funcwrap.so $(LDPATH_linux)
CGO_DEPS=_cgo_export.o calladd.so funcwrap.so

CLEANFILES+=main

include $(GOROOT)/src/Make.pkg

funcwrap.o: funcwrap.c _cgo_export.h
    gcc $(_CGO_CFLAGS_$(GOARCH)) -g -c -fPIC $(CFLAGS) funcwrap.c

funcwrap.so: funcwrap.o
    gcc $(_CGO_CFLAGS_$(GOARCH)) -o $@ funcwrap.o $(_CGO_LDFLAGS_$(GOOS))

main: calladd.so install main.go
    $(GC) main.go
    $(LD) -o $@ main.$O

2 comments:

  1. it is a good mind gymnastic, but quite painful too... I hope that in the future some steps will be made internal
    regards

    ReplyDelete
  2. I was thinking it worked like this..too bad I discovered it does!

    By the way, the Makefile does not work out of the box for me, but I am hacking around it to make it compile.

    Thanks for the explanation!

    ReplyDelete

Followers