emscripten and PNaCl: App entry in PNaCl
The is the followup to last week's post about application entry in emscripten. If you haven't done yet I would recommend reading this first before continuing.
2 main points to keep in mind about the (P)NaCl platform:
- Blocking the main thread will block the entire browser tab.
- NaCl has true threading support which can be used to workaround these blocking limitations.
Point (1) is the same as on the emscripten platform, and point (2) is the big difference to emscripten.
In a Nebula3/PNaCl application, the main function looks the same as on any other platform (I'm using emscripten's "simulate_infinite_loop" approach now):
#include "myapplication.h"
ImplementNebulaApplication();
void
NebulaMain(const Util::CommandLineArgs& args)
{
MyApplication app;
app.SetCommandLineArgs(args);
app.StartMainLoop();
}
However under the hood, the startup process until the NebulaMain() function is entered is completely different from other platforms, since PNaCl doesn't have a main() function. Instead PNaCl has the concept of application Module and Instance objects. This is where the plugin-nature of a PNaCl app shines through. There is a single Module object created on a web page containing a PNaCl app, and for each <embed>
element on the page, one Instance object. In reality though, most of the time there will be exactly one Module and one Instance object, so the distinction doesn't really matter.
PNaCl offers two different startup APIs for C and C++. The C++ API is easier to grasp IMHO, so I'll just concentrate on this (this dual C/C++ nature continues through the whole NaCl API, there's a pure C API, extended by a slightly higher-level C++ API.
Hooking up your code to NaCl basically means to write 2 subclasses, one deriving from pp::Module, and one deriving from pp::Instance, and the NaCl runtime will then call into these classes through virtual methods for initialisation and notifying the application about events.
But first things first:
Everything starts at a global C Function called pp::CreateModule() which you must provide, and which must return a new object of your pp::Module subclass (called N3NaclModule in this case):
namespace pp
{
Module* CreateModule()
{
return new N3NaclModule();
};
}
Although this is the very first function that NaCl will call, you should be aware that initialisers in the global scope (static objects) will already be initialised and have had their constructors called at this point.
The main job of the derived Module class is to create Instance objects, but we can also put some one-time init code in there. There's a pair of functions to initialise and shutdown GL rendering called glInitializePPAPI() and glTerminatePPAPI(). The only rule is that no GL calls must be made outside these two functions, so I guess we could also put them somewhere else, as long as is guaranteed that they are not called multiple times.
But - the most important method in the derived Module class is the factory method for Instance objects called CreateInstance. In my case, I have created a subclass of pp::Instance called NACL::NACLBridge.
The entire N3NaclModule class looks like this:
class N3NaclModule : public pp::Module
{
public:
virtual ~N3NaclModule()
{
glTerminatePPAPI();
}
virtual bool Init()
{
return glInitializePPAPI(get_browser_interface()) == 1;
}
virtual pp::Instance* CreateInstance(PP_Instance instance)
{
return new NACL::NACLBridge(instance);
};
};
All the really interesting stuff from here on happens in the NACLBrigde object.
These two source snippets live inside the ImplementNebulaApplication() macro which all in all looks like this:
...
#elif __NACL__
#define ImplementNebulaApplication() \
class N3NaclModule : public pp::Module \
{ \
public: \
virtual ~N3NaclModule() \
{ \
glTerminatePPAPI(); \
} \
virtual bool Init() \
{ \
return glInitializePPAPI(get_browser_interface()) == 1; \
} \
virtual pp::Instance* CreateInstance(PP_Instance instance) \
{ \
return new NACL::NACLBridge(instance); \
}; \
}; \
namespace pp \
{ \
Module* CreateModule() \
{ \
return new N3NaclModule(); \
}; \
}
#elif __MACOS__
...
Now on to the NACLBridge class, this is (I know I'm repeating myself) derived from the pp::Instance class, but is called "Bridge" for a reason: in the PNaCl we're spawning a dedicated thread for the game loop, and leave the main thread (aka the Pepper thread) for event handling and rendering. Our derived pp::Instance subclass serves as a "bridge" between these 2 threads, that's why it's called NACLBridge.
The NaCl runtime will call into virtual methods of an pp::Instance object for handling events, the most important of these are Init(), DidChangeView(), HandleInputEvent(). For a complete overview and exhaustive documentation of those callback methods I recommend sifting directly through the SDK header: include/ppapi/cpp/instance.h
In the Init() method I'm only building a CommandLineArgs object from the provided raw arguments (these have been extracted from our <embed>
element in the HTML page).
The actual initialisation work happens (in my case) in the first call to DidChangeView() by calling a Setup() method in the NACLBridge object. I choose this place because this is where I'm getting the current display dimensions of the <embed>
element, which is required for the renderer initialisation (although now thinking about it, I might also be able to extract these from the arguments provided in the Init() method, need to try this out some time).
The NACLBridge::Setup() method only does one thing: create a thread with the NebulaMain() function as entry point, and then return to the NaCl runtime. The code inside NebulaMain() works just as on any other platform, with the only difference that it is not running on the main thread, but in its own dedicated game thread.
The big advantage to run the game loop in its own thread is that you "own the game loop", and you can perform blocking, for instance to wait for IO. The disadvantage is that you can't call any PPAPI (NaCl system functions) from the game thread, which is a blog-post-topic on its own.
So to recap: The ImplementNebulaApplication macro runs on the main thread, and creates one pp::Module and one pp::Instance object. The pp::Instance object creates the dedicated game thread, which calls into the NebulaMain() function, which from that moment on runs the game loop like on any other platform. With this approach we don't need to slice the game loop into frames like on the emscripten platform.
Now that you heroically worked your way through through all of this I'll tell you a secret: NaCl also provides a simple alternative to this complicated mess called the ppapi_simple library, which essentially provides a classic main() function running in its own thread, and because blocking is allowed on this thread, also provides normal POSIX fopen()/fclose() style blocking IO functions (sound familiar?).
Check out the header file include/ppapi_simple/ps.h as starting point.
Unfortunately this ppapi_simple library didn't exist when I started dabbling with NaCl about 2 years ago, certainly would have made life a lot easier. On the other hand, the work that had already gone into the NaCl port made the emscripten port easier, which wouldn't be the case had I used the ppapi_simple wrapper code.
Written with StackEdit.