Last week I briefly reported on Google+ that I had written a Go-based Native Client module, built
it with llgo, and successfully loaded it into Google Chrome. I'd like
to expand on this a little now, and describe how to build and run it.
Before your start...
If you want to
want to try this out yourself, then you'll need to grab yourself a
copy of the Native Client SDK. I've only tested this on Ubuntu Linux
12.10 (x86-64), so if you're trying this out on a different OS/arch
you may need to alter the instructions.
Anyway, grab the SDK according to the instructions on the page I linked to above. Be sure to get the devevelopment/unstable branch, by updating with the "pepper_canary" target:
$
cd nacl_sdk; ./naclsdk
update pepper_canary
This is not a
small download, so go and brew some tea, or just read on to see where
we're going with this.
The anatomy of a PNaCl module
By
now I guess you probably know what
Native Client is, but
if you don't, I suggest you take a moment to read about it on the
Google Developers (https://developers.google.com/native-client/)
site.
What may not be so
well known is PNaCl, the next evolution of Native Client. PNaCl
(pronounced pinnacle), is short for Portable
Native Client, and
is based on LLVM.
Developers
continue
to write
their code the same as in traditional NaCl, but now it is compiled to
LLVM bitcode;
PNaCl
restricts usage to a portable subset of bitcode so
that it can then be translated to native x86, x86-64, or ARM machine
code. To
compile C or C++ modules to PNaCl/LLVM bitcode, one uses the
pnacl-clang compiler provided with the Native Client SDK.
To
make use of Native Client, one develops a module,
which is an executable, that can be loaded into Google Chrome (or
Chromium).
A
module implements certain functions specified in the Pepper API
(PPAPI),
which is the API that interfaces your module with
the browser. One
of the functions is PPP_InitializeModule,
and another is PPP_GetInterface.
The
former provides a function pointer to the module for calling back
into the browser; the latter is invoked to interrogate the module for
interfaces
that it implements.
A nacl/ppapi package for Go
Since llgo speaks LLVM, it should be feasible to write PNaCl modules
in Go, right? Right! So I set about doing this last week, and found
that it was fairly easy
to do. I have written a demo module which you can find here:
https://github.com/axw/llgo/tree/master/pkg/nacl/ppapi,
which I later intend to morph into a reusable Go package, with a
proper API. I have made a lot of shortcuts, and the code is not
particularly idiomatic
Go; bear in mind that llgo is still quite immature, and that this is
mostly a proof of concept.
Most of the code in the package is scaffolding; the example module is
mostly defined in example.go, some also in ppapi.go. At
the top of example.go, we instantiate a pppInstance1_1, which is a
structure which defines the “Instance” interface. This interface
is used to communicate the lifecycle of an instance of the module;
when a module is loaded in a web page, then this interface is
invoked. We care about when a module instance is created, and when it
is attached to a view (i.e. the area of the page which contains
the module). Note that when I say interface, I mean a PPAPI
interface, not a Go interface. Later, I hope to have modules
implement Go interfaces, and hide the translation to PPAPI
interfaces.
The example is contrived, and quite simple; it demonstrates the use
of the Graphics2D interface, which, as the name suggests,
enables a module to perform 2D graphics operations. The demo simply
draws repeating rectangles of different colours, animated by
regularly updating the graphics context and shifting the pixels on
each iteration. I would have used the standard “image” Go
package, but unfortunately llgo is currently having trouble compiling
it. I'll look into that soon.
Building llgo
Alright,
how do we build this thing? We're going to do the following things:
- Build llgo, and related tools.
- Compile the PNaCl-module Go code into an LLVM module.
- Link the llgo runtime into the module.
- Link the ppapi library from the Native Client SDK into the module.
- Translate the module into a native executable.*
*The final step is
currently necessary, but eventually Chrome/Chromium will perform the
translation in the browser.
Let's
begin by building the llgo-dist
tool. This will be
used to
build the llgo compiler, runtime, and linker. More on each of those
in a moment. Go ahead and build llgo-dist:
$
go get
github.com/axw/llgo/cmd/llgo-dist
The
llgo-dist tool takes two options: -llvm-config,
and -triple.
The former is the path to the llvm-config
tool, and defaults to simply “llvm-config” (i.e. find it using
PATH). The latter is the
LLVM target triple used
for compiling the
runtime package (and other
core
packages, like syscall). The
Native Client SDK contains an llvm-config and the shared library that
we need to link with to use LLVM's C API.
As I said above, I'm running on Linux x86-64, so for my case, the
llvm-config tool can be found in:
$
nacl_sdk/pepper_canary/toolchain/linux_x86_pnacl/host_x86_64/bin/llvm-config
At
this point, you should put the “host_<arch>/bin” directory
in your PATH, and the “host_<arch>/lib” directory in your
LD_LIBRARY_PATH, as llgo
currently requires it, and I refer to
executables without their full paths in
some cases.
The
Native Client SDK creates
shared libraries with the target armv7-none-linux-gnueabi,
so we'll do the same. Let's go ahead and build llgo now.
$
llgo-dist
-triple=armv7-none-linux-gnueabi
-llvm-config=nacl_sdk/pepper_canary/toolchain/linux_x86_pnacl/host_x86_64/bin/llvm-config
We
now have a compiler, linker, and runtime. As an aside, on my laptop
it took about 2.5s to build, which
is great! The gc toolchain
is a wonderful thing.
You can safely ignore the
warning about “different data layouts” when llgo-dist compiles
the syscall package, as
we
will not be using the syscall package in our example.
Building the example
Now, let's compile the PNaCl module:
$
llgo -c -o main.o
-triple=armv7-none-linux-gnueabi llgo/pkg/nacl/ppapi/*.go llgo/testdata/programs/nacl/example.go
This creates a file called “main.o”, which contains the LLVM
bitcode for the module. Next, we'll link in the runtime. Eventually,
I hope that the “go” tool will be able to support llgo (I have
hacked mine up to do this), but for now you're going to have to do
this manually.
$
llgo-link -o main.o main.o
$GOPATH/pkg/llgo/armv7-none-linux-gnueabi/runtime.a
Now
we have a module with the runtime linked in. The
llgo runtime defines things like functions for appending to slices,
manipulating maps, etc. Later, it will contain a more sophisticated
memory allocator, a garbage collector runtime, and a goroutine
scheduler.
We
can't translate this to a native executable yet, because it lacks an
entry point. In a PNaCl module, the entry point is defined in a
shared library called libppapi_stub.a,
which is included by the
libppapi.a linker script.
We
can link this in using pnacl-clang, like so:
$
pnacl-clang
-o main.pexe main.o -lppapi
This
creates a portable
executable (.pexe), an
executable still in LLVM bitcode form. As
I mentioned earlier, this will eventually be the finished product,
ready to load into Chrome/Chromium. For now, we need to run a final
step to create the native machine code executable:
$
pnacl-translate -arch x86-64
-o main_x86_64.nexe main.pexe
That's it. If you want to load this in an x86 or ARM system, you'll
also need to translate the pexe to an x86 and/or ARM nexe. Now we can
run it.
Loading the PNaCl module into Chrome
I'm
not sure at what point all the necessary parts became available in
Chrome/Chromium, so I'll just say what I'm running: I have added the
Google Chrome PPA, and installed google-chrome-beta. This
is currently at version 24.0.1312.35 beta.
By
default, Chrome only allows Native Client modules to load from the
Chrome Web Store,
but you can override this by mucking about in about:flags. Load
up Chrome, go to about:flags, enable
“Native Client”, and
restart Chrome so the change takes effect.
Curiously, there's a
“Portable Native Client” flag; it may be that the translator is
already inside Chrome, but I'm not aware of how to use it.
To simplify matters, I'm going to hijack the hello_world example in
the Native Client SDK. If you want to start from scratch, refer to the Native Client SDK
documentation. So anyway we'll build the hello_world example, then
replace the executable with our own one.
$ cd
nacl_sdk/examples/hello_world
$ make
pnacl/Release/hello_world.nmf
$ cp
<path/to/main_x86_64.nexe>
pnacl/Release/hello_world_x86_64.nexe
Now start an HTTP server to serve this application (inside the
hello_world directory):
$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
Finally, navigate to the following location:
Behold,
animated bars! Obviously the
example is awfully simplistic, but the I wanted to get this out so
others can start playing with it. I'm not really
in the business of fancy
graphics, so I'll leave more impressive demos to others.
Next Steps
I'll keep dabbling with this, but my more immediate goals are to
complete llgo's general functionality. As wonderful as all of this
is, it's no good if the compiler doesn't work correctly. Anyway, once
I do get some more time for this, I intend to:
- Clean up nacl/ppapi, providing an external API.
- Update llgo-link to transform a “main” function into a global constructor (i.e. an “init” function) when compiling for PNaCl.
- Update llgo-link to link in libppapi_stub.a when compiling for PNaCl, so we don't need to use pnacl-clang. Ideally we should be able to “go build”, and have that immediately ready to be loaded into Chrome.
- Get the image package to build, and update nacl/ppapi to use it.
- Implement syscall for PNaCl. This will probably involve calling standard POSIX C functions, like read, write, mmap, etc. Native Client code is heavily sandboxed, but provides familiar POSIX APIs to do things like file I/O.
If
you play around with this and produce
something
interesting, please let me know.
That's
all for
now
–
have
fun!