Yep, it’s WebGL and Native together. This is a short tutorial on using Emscripten to integrate a C++/OpenGL code with JS environment. There are some other tutorials on the net you might want to have a look at if you want to simply convert an existing native app and not bother with web-specific input events or render loops (gears,simplegl). If you have some native code you want to use within a bigger webapp, e.g., write a JS API around your existing library, read on. Also there are some conclusions regarding performance and maturity of Emscripten+WebGL solutions closer to the end of this page.
Quick links
Live demo for results (we suggest you use Chrome or Opera).
Github project for sources.
Build instructions:
- Install the Emscripten toolset
- Build the project using
make
(ormingw32-make
) - Open
index.html
in your chrowser.
Introduction
In this tutorial we give an introduction to how Emscripten-generated code could be integrated with the rest of your web-app, and there are some sources and build files that might be helpful for a quick start. It’s a simplified outcome of what we actually did as a (successful) experiment over one of our commercial products.
Emscripten is an LLVM-to-Javascript compiler that… well.. if you know what a compiler is, that pretty much explains the whole thing. Now we can write code in anything that compiles to LLVM (and this includes C++), run Emscripten and get a JS equivalent. It does not bring full coverage of C++ features but a reasonable subset of features is there and if you are not using anything fancy (and most people don’t) you should be able to convert your code to JS without much trouble. The major feature here is that the resultant JS code is not a syntactical translation but a bytecode-level translation which means things like pointers to functions returning pointers to pointers to.. would work. You can see the output as a kind of a virtual machine written in JS that executes your C++ code. Plus it uses WebGL as a back-end for your normal GL calls. As a result, all sorts of native libraries and apps are now possible to port to JS that would seem crazy some few years ago: Unreal Engine 3, Nebula 3, ffmpeg, etc.
Setup
First of all, get Emscripten installed: https://github.com/kripken/emscripten/wiki/Emscripten-SDK
Make sure emcc
is visible in your environment, typing emcc
in console/cmd should return something like this:
which means emcc is installed and wants to do some job. You must also have clang
, python
, and node
installed. I strongly recommend going through the Emscripten tutorial to make sure the whole toolchain is installed and working fine.
Getting the code
Download the project files manually from here or type
There are three files containing native C code, but let’s pretend there are hundreds of classes and millions of loc :-) If you don’t want to download anything and just skim through, I give here some bits of code and a general picture of what’s happening.
The shaders
couple of files have functions for compiling shaders and linking GL programs:
shaders.h
The main.cpp
uses those to initialise the actual shaders. There are two functions in main.cpp
: initGL
initialises an SDL screen which in turn initialises the GL context, and drawTriangle
(surprise!) draws a triangle. Note that there is no main
entry point and in order to initialise the context and draw something, we actually need to call the two functions from somewhere. And that “somewhere” will be our JS code. The code is quite self-explanatory for those that have ever used OpenGL before.
a bit of main.cpp
Let us compile this code. Go to the directory with the files and type
It should generate a file named glcore.js
that contains our “compiled” library. You can have a look what has been generated but it’s not really meant to be edited or read. We are able to debug this though but a bit later on that.
The Javascript / Typescript part
Now that we have our C++ library ported to JS, we can call its functions from our JS code.
The web side of our tutorial makes use of TypeScript which is basically a bit of OO syntactic sugar over JavaScript plus static type-checking. Overall, you can get away staying with JavaScript of course but you’d be better off using TypeScript for this tutorial. Given that you would already have node
installed, getting TypeScript is as simple as
Here are some bits of TypeScript code doing most of the gluing work (note the file extensions):
triangle.ts
Emscripten provides a Module
object sitting in the global scope and that’s where all JS reference to our native functions reside. In triangle.ts
, we define Module
and its members to make the type-checker happy and, therefore, make less type mistakes. The Bindings
class contains references to the native functions obtained through this Module
object. For example, in order to get a “pointer” to our native function initGL
, we call
The parameters here are: function name, return type (as a JS type), and a list of function argument types as an array of strings. This would return a function that you can call as you would do with a usual JS function:
Note that where a C function expects an argument of a pointer type, the converted JS function would expect an integer which in fact represents a pointer to the Emscripten heap (or stack, choose for yourself). And since JS arrays and other JS objects are not located on that heap, we need to allocate a bit of that memory, use it in native functions by passing a pointer (JS integer number) as an argument, and later on free it as you would normally do in C. That is exactly what we do in our render()
method to pass the current translation
object as a float array.
Now that we have our TypeScript code, let us translate it to JavaScript:
Since triangle.ts
has a reference to another TypeScript file, input.ts
, the two will be merged together and translated into triangle.js
. Have a look in the generated file. The JavaScript version is not much different from the TypeScript code but all classes and types are gone, naturally.
Putting everything together
Ok now as we’ve got the ported native library and the JS code that supposedly represents our JS API, we can glue everything together. We already have references to native functions (in the Bindings
class), and our JS code calls those functions when needed. What’s left is to make sure both the “native” glcore.js
and the JavaScript triangle.js
scripts are loaded into our page, also there should be a canvas on our page that can receive those GL calls, and Emscripten needs to have a reference to that canvas.
index.html
We set the canvas which we want to be receiving GL calls to the Module.canvas
property. This says Emscripten not to generate its own canvas and use ours instead.
Also you’d want to make sure you have proper style sheet in the styles
folder and have this mousewheel library in your main folder (if you cloned the whole tutorial project earlier, you should have all of this set up).
One more (IMPORTANT) thing left to do is to say Emscripten which functions we actually want to be able to call from within JavaScript. By default all of them would be hidden, and since we need two C functions available to us, we add the EXPORTED_FUNCTIONS
parameter to the build command:
You can read more details about name mangling and interaction with the native code here
Now build the library and open index.html
. You should see a nicely coloured triangle which we can actually move around and zoom with a mouse.
Automating the build process
Since you would already have MinGW and/or MSYS installed (if using Windows), there’s a high chance you’d have make
tool available to you. This could be a nice and simple way for automating your emcc build command. Create a file called makefile
in the same folder next to other sources and put this inside:
makefile
Also, move the array you supplied to EXPORTED_FUNCTIONS
to a separate file:
makefile_exports.txt
Now you can call
or simply
from within that folder to build the library, and call
to remove the generated files. There are certainly more scalable approaches to automating the build process out there but this would do for a quick start. Remember: if you are using TypeScript, you also need to build the main .ts
file to get a JavaScript output.
Debugging
It might sound crazy to some, and indeed it did look outlandish to me, nevertheless, we can debug C++ code in a browser. Ok, not the native code itself but its JS version mapped back to C++ which is close enough in the context of porting code to JS. To enable this, add the -g
flag to your emcc compilation:
This would generate an additional .map
file sitting next to the output file (in our case it would be glcore.js.map
) which we need to make sure is available to the browser at runtime.
Refresh your index.html
, open the developer console (in Chrome press F12), go to the Sources tab, open up the list of source files and you should see and able to open our original .cpp
files. Breakpoints and stepping through should all work as expected. The only thing is the variable viewer would not be able to resolve things like Emscripten pointers which would naturally appear as numbers. Otherwise you should be able to debug just fine although I would imagine writing a simple wrapper and using a native debugger and a native IDE would certainly bring its advantages in debugging.
Optimisation
There are two levels of optimisation available with the Emscripten tool chain. The first one is LLVM compiler optimisation which is triggered by setting the --llvm-opts
level of emcc
command, and the second is emcc optimisation set by -O
level which works at the JavaScript level. There are 4 levels of each ranging from 0 (no optimisation) to 3 (heavy optimisation). We tried different optimisation flags and measured performance on different platforms and here are some conclusions:
* Emcc optimisation -O3
can sometimes over-optimise things and throw out relevant code, so use with care.
* -O2 --llvm-opts 2
seems to be the most reasonable in terms of performance and stability combination. Emcc optimises for performance and size in one shot, so a difference between -O2
and -O0
can be of a factor up to 3.
* Emcc optimisation flag -O
makes a noticeable difference in performance (up to a factor of 3 again) whereas --llvm-opts
not so much.
* Emscripten-generated JavaScript code is 2 to 20 times slower than its equivalent native code. This heavily depends on language features being used and is quite close to measurements done by other developers you could find on the net.
Other considerations
All in all, we had almost no problems porting our C++ code to JS using the Emscripten toolchain. We hope this tutorial was useful to those looking at porting native code to JS as a possible solution. There is indeed a bit of learning and tweaking to do but it’s quite a flat learning curve for those who has some C++ and JS coding experience. And it is definitely easier than re-writing your library in JS from scratch so it looks like a viable solution in certain cases.
The only issue that might arise in a commercial project is the final size of the generated JS code. It is somewhat larger than the native binaries, and JS version needs to be loaded by a browser, so some measures need to be taken to not put off your customer while he/she waits for this to finish. This might be only necessary to do once and hope the browser would cache it, but again, deal with care. If you need to load a 20MB script and the user has only got a 1Mb connection, it would take him/her more than 2m 40s at best.
The story of WebGL is not so bright though. Since it is a standard and not a particular implementation, browser vendors need to actually make efforts to support it and not all do. You can have a look to what extent different browsers support WebGL (e.g. http://caniuse.com/webgl), but in our experience, recent Chrome and Opera (which is Chrome’s clone feature-wise) are the only browsers capable of rendering whatever we render natively. Others either do not support WebGL at all or produce visual artefacts given the same sequence of GL commands that we feed to GLES 2.0 capable devices. The example given in this tutorial does produce artefacts in Firefox 27, and we had artefacts in IE 11 with some of our other examples when we used VBOs. So, unless you can “suggest” your customers to only use Chrome, WebGL is not there yet for commercial products (as of March 2014) but it as well may be there in a couple of years if Firefox, IE and Safari catch up.