Go 1.5: Calling Go shared libraries from Firefox addons
Go 1.5 has shipped a long awaited feature, the ability to build shared libraries (.so, .dll) that can be called using the C ABI.
Graham King already made a pretty good intro on doing this from C and C++ (Part 1, Part 2).
I’m excited by this because you often run into the limitations of your environment when building things - you might be lacking APIs or libraries, be limited by performance or dreading introducing external dependencies. Often, a plugin in C might be an option, but you don’t trust yourself to safely use C.
This was certainly the case when I was looking at writing a password manager browser addon. Looking at the Web Crypto API was doing my head in and lacked the right combinations of features I needed. “I wish I could just do this in Go”, I thought to myself.
If you want to just see what a project looks like, you can find the experiment on Github, or scroll down for a video of the addon in action.
Building a shared library
Best off reading the docs or the above linked posts, but basically this just involves annotating the functions you want to export:
//export Connect
func Connect(connectionString string) string {
return ""
}
and building with go build -o firefox-mysqladmin.so -buildmode=c-shared
, which generates a shared library and header file.
From the name, you can guess that I’m trying to make a MySQL administration interface Firefox Addon with the meat written in Go - there’s no way to natively talk to MySQL outside of native code. I think if I was to do this for real I would just use libmysql
, though.
We’ll add a couple of other functions:
func Disconnect() string
func SelectQuery(query string) (string, string)
What Go types look like
You can immediately see that I’m using the Go string
type where I would usually use an error
. Something I did not look into deeply was how to interface with the Go error
type, because it is an interface and thus generates header that looks like:
extern GoInterface MyFunc()
typedef struct { void *t; void *v; } GoInterface;
Eh, not sure. On the other hand, GoString
is fairly simple (at least, it’s somewhat typed):
typedef struct { char *p; GoInt n; } GoString;
The other case I ran into that requires some care is multiple-return functions. Since there is no analogue in C, we are instead given a struct
which contains all the return values together. This appears in the header as:
/* Return type for Blah */
struct Blah_return {
GoString r0;
GoInterface r1;
};
extern struct Blah_return Blah();
Talking to Go from Firefox
Creating an addon is a lot easier than it used to be, since Mozilla has introduced the jpm tool to generate and develop projects.
The specific part of the Addon SDK that we need is js-ctypes.
Getting the Firefox runtime to actually load our shared Go library is a little nightmarish as far as the API goes, but essentially we will bundle the binary with the addon package, and find the Chrome URL for it.
See ctypes_loader.js for specific code in this instance.
With a handle on the library from the Addon SDK, we can begin to define the functions we want to be interfacing with. For the functions we declared earlier, we have (Mozilla docs on this):
var connect = lib.declare("Connect", ctypes.default_abi, GoString, GoString);
var disconnect = lib.declare("Disconnect", ctypes.default_abi, GoString);
var selectQuery = lib.declare("SelectQuery", ctypes.default_abi, SelectQueryResult, GoString);
and the type definitions that go with them:
var GoString = new ctypes.StructType("GoString", [
{ "p": ctypes.char.ptr },
{ "n": ctypes.int }
]);
var SelectQueryResult = new ctypes.StructType("SelectQuery_return", [
{ "r0": GoString }, { "r1": GoString }
]);
(Field names copied from header). Not too pretty, but it’s not THAT much boilerplate either.
We need to construct GoStrings by hand, and unwrap the incoming GoStrings as well:
var makeGoString = function(str) {
var gs = new GoString();
gs.p = ctypes.char.array()(str);
gs.n = str.length;
return gs;
};
var SelectQuery = function(qry) {
var res = selectQuery(makeGoString(qry));
if(!res.r1.p.isNull()) {
return res.r1.p.readString();
}
return res.r0.p.readString();
}
After enjoying ostensibly non-nillable strings in Go it felt a bit sad to be doing this.
I am not clear on the safety of reading the GoStrings on the JS side. Are they copied, or is the memory still managed by the Go runtime? Should we be providing a buffer for the exported functions to write into? Input welcome here.
Taking it for a spin
Eh, works well enough. If I wasn’t supremely lazy I would make it look nice and display errors separately.
Gotchas (or, oh man, how does this actually work?)
- If the Go runtime panics, Firefox just exits. People who are familar with what panicing actually does (stack unwinding and all) will probably know why.
- Unclear of safety of memory that is allocated by Go and read by JavaScript.
- Binary bloat. If you want a cross-platform addon, you need to ship 2-3 shared libraries, and load the correct one for the user platform. The Linux version of my test addon clocked in at a whopping 8.1mb, so 3 of them is going to hurt very badly. Still, it is possibly better than requiring
libmysql
to be installed. - I am not sure what the AMO policy is regarding accepting addons with native libs shipped with them.
- Like with all things native, it’s probably very very easy to screw this up (and I likely have along the way). However, the number of fuckups you can make is (probably) smaller than doing the whole thing natively.
Despite all that, I’m still happy to have access to the epic Go stdlib from the browser now.