For the purpose of this demo, we will be running googletest as a dynamically loaded library and executing some example tests. To make things more interesting the tests will use threads so that we have to use an exciting new feature: dynamic linking + pthread.

We will use the following unit tests

#include <gtest/gtest.h> // googletest header file
#include <thread>

const char *actualValTrue  = "hello gtest";
const char *actualValFalse = "hello world";
const char *expectVal      = "hello gtest";

TEST(StrCompare, CStrEqual) {
    std::thread([]{
        EXPECT_STREQ(expectVal, actualValTrue);
    }).join();
}

TEST(StrCompare, CStrNotEqual) {
    std::thread([]{
        EXPECT_STREQ(expectVal, actualValFalse);
    }).join();
}

And here is the result, running right in your browser

Open in new tab ↗

About

This demo shows a demo application (a googletest unit test) built using dynamic linking and pthread support in Emscripten.

First, googletest is built as a dynamic library with pthread support.

Then an example test that dynamically links to googletest is also built as a dynamic library with pthread support. Although it is built as dynamic library this module contains the main entry point.

Finally, an empty file is built as Emscripten’s main module, which hosts the system libraries. This main module dynamically links to the example test library.

main.jssystem librariesmain.wasmsystem librariesstring-compare.soentry pointtestsgtest.sotesting frameworkmain.html

The reason to run the main entry point from a dynamically loaded module is so that global dependencies are constructed in the correct order.

The globals in the main module, which host the system libraries, will be constructed first. Then the globals in the side modules will be constructed following the correct dependency order.

In this case the test creates global objects that depend on the testing framework and needs to be constructed after it. If the test globals were included in the main module they would be constructed before the testing framework globals resulting in undefined behavior. Moving the tests (and the main entry point) to a dynamic library solves this problem.

Build

Get the source

$ git clone https://github.com/jprendes/emscripten-dylink-demo.git

Then run the following commands to build the demo and launch it in a local web server.

$ docker build -t emscripten-dylink .
$ docker run --rm -it --net=host -v "$(pwd)":"/app/" emscripten-dylink

This will create a docker container with emsdk 2.0.13 and pull a patched version of emscripten from jprendes/emscripten dylink-merged-fixes that implements some functionalities currently missing from emscripten-core/emscripten master. The most important of those changes are:

Then the container will pull a copy of googletest and compile it as a shared library qtest.so. Then it will compile the example tests as another shared library string-compare.so which depends on gtest.so. Finally it will compile an empty file as a main module which will generate main.(html|js|wasm) as well as main.worker.js.

Once all modules are compiled, the container will launch a local web server so that you can see the result. You can access it by visiting http://localhost:5000/main.html on your browser. You should see the following output (up to timing differences):

[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from StrCompare
[ RUN      ] StrCompare.CStrEqual
[       OK ] StrCompare.CStrEqual (614 ms)
[ RUN      ] StrCompare.CStrNotEqual
src/string-compare.cpp:19: Failure
Expected equality of these values:
  expectVal
    Which is: "hello gtest"
  actualValFalse
    Which is: "hello world"
[  FAILED  ] StrCompare.CStrNotEqual (12 ms)
[----------] 2 tests from StrCompare (632 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (643 ms total)
[  PASSED  ] 1 test.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] StrCompare.CStrNotEqual

 1 FAILED TEST