WAjic is a simple way to build C/C++ WebAssembly application with meaningful browser integrations like WebGL.
Inspired by Emscripten's EM_JS macro, WAjic is intended to be a more direct approach to building that offers more control and customization while being straight forward from C/C++ code to the web browser.
It starts out with a single command to call the Clang compiler to produce a .wasm file. The output can be loaded in the WAjic viewer (available online) or further processed with WajicUp (available as command line tool or online) for customized deployment with minimal file size.
- Samples
- Why
- Setup
- Building an Application
- Introducing WAjicUp
- Creating your own WAJIC functions
- Advanced Features
- Notes
- Missing Features
- License
Samples
Check out the online sample gallery.
Why
WebAssembly makes it possible to run C/C++ programs in the browser on a website. But WebAssembly alone has no system/user interface. Code is only executed when asked to do so through JavaScript, and similarly it's output needs to be processed by JavaScript.
For example, to run a C program that renders 3D graphics with OpenGL on the web, every OpenGL system function needs to be implemented in JavaScript to then call the appropriate WebGL API and pass data between WebAssembly and that API.
This is where WAjic comes in. WAjic provides a way to write these interface functions and libraries directly in C/C++ code. Furthermore, compiled .wasm files run directly with a generic JavaScript loader on the web and in the command line.
Setup
Getting WAjic
To get started download this project from GitHub with the Download ZIP
button or by cloning the repository.
For just compiling code you only need the header files, the full repository archive comes with some tools and samples as explained below.
Getting LLVM
We need only Clang and wasm-ld from LLVM 8.0.0 or newer which is available on the official LLVM releases page.
On Windows it's much simpler to use 7zip to extract just clang.exe
and wasm-ld.exe
instead of installing the whole suite. That's all we need.
Getting Node.js
This is not required for building, but required for testing in the command-line and the tools explained below.
WebAssembly support and the tools run fine in Node.js version 8 and newer, and don't have any external package dependencies.
If you use Node only for this, just get the latest long-term-support executable.
You can find the official Win64 EXE here.
Building an Application
Building can be done automatically or manually by calling Clang in the command line or by using a build system like GNU Make.
Automatically Building
First, make sure clang, wasm-ld and wasm-opt exist in the same directory as wajicup.js.
See the section Using Symbolic Links for how to do this without fully copying these files.
Then just run the following command to get a fully packaged, optimized and independent html file of the first sample:
node wajicup.js Samples/Basic.c Basic.html
At first, this only builds raw C/C++ applications without the C/C++ standard libraries or dynamic memory allocations.
To get support for these system libraries, just download the pre-built system libraries and headers and put it into the wajic directory.
Now we can build the rest of the samples, for example the WebGL sample, with:
node wajicup.js Samples/WebGL.c WebGL.html
See the sections Introducing WAjicUp and Directly Compiling with WAjicUp for details and how to output separate html/js/wasm files.
Manually Building
For example, to build the basic sample, run this command to create Basic.wasm:
clang -I. -Os -target wasm32 -nostartfiles -nodefaultlibs -nostdinc -nostdinc++ -Wno-unused-command-line-argument -DNDEBUG -D__WAJIC__ -fvisibility=hidden -fno-rtti -fno-exceptions -fno-threadsafe-statics -Xlinker -strip-all -Xlinker -gc-sections -Xlinker -no-entry -Xlinker -allow-undefined -Xlinker -export=__wasm_call_ctors -Xlinker -export=main samples/Basic.c -o Basic.wasm
The built .wasm file can be loaded in the WAjic viewer or via Node.js CLI node wajic.js Basic.wasm
.
This only builds raw C/C++ applications without the C/C++ standard libraries or dynamic memory allocations.
To get support for these system libraries, just download the pre-built system libraries and headers and put it into the wajic directory.
Once you have the system files some more arguments need to be added to the build command.
Now we can build the rest of the samples, for example the WebGL sample, with:
clang -isystem./system/include/libcxx -isystem./system/lib/libcxx/include -isystem./system/include/compat -isystem./system/include -isystem./system/include/libc -isystem./system/lib/libc/musl/include -isystem./system/lib/libc/musl/arch/emscripten -isystem./system/lib/libc/musl/arch/generic -Xlinker ./system/system.bc -D__EMSCRIPTEN__ -D_LIBCPP_ABI_VERSION=2 -I. -Os -target wasm32 -nostartfiles -nodefaultlibs -nostdinc -nostdinc++ -Wno-unused-command-line-argument -DNDEBUG -D__WAJIC__ -fvisibility=hidden -fno-rtti -fno-exceptions -fno-threadsafe-statics -Xlinker -strip-all -Xlinker -gc-sections -Xlinker -no-entry -Xlinker -allow-undefined -Xlinker -export=__wasm_call_ctors -Xlinker -export=main -Xlinker -export=malloc -Xlinker -export=free samples/WebGL.c -o WebGL.wasm
With the .wasm file ready and tested in the WAjic viewer, we can go on further optimizing the result.
If the program doesn't do WASM memory allocation from JavaScript code, -Xlinker -export=malloc
can be removed.
Same with freeing memory from JavaScript code, -Xlinker -export=free
can be removed.
The packing utility program explained below will warn when there are unused or missing exports.
In the notes below you'll find explanation for the various parameters and also sample commands to run the compiling and linking separately.
If you want to build the system libraries yourself, check the chapter about it below.
Running wasm-opt
The tool wasm-opt from Binaryen provides a 10% to 15% size reduction of the generated .wasm files.
It also offers generation of shim functions to pass 64-bit numbers between C and JavaScript.
Binary releases are available on the Binaryen project page.
Feel free to extract only wasm-opt
and ignore the rest.
Then run it over the wasm file with:
wasm-opt --legalize-js-interface --low-memory-unused --converge -Os WebGL.wasm -o WebGL.wasm
Functionality wise nothing changes (unless there are 64bit parameters passed to/from JavaScript) and it still runs in the browser just like before.
Introducing WAjicUp
The WebAssembly JavaScript Interface Creator Utility Program is a tool bundled with WAjic that allows optimization and customized JavaScript/HTML generation. It is available as command line tool (with Node.js) or online.
The most basic command:
node wajicup.js WebGL.wasm WebGL.wasm
This will minify the JavaScript code that has been embedded.
Next we can also generate multiple output files:
node wajicup.js WebGL.wasm BuiltWebGL.wasm BuiltWebGL.js BuiltWebGL.html
This will take out the JavaScript code embedded in the wasm file and put it into a standalone JavaScript loader file. It even generates a simple HTML file that loads the loader (which then loads the wasm file).
Any combination of wasm/js/html file is supported. For example outputting just a single HTML file will make WAjicUp embed both the WASM data and the JavaScript code inside it.
Some option switches are also available:
Name | Explanation |
---|---|
-no_minify |
Don't minify JavaScript code |
-no_log |
Remove all output logging |
-streaming |
Enable WASM streaming (needs web server support, new browser) |
-rle |
Use RLE compression when embedding the WASM file |
-loadbar |
Add a loading progress bar to the generated HTML |
-node |
Output JavaScript that runs in Node.js (CLI) |
-embed N P |
Embed data file with embed name N from path P (see file embedding) |
-gzipreport |
Report the potential output size with gzip compression |
-args |
Enable C program arguments (argc/argv) that are passed to main (can be customized in the WASM loading html) |
-arg X |
Passes X to the program arguments, can be specified multiple times (first arg must be the program name) |
-template H |
Uses HTML template file H instead of generating a default file. |
-stacksize S |
Overrides the size of the stack from the WebAssembly default of just 64 kb |
-cc "X" |
When compiling directly with WAjicUp, this passes argument X to the compiler |
-ld "Y" |
When compiling directly with WAjicUp, this passes argument Y to the linker |
-v |
Be verbose about processed functions |
-h |
Show command line usage |
Creating your own WAJIC functions
With the WAJIC macro you can declare a function callable from C/C++ with a JavaScript code body that can then access all kinds of web APIs:
WAJIC(void, ShowAlert, (const char* msg),
{
Alert(MStrGet(msg));
})
The first line declares the function, it's return value and it's arguments as used by the C code.
The function body after that is JavaScript code that can execute web APIs, manipulate the WASM memory and call other WASM functions, too.
WAJIC(int, AddThenMultiply, (int factor),
{
return ASM.Add(1, 2) * factor;
}
WA_EXPORT(Add) int Add(int a, int b)
{
return a + b;
}
This creates a C function Add
that adds two values and a JavaScript function (callable form C) AddThenMultiply
that then invokes the Add function and returns its result multiplied by a factor.
WAJIC(char*, GetDocumentTitle, (),
{
return MStrPut(document.title)
})
This creates a function callable from C that returns the document.title string. Because this allocates memory in the JavaScript side with malloc, the C program needs to call free() on the returned pointer.
Functions and objects available in WAJIC code
On the JavaScript side there are a handful of utility functions and variables available. Some of them were already shown off in the examples above.
Name | Explanation |
---|---|
MStrPut(str) |
Allocate memory and store a JavaScript string str encoded as UTF8 with a \0 null terminator on the WASM heap and return the new pointer. |
MStrPut(str, ptr, buf_size) |
Store a JavaScript string str encoded as UTF8 with a \0 null terminator in a prepared WASM buffer at ptr of size buf_size . Returns the actual number of bytes written (excluding the null terminator). Returns 0 and does nothing if buf_size is 0. |
MStrGet(ptr) |
Read a \0 null terminated UTF8 string from the WASM memory at address ptr . |
MStrGet(ptr, length) |
Read a UTF8 string of size length bytes from the WASM memory at address ptr . |
MArrPut(arr) |
Allocate memory and store a JavaScript array/typed array/buffer on the WASM heap and return the new pointer. |
ASM |
An object which contains all the exports from the WASM module. Its primary use is to call C/C++ functions/callbacks from WAJIC functions. |
WM |
Gives access to the WebAssembly module object, used for accessing embedded files. |
MU8 |
Access to the WASM memory as unsigned 8-bit integers |
MU16 |
Access to the WASM memory as unsigned 16-bit integers. Usually accessed by right shifting pointers by 1, like MU16[ptr>>1] = 789 . |
MU32 |
Access to the WASM memory as unsigned 32-bit integers. Usually accessed by right shifting pointers by 2, like MU32[ptr>>2] = 1 . |
MI32 |
Access to the WASM memory as signed 32-bit integers. Usually accessed by right shifting pointers by 2, like MI32[ptr>>2] = -1 . |
MF32 |
Access to the WASM memory as 32-bit floats. Usually accessed by right shifting pointers by 2, like MF32[ptr>>2] = 0.5 . |
STOP |
A boolean variable that is set to true when the program aborts/crashes. If you use requestAnimationFrame/setInterval/setTimeout/event listeners you should check this first before continuing. |
abort(code, msg) |
This will abort a running program where code can be a predefined tag like 'BOOT', 'CRASH', 'MEM' or your own if you extend the WA.error function in the front-end. msg contains more error details. |
Exporting functions
To make a C/C++ function available to the JavaScript world (both for custom front-end scripts and WAJIC functions), you can tag them with WA_EXPORT().
For an example, you can check the code above and how the Add
function is annotated with it.
Shared Init Code Block
If you want to share functionality and variables between multiple WAJIC JavaScript functions, you can add a shared init code block.
WAJIC_WITH_INIT(
(
var myCounter = 1;
),
int, GetCounter, (),
{
return myCounter;
})
WAJIC(void, AddCounter, (int num),
{
myCounter += num;
})
Here the variable myCounter is defined outside of the two WAJIC functions and it is available to both.
Check the SharedInit sample
There is one caveat with init blocks, its availability is attached to the function it's defined together with.
So in the example above, if the C program never calls GetCounter
, AddCounter
will not work properly.
You have to make sure to attach the init block to a function that must be referenced by the main program.
Libraries
There are two more macros specifically for encapsulating libraries into distinct function groups:
WAJIC_LIB
and WAJIC_LIB_WITH_INIT
These in itself are very similar to the base WAJIC
and WAJIC_WITH_INIT
, except that there is one more argument at the beginning.
The main feature of library grouping is that the init code block is only shared between functions in the same library group.
WAJIC_LIB_WITH_INIT(MYLIB, (...), int, InitMyLib, (...), {...})
WAJIC_LIB(MYLIB, void, DoSomethingInMyLib, (...), {...})
Advanced Features
Embedding Files
You can embed binary files with WAjicUp and read them in your program. To embed a file add the -embed parameter:
node wajicup.js EmbedFile.wasm EmbedFile.wasm -embed MYFILE data.bin
And then in your program you can use standard C functions to read embedded files:
char buf[1024];
FILE* f = fopen("MYFILE", "rb");
int len = fread(buf, 1, 1024, f); // read the first 1024 bytes of the file
fclose(f);
Alternatively you can access the file contents with the wajic file API:
#include <wajic_file.h>
unsigned int size = WaFileGetSize("MYFILE") // get the file size
unsigned int read = WaFileRead("MYFILE", data, start, sizeof(data)); // read sizeof(data) bytes from file at offset start
const char* file = (const char*)WaFileMallocRead("MYFILE", &size); // allocate memory and read the full file (optional start/offset)
free(file); // free the memory allocated by WaFileMallocRead
It is also possible to embed all files in a directory.
node wajicup.js EmbedFile.wasm EmbedFile.wasm -embed somedir/ ../path/to/somedir/
Check the EmbedFile sample and the implementation in wajic_file.h.
Loading URLs
You can load data at URLs with optional progress updates during the download (for example to show a progress). The URL can be relative to the HTML file that executes the WASM file.
To do so, you have to pass a string of the name of the callback function like this:
#include <wajic_file.h>
// This function is called when the HTTP request finishes (or has an error)
WA_EXPORT(FinishCallback) void FinishCallback(int status, char* data, unsigned int length, void* userdata)
{ printf("Got response - status: %d - length: %u - data: '%.4s...' - userdata: %p\n", status, length, data, userdata); }
// This function is called periodically with download progress updates until download is complete
WA_EXPORT(ProgressCallback) void ProgressCallback(unsigned int loaded, unsigned int total, void* userdata)
{ printf("Progress - loaded: %u - total: %u - userdata: %p\n", loaded, total, userdata); }
WaFileLoadUrl("FinishCallback", url, (void*)0x1234, "ProgressCallback");
The third parameter is a user data pointer which can be anything that will be given back in the callbacks so if you do multiple LoadUrl requests you know which one is which. POST requests and setting a custom timeout are also available.
Check the LoadUrl sample and the implementation in wajic_file.h.
WebGL
Currently WAjic comes with a WebGL version 1 header that emulates OpenGL ES 2.0 API which in itself is a subset of desktop OpenGL 2.0/3.0.
OpenGL ES based means vertex/fragment shaders required, no fixed function pipeline and also no unbuffered vertex attribute arrays.
There is no shader code transformation, shaders need to be written with WebGL compatibility in mind (i.e. explicit float precision, no f suffix for floats).
Check the WebGL sample for how to set up a canvas and render something.
Coroutines
Coroutines allows execution to suspend (yield back to the browser) or switch between function contexts. It can be used to suspend a running program mid-function (wait for time to pass or until the next animation frame) or to emulate threads.
Check the Coroutine sample and the implementation in wajic_coro.h.
If you don't use WAjicUp, you will need to run another step of wasm-opt with the following command:
wasm-opt --asyncify Coroutine.wasm -o Coroutine.wasm
Notes
Files in this Repository
File | Explanation |
---|---|
wajic.h | The main header defining the WAJIC macros as well as WA_EXPORT |
wajic_gl.h | Header defining the WebGL functionality |
wajic_file.h | Header defining functions for dealing with embedded files and loading URLs |
wajic_coro.h | Header defining functions for dealing with Coroutine functionality |
wajic.js | The generic WASM loader that extracts WAJIC functions and instantiates them in JavaScript. Compatible with web and Node.js (commandline). |
wajic.minified.js | Minified version of wajic.js. |
wajic.mk | A GNU make makefile to build the system libraries as well as wasm files. |
wajicup.js | WAjic Utility Program for optimizing of wasm files and generating front-ends/loaders. |
wajicup.html | Web UI for WAjicUp to use it without Node.js (also available online). |
viewer.html | Viewer tool to easily load and test built wasm files (also available online). |
Clang Parameters
Parameter | Explanation |
---|---|
-I<wajic directory> |
Add the WAjic home directory to the include search path list (for wajic.h, etc) |
-Os |
Optimize for performance and output size, see clang manual |
-target wasm32 |
Build for the WebAssembly target |
-nostartfiles |
Avoid clang trying to build for a native console application (don't link with crt1.o) |
-nodefaultlibs |
Don't link with the clang standard libraries (don't link with libc.a) |
-nostdinc |
Remove the internal system directories from the include search path |
-nostdinc++ |
Remove the internal C++ directories from the include search path |
-Wno-unused-command-line-argument |
Don't complain if we have C++ exclusive arguments (like -fno-rtti) when building a C program |
-DNDEBUG |
Define the macro NDEBUG (removes debug overhead in some libraries) |
-D__WAJIC__ |
Define the macro __WAJIC__ to allow checking if we're building with the WAjic headers available |
-fvisibility=hidden |
Mark all functions and symbols as hidden so unused code can get removed |
-fno-rtti |
Disable C++ run-time type information |
-fno-exceptions |
Disable C++ exceptions |
-fno-threadsafe-statics |
Do not emit code to make initialization of local statics thread safe |
-Xlinker -strip-all |
Strip debug information from the output |
-Xlinker -gc-sections |
Strip unused functions and symbols from the output |
-Xlinker -no-entry |
Disable entry symbol of native application (the JavaScript loader does this for us) |
-Xlinker -allow-undefined |
Allow undefined symbols (the functions imported from JavaScript are undefined during compiling) |
-Xlinker -export=__wasm_call_ctors |
Export the special startup function which constructs global objects |
-Xlinker -export=main |
Export the main function if available (will do nothing if it doesn't exist) |
-Xlinker -export=malloc |
Export the malloc function to do memory allocation from JavaScript |
-Xlinker -export=free |
Export the free function to free allocated memory from JavaScript |
Samples/Basic.c |
Set the list of source files |
-o Basic.wasm |
Defines the output file |
-isystem<wajic dir>/system/<directories> |
When building with the C/C++ standard libraries, this sets the required include search paths |
-Xlinker <wajic dir>/system/system.bc |
When building with the C/C++ standard libraries, this links against the precompiled library |
-D__EMSCRIPTEN__ |
When building with the C/C++ standard libraries, this is required for musl-libc for WASM |
-D_LIBCPP_ABI_VERSION=2 |
When building with the C++ standard library, this is a required macro for WebAssembly builds |
Debugging
In Chrome it is easily possible to debug the JavaScript code that has been embedded in the wasm file.
Open the Developer tools in your browser and place a breakpoint on the line of code where WebAssembly.instantiate
is called.
Then check the Scope view and find the imports object that is passed to the function call and look into it. There you'll find clickable
values tagged with [[FunctionLocation]]
. After following the location, click the "Pretty print" button in the lower left of the source
view to make things readable. Breakpoints and everything is supported.
Sadly Firefox is not yet on the same level regarding debugging of functions generated at runtime, but hopefully in the future it will.
Compiling and Linking Separately
To build one of the samples by calling the compiler separately from the linker, first call clang for each source file to create an object file with .o extension:
clang -cc1 -triple wasm32 -emit-obj -fcolor-diagnostics -I. -isystem./system/include/libcxx -isystem./system/lib/libcxx/include -isystem./system/include/compat -isystem./system/include -isystem./system/include/libc -isystem./system/lib/libc/musl/include -isystem./system/lib/libc/musl/arch/emscripten -isystem./system/lib/libc/musl/arch/generic -fno-common -mconstructor-aliases -fvisibility hidden -fno-threadsafe-statics -fgnuc-version=4.2.1 -D__WAJIC__ -D__EMSCRIPTEN__ -D_LIBCPP_ABI_VERSION=2 -DNDEBUG -x c -std=c99 -Os samples/WebGL.c -o WebGL.o
(For C++ you'd replace -x c -std=c99
with -x c++ -std=c++11 -fno-rtti
)
Then to link the object files together into one .wasm file, call the linker wasm-ld like this:
wasm-ld -strip-all -gc-sections -no-entry -allow-undefined ./system/system.bc -export=__wasm_call_ctors -export=main -export=malloc -export=free WebGL.o -o WebGL.wasm
Manually Building System Libraries
Prebuilt system libraries and headers are provided as a download on this repository.
To build them yourself you can use the provided GNU make file.
-
Getting System Library Sources
The system libraries (libc/libcxx prepared for WASM) are maintained in the Emscripten project.
Just download its GitHub main archive and extract only thesystem
directory from it. -
Getting GNU Make
If you're on Windows, GNU Make is a small 180 KB EXE file which you can get here.
On Linux you can install the Make package and on macOS it comes as part of Xcode.
Next create a file called LocalConfig.mk
and place it next to wajic.mk
with the following content:
LLVM_ROOT = D:/dev/wasm/llvm
SYSTEM_ROOT = D:/dev/wasm/system
The variables are:
LLVM_ROOT
: Path to LLVM with clang and wasm-ld executables (see Getting LLVM)SYSTEM_ROOT
: Pointing to the path of thesystem
directory explained above.
Then you can build the system libraries (contains libc + libcxx + malloc) with the following command (use forward slashes even on Windows):
make -j 8 -f <path-to-wajic.mk> <path-to-wajic-root>/system/system.bc
Directly Compiling with WAjicUp
WAjicUp actually accepts c/cpp files as input. To use it, the executables of clang, wasm-ld and wasm-opt need to be in the same directory as wajicup.js. See the section Using Symbolic Links for means to avoid copying these files.
Then it's as easy as running it like this:
node wajicup.js Samples/Basic.c Basic.wasm
Just like with a .wasm file as input, all output variations of wasm/js/html are supported.
To pass additional command line options (like -I or -D) to the compiler, you can use one or more -cc
switches.
And similarly with one or more -ld
switches options can be passed to the linker.
When passing the special -cc -g
switch, code will be built in debug mode with full DWARF debug information included.
This makes it possible to debug through the native code and have breakpoints in the actual C/CPP files.
It is also possible to output a single c/cpp file into a object .o file. This later can then be linked to a .wasm with -ld obj.o
.
Further more, one or more source files can be compiled into a single bitcode archive .bc file. Just like .o this can be linked.
Example:
node wajicup.js big.c big.o
node wajicup.js multiple.c source.c files.c multi.bc
node wajicup.js main.cpp -ld big.o -ld multi.bc combined.html
Using Symbolic Links
To compile directly with WAjicUp, the executables clang, wasm-ld and wasm-opt need to be in the same directory as wajicup.js. If you have them somewhere else on your system then it's easiest to use symbolic links.
On Windows, you can accomplish this with (replace 'D:\dev\wasm*' with your paths):
mklink "<path-to-wajic-root>\clang.exe" "D:\dev\wasm\llvm\clang.exe"
mklink "<path-to-wajic-root>\wasm-ld.exe" "D:\dev\wasm\llvm\wasm-ld.exe"
mklink "<path-to-wajic-root>\wasm-opt.exe" "D:\dev\wasm\wasm-opt.exe"
On Linux, this can be done by running (replace '/usr/bin/' with your paths):
ln -s /usr/bin/clang <path-to-wajic-root>/clang
ln -s /usr/bin/wasm-ld <path-to-wajic-root>/wasm-ld
ln -s /usr/bin/wasm-opt <path-to-wajic-root>/wasm-opt
Missing Features
At this point in time, WAjic has no support for the following features:
- Threads (or posix thread emulation) *Coroutines might be an alternative
- setjmp/longjmp *Coroutines might be an alternative
- Full Filesystem emulation *Embedding Files supports file access
- C++ exceptions
- TCP socket emulation
- SIMD
These features are all fully or partially addressed by Emscripten.
If you rely on any of them, you should use Emscripten or try contributing to this project.
License
WAjic is available under the zlib license.
WAjicUp uses Terser JavaScript compressor which is under the BSD license.