gopy: extending CPython with Go
Applications and libraries do not live in a vacuum.
This is of course true for Go packages and commands.
More often than not, you need your code to interact with legacy applications or
old and battle tested libraries.
The standard library of Go provides many facilities for interacting with such
entities: encoding/json, encoding/xml or net/rpc.
Reach out of the standard library and you have ProtoBuf.
All of these facilities meant to arrange for some kind of IPC (Inter-Program
Communication) and before Go-1.5, this was the only portable way to do so.
With Go-1.5 and the new execution
modes
it is now possible to create C shared libraries from a Go package: that’s
performed via the -buildmode=c-shared command-line option.
As you have seen in libc-hooking-go-shared-libraries, creating a C shared library is not particularly complicated but the process does involve a few steps.
gopy automates the drudgery work of
creating a CPython-2 C-extension module out of a Go package: eventually
allowing you to write nice concurrent libraries in Go and share them with your
CPython friends.
Given a Go package, gopy will:
- inspects the
Gopackage - extracts the exported
types,funcs,varsandconsts - creates a
cgopackage that exports these entities toC - creates a
Cextension module, using theCPython-2API, calling thesecgoexported entities - compiles everything together into a
.soshared object
Installation
gopy is a pure-Go command and can thus be installed like so:
|
|
Of course, at runtime, for gopy to be able to actually generate the CPython
extension module, it will need:
- the
python-devpackage (which contains theCPythondevelopment headers andlibpython.so.2.X) pkg-configand the correspondingpython2.pcconfiguration file which holds and describes the correctCincantation commands (-I,-Land-l)- and, finally, a
Ccompiler.
Example
Consider the following (fictious) github.com/me/hello package:
|
|
To create a CPython extension module out of it, you would run:
|
|
The hello CPython extension module can then be imported from your favorite
CPython-2 interpreter like any other module:
|
|
|
|
gopy extracted all the Go documentation strings and attached them to their
python counterpart.
gopy also created a function Hello, translated the Go arguments into their
native python counterpart.
If you were curious enough to look under the hood of what gopy generated, you
would find:
- a
Gofile, - a
Cheader file, and a
Csource file.1 2 3 4 5sh> ll /tmp/gopy-012085634 total 12K -rw-r--r-- 1 binet binet 4.0K Dec 7 17:23 hello.c -rw-r--r-- 1 binet binet 2.5K Dec 7 17:23 hello.go -rw-r--r-- 1 binet binet 1.7K Dec 7 17:23 hello.h
In the Go file:
|
|
gopy generates a cgo function which wraps the original Go function
hello.Hello and also cgo-export (as cgo_func_hello_Hello) so it is
callable from C.
The C side looks like this:
|
|
The generated C code uses the CPython API to create a CPython module, fill
the ad hoc structures and connect the cgo functions with the CPython
infrastructure, together with the python docstrings.
Implementation notes
gopy, like gomobile, has the
interesting task of binding two languages together, each of them with a garbage
collector.
Thus, gopy needs to arrange somehow for the bindings to setup a kind of
protocol so that each garbage collector knows when a foreign value is not needed
anymore.
To that end, gopy notes when a Go value crosses the python boundary and
increments a counter so the Go garbage collector will not try to collect it
while the python side has a hold on it.
On the python side, the C extension module is generated in such a way that
the python garbage collector will tell the Go side when that value is not
needed anymore, as far as the python side is concerned.
As a consequence, any Go value that somehow crosses the language boundary
needs to be allocated on the heap: gopy takes care of that while generating
the cgo package.
Schematically, a call from python to a Go function looks like:
1 2 3 4 5 6 |
// Call sequence (lots of hand-waving)
-> python: hello.Hello("foo")
-> cpython: cpy_func_hello_Hello(...)
-> cgo: cgo_func_hello_Hello(...)
-> go: hello.Hello(...) |
so, while gopy will help bring some of the Go goodies to python, the
wrapped entities should not be called inside a very tight loop.
Another interesting point to note is that, as gopy uses cgo, it is subject
to the new and more stringent rules that are devised for Go-1.6.
As a consequence, some internal parts of gopy will need to be modified to
generate code that follows these news.
Unfortunately, this probably means another slight performance hit with regard to
what could be achieved with Go-1.5.
gopy is currently able to wrap struct types, named types, slice types and array
types, as well as the methods which are attached to it.
gopy is also clever enough to translate functions using the comma-error idiom into their python equivalent (ie: a function raising an Exception.)
Exported vars and consts are also handled: gopy generates “getters” and
“setters” for the former and only “getters” for the latter, to preserve the
semantics of the original Go package.
For slices and arrays, gopy generates code that implements the sequence and
buffer protocols.
Hence, gopy strives to generate types that look like idiomatic python classes.
Limitations
gopy is still a very young project and many features are still missing.
It currently does not support:
- exposing
map[T]Utypes - exposing
chan Ttypes - exposing
interfaces(excepterror) - implementing a
Gointerface frompython - exposing functions or methods taking pointers to values
- wrapping a package which exposes as part of its API types from another package.
Also, currently, gopy generates code only for the CPython-2 API, even if
there are no known showstoppers to support CPython-3 or other python VMs.
gopy.py
gopy ships with a little python module – gopy.py – to test and bind
interactively any Go package:
|
|
gopy.py calls the gopy bind command to generate and compile the Go package
and then imports the generated C extension module into the current
interpreter.
This effectively makes every single Go package available from the python
interpreter (as long as it is supported by gopy, of course.)
Conclusions
Thanks to the simple rules of the Go language and the packages available with
the standard library, it is possible and reasonnably easy to automatically and
programmatically:
- inspect a
Gopackage, - extract the exported API and
- generate bindings for a foreign language,
python.
It is not unreasonnable to imagine Go to become the perfect low-level
companion language for python, thanks to its quick development cycle and its
runtime performance, especially in the concurrent programming space.
gopy is a young project, recently made possible thanks to Go-1.5 and there
is still a lot of work to support the whole Go language.
gopy is released under the BSD-3 license and welcomes bug reports,
contributions or new ideas: join us here.
gopy may be just a first step towards a tool suite akin to
what SWIG is for C/C++ but for Go.
This would help the percolation of Go through “foreign” code bases and
strengthen Go’s case as a C replacement.
I encourage people interested in this idea to join us at the
go-binder organization.