emscripten-qt

Note: (8/7/18) - emscripten-qt is an old project (dating from 2012) using an old version of Qt (4.8.7) and is no longer developed, so I don't recommend using it. Feel free to play with the Demos, though!

Check out Lorn Potter's work on Qt5 for WebAssembly!

What is "emscripten-qt"?

emscripten-qt is a port of Qt to Emscripten. Emscripten is a very clever project that essentially allows one to compile a C/C++ project into Javascript, so essentially, emscripten-qt allows many C++ + Qt apps to be compiled so that they run in a web browser. See the Demos, here!

qtgui-chips-in-your-browser.png

qtgui-when-mice-collide.png

What browsers are supported?

In theory, it should work with any browser that supports the HTML5 canvas element and which supports typed arrays. So far, emscripten-qt has been tested and found to work in Firefox and Chrome. Konqueror and Rekonq have been tested and, alas, do not run programs compiled with emscripten-qt, seemingly due to missing typed array support (an app compiled with emscripten's TYPED_ARRAY=0 will at least start running, but Qt really needs full typed array support in order to work correctly)

What kind of Qt apps will run?

Many Qt demos, and even some full-fledged KDE productivity applications such as Kate have been ported, but there are some kinds of apps that will definitely need to be re-worked in order to run in emscripten-qt. A non-exhaustive list:

  • Apps that use threads/ QThreads. Javascript has no built-in threading support, and HTML5s Web Workers have unworkable limitations: one thread would not be able to access data from another thread, for example.
  • Apps that use local event loops. While Javascript code is running, there is no way for the browser to receive and process events (and if it runs continuously for a long time, you will likely see a warning pop up in your browser window, asking if you would like to terminate the offending script), so any application will have to exit main() before your browser can resume its normal duties. This means, for a start, that the usual method of running Q(Core)Application::exec() and having it block (and so, prevent main() from exiting and all the global destructors etc from executing) until the user exits the app is not possible in apps that you want to run with emscripten-qt. More subtley, you won't be able to show dialogs or popup menus, or perform synchronous network operations (if I end up supporting QtNetwork, that is - it's not supported as of the time of writing) and wait around for their response: you must use the asynchronous versions instead. This will generally require you to restructure your code somewhat, but is usually not the end of the world - and there are good arguments for writing code in this manner as a matter of course. In general, then, methods with *names like "exec()" or "processEvents()" * will likely not work with emscripten-qt without changes.
  • Apps that use drag and drop. Related to the above point, but with the added annoyance that QDrag appears not to have an asynchronous version. This is probably fixable with a small, emscripten-specific API-addition, plus patching of any Qt internals that use the original QDrag API (the Qt ItemViews, for example).
  • Apps that use any Qt modules except for, at the time of writing, QtCore, QtGui, QtXml and QtScript. Apps that use QtNetwork will compile, but the networking part currently won't work.
  • Apps that use any parts of QtCore or QtGui that have been (temporarily?) compiled out: In the beginning, I was concentrating on just getting emscripten-qt up and running at all, and so many features of Qt got compiled out if they looked like they would be even slightly problematic - for example, JPEG support and QFileDialog were ditched immediately and only added back in after several weeks!
  • ... and of course, apps that have additional dependencies that have not yet been converted to emscripten!

qtgui-some-unpleasantly-coloured-shapes.png emscripten-kate.png

How can I get it?

You can browse the repository here, but since this is a fork of Qt, and Qt is pretty big, my hosting is really not equipped to serve it. Please use the mirror on gitorious.

How do I use it?

The instructions below are for Unix-y operation systems, such as Linux.

First, we need to check out Emscripten itself. Decide where you want to place it - somewhere in your home directory is fine - call that /home/path/to/put/emscripten/in. Change to /home/path/to/put/emscripten/in, and do


git clone https://github.com/kripken/emscripten.git emscripten

Then, from the same directory, do


git clone git://gitorious.org/qt/emscripten-qt.git

and then, still within the same directory, create a directory for compiling emscripten-qt:


mkdir build-emscripten-qt

Your /home/path/to/put/emscripten/in should now contain the directories "emscripten", "emscripten-qt", and "build-emscripten-qt". cd into your build directory:


cd build-emscripten-qt

Emscripten needs, at the time of writing, version 3.2 of clang and llvm to be installed. Your distro might have these packaged for you, or you may need to compile from source. Install them now.

Before we build emscripten-qt, we need to tell it where emscripten's root directory is (the directory containing "emcc", "em++", etc) - apps compiled with emscripten should use the header files from the "system" subdirectory there instead of the system-wide ones. We do this via an environment variable:


export EMSCRIPTEN_ROOT_PATH=/home/path/to/put/emscripten/emscripten/

Now we can configure emscripten-qt:


../emscripten-qt/configure -xplatform qws/emscripten-clang  -embedded emscripten -static -opensource -debug  -no-qt3support -no-opengl -no-openssl   -system-zlib -no-gif -qt-zlib -qt-libpng -no-libmng -no-libtiff -qt-libjpeg -no-accessibility -dbus -script -no-fpu -no-mmx -no-3dnow -no-sse -no-sse2 -no-sse3 -no-ssse3 -no-sse4.1 -no-sse4.2 -no-icu -no-rpath  -confirm-license -no-webkit -no-phonon -no-freetype -nomake demos -nomake examples -little-endian -no-feature-socket  -no-feature-codecs -no-feature-textcodecplugin -no-feature-systemlocale  -no-feature-qws_multiprocess -no-feature-sound    -no-feature-printpreviewwidget  -no-feature-printpreviewdialog  -no-feature-systemsemaphore -no-feature-sharedmemory -no-feature-localeventloop -feature-qws_clientblit -feature-qws_cursor  -depths 32 -make tools  --prefix=$(pwd)/install

This should succeed: note that it shouldn't matter too much what you have installed on your system (besides clang & llvm 3.2, that is) as it only relies on the headers that emscripten provides.

When the "configure" step has succeeded, do


make sub-tools-bootstrap && make install_qmake sub-moc-install_subtargets sub-uic-install_subtargets sub-rcc-install_subtargets && make  sub-corelib-install_subtargets sub-gui-install_subtargets install_mkspecs

(you can add "-j <number of cores>" after each occurrence of "make" if you have a multi-core computer, to speed things up).

This should also succeed, and install Qt headers, libraries etc inside /home/path/to/put/emscripten/build-emscripten-qt/install.

There are a handful of source files that profiling has shown will benefit hugely from being compiled with a higher level of optimisation; if you want to re-compile these source files, do


for i in qcosmeticstroker qdrawhelper qgrayraster; do opt -O3 src/gui/.obj/debug-static-emb-emscripten/$i.o > tmp-llvm-$i.o; mv tmp-llvm-$i.o src/gui/.obj/debug-static-emb-emscripten/$i.o; done && make  sub-corelib-install_subtargets sub-gui-install_subtargets 

You are now ready to try and compile a sample app!

qtgui-font-dialog.png qtgui-color-dialog.png

Compiling a C++ + Qt app to Javascript

Currently, only QMake-based projects will work, though I hope to support CMake-based ones eventually.

Let's try one of Qt's own examples - the chips demo. The source is included with emscripten-qt.

At the time of writing, compiling Qt apps is split into two stages - first, the apps is compiled to llvm bitcode, and then the resulting llvm bitcode file is run through Emscripten's "emcc" compiler, which does the hard work of compiling it into Javascript.

Stage 1: Compiling Qt app to llvm

For reasons that are quite impenetrable to me, we don't seem to be able to build apps with emscripten-qt from anywhere within our build-emscripten-qt, so we'll have to move up a directory first, and then create and cd into a new subdirectory for building the chips demo:


cd ..
mkdir qtdemos
cd qtdemos

We'll have to make sure we our freshly compiled QMake, and the correct compiler settings:


export PATH=/home/path/to/put/emscripten/build-emscripten-qt/install/bin/:$PATH
export QMAKESPEC=/home/path/to/put/emscripten/build-emscripten-qt/install/mkspecs/qws/emscripten-clang

As mentioned, the "QApplication::exec()" procedure for stopping main() from exiting straight away will not work with emscripten-qt, so we must make some small changes to the chips demo. The changes basically ensure that any objects local to main that must be kept alive while the app is running are allocated on the heap instead of the heap. We'll copy the chips source into our build directory since we need to patch it:


cp -R ../emscripten-qt/demos/chip .

The patch is included with emscripten-qt (you can view it here: source:emscripten-stuff/chip-emscripten-qt-patch.patch). Let's apply it:


cd chip
patch main.cpp < ../../emscripten-qt/emscripten-stuff/chip-emscripten-qt-patch.patch 
cd ..

I like out-of-source builds, so let's have another build directory :)


mkdir build
cd build

Then we should be able to QMake the chips project file as usual:


qmake ../chip/chip.pro

Then make - again, you can use the "-j" flag to speed things up if you have multiple cores:


make

This should result in an LLVM bitcode file called "chip.bc". This concludes the Phase 1 (compilation to bitcode); now we need to use emcc to compile this to Javascript.

Stage 2: Compiling LLVM to Javascript

Emscripten's "emcc" does all the hard work for us, here, but first we need a few of Qt's fonts - these will be embedded in the resulting webpage. I generally pick a very small sampling of them - the line below will copy about 156k worth of fonts:


mkdir -p qt-fonts && cp ../../build-emscripten-qt/install/lib/fonts/helvetica_*.qpf ../../build-emscripten-qt/install/lib/fonts/fixed_*.qpf qt-fonts/

This is pretty clunky, and I hope to get it a bit more automated at some point, but we also need to copy the QtCore.a and QtGui.a to the current directory and rename to .so:


cp ../../build-emscripten-qt/install/lib/QtGui.a QtGui.so
cp ../../build-emscripten-qt/install/lib/QtCore.a QtCore.so

Now we let emcc work its magic:


../../emscripten/emcc \
  chip.bc \
  QtGui.so QtCore.so \
  -O2 \
  --closure 0 \
  --jcache \
  --pre-js ../../emscripten-qt/emscripten-stuff/pre-qt.js \
  --js-library ../../emscripten-qt/emscripten-stuff/pre-qt-library.js \
  --embed-file qt-fonts \
  -s EXPORTED_FUNCTIONS="['_main', '_EMSCRIPTENQT_resetTimerCallback', '_EMSCRIPTENQT_timerCallback', '_EMSCRIPTENQT_timerCallback_springboard', '_EMSCRIPTEN_canvas_width_pixels', '_EMSCRIPTEN_canvas_height_pixels', '_EMSCRIPTENQT_mouseCanvasPosChanged', '_EMSCRIPTENQT_mouseCanvasButtonChanged']" \
  -s TOTAL_MEMORY=67108864 \
  -s INLINING_LIMIT=50 \
  -o chip.html 

A brief explanation of the arguments:

  • chip.bc is of course the llvm bitcode file that we want to convert to Javascript;
  • then we provide the paths to QtCore.a and QtGui.a, which are the bitcode libraries we created when we compiled emscripten-qt;
  • -O2 is the optimisation level;
  • --closure 0 disables Google's "Closure" compiler as it currently seems to generate incorrect code with emscripten-qt;
  • --jcache just tells emcc to cache the results so hopefully things will go faster should we want to compile this again;
  • --pre-qt.js contains a bunch of Javascript helpers for interfacing with the canvas (flushing the QWS framebuffer to it; passing canvas mouse events to QWS; setting up the single font file provided with emscripten-qt; etc);
  • --embed-file qt-fonts adds the fonts that we just copied over - note that the directory name "qt-fonts" is important here, as emscripten-qt is hardcoded to look for fonts stored in a directory called "qt-fonts";
  • -s EXPORTED_FUNCTIONS is something that will hopefully be useful to us when/ if I figure out how to get Closure working with emscripten-qt;
  • -s TOTAL_MEMORY sets the amount of "simulated" memory that will be allotted to the app when it runs; the "chip" demo takes up a fair bit of RAM, and Emscripten in asm.js mode requires RAM to be a power of 2, so we allocate ~64MB to be on the safe side; and
  • chip.html will be the web page containing the converted Javascript + canvas version of the chips demo that we can run in a browser.

qtgui-elasticnodes.png qtgui-embeddeddialogs.png

Are you using emscripten-qt?

If so, please drop me a line - it's always nice to hear about people who are putting it to good use! .

My email address is at the "etotheipiplusone" domain in the URL (without the vps2), and begins with "kdedevel".

You can also contact me on my blog, or over twitter. My linkedin profile is here.

qtgui-chips-in-your-browser.png (127 KB) Simon St James, 12/01/2012 04:30 pm

qtgui-when-mice-collide.png (147 KB) Simon St James, 12/01/2012 04:41 pm

qtgui-some-unpleasantly-coloured-shapes.png (109 KB) Simon St James, 12/01/2012 05:29 pm

qtgui-dull-and-businesslike.png (79.6 KB) Simon St James, 12/01/2012 05:29 pm

qtgui-font-dialog.png (89.6 KB) Simon St James, 12/01/2012 08:21 pm

qtgui-color-dialog.png (96.1 KB) Simon St James, 12/01/2012 08:21 pm

qtgui-elasticnodes.png (117 KB) Simon St James, 12/21/2012 03:55 pm

qtgui-embeddeddialogs.png (154 KB) Simon St James, 12/21/2012 04:09 pm

emscripten-kate.png (103 KB) Simon St James, 06/20/2013 10:13 am