Sunday, December 9, 2012

Go in the Browser: llgo does PNaCl


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:
  1. Build llgo, and related tools.
  2. Compile the PNaCl-module Go code into an LLVM module.
  3. Link the llgo runtime into the module.
  4. Link the ppapi library from the Native Client SDK into the module.
  5. 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!