Alastair Barber

Simple WebAssembly App From The Ground Up

Tool demo with LTSpice window I decided to try and learn about WebAssembly from the ground up by building the (very simple) C++ PWL file generator for the web using WebAssembly. I spent some time learning about how WebAssembly is implemented and finding out what is needed to get the program to run. Here are some notes from this process. It is intended as a bird’s eye overview of the concepts, there are links here to much more thorough and in-depth documentation. Of course this is complete overkill for this kind of utility - and conversely for anything more complicated you will appreciate the tooling available in Emscripten or similar rather than figuring it all out manually, as this article does…

I recently made a tool for producing PWL (Piece-wise Linear) signal files to use in LTspice to simulate PWM (pulse-width modulation) signals in a digital to analogue circuit. The actual logic and code behind this is extremely simple. I chose to write it in C++ because there’s really not much to it apart from looping over values and streaming to and from files. My workflow for the electronics project I’m working on meant that I wanted it to work from the command line and simply pipe the values to a text file.

Naturally I though it’d be useful to share it. Even though it’s C++ it uses nothing outside of the std library, so building it on multiple platforms is not a problem. But who wants to either a) Build something from source, no matter how simple or b) run untrusted binaries from around the internet? So let’s have a look at some of the basics of WebAssembly…

Magic Number

The most simple of all! Simply puts the number 42 in a box!

C++ Code

Nothing at all exciting here - declared as “C” as I struggled with the C++ mangler changing the function names and being unable to export them.

Build Command

Some trial and error and stackoverflow searching and I figured out the build command to use. I am sure that instead of exporting the functions explicitly here it is possible to use the EMSCRIPTEN_KEEPALIVE export directive too.

JavaScript and HTML

Here things start to get interesting as we begin to set up our environment and actually call things within the C++ program. In-depth details can be found at MDN Web Docs - Loading and Running WebAssembly Code. In short there is also not much going on apart from setting up the page and loading the WA Module. And the HTML document, which will stay the same for a bit…

Result for show number

Sharing Memory

Now we should see if we can send some data back and forth. The command-line app reads in LUTs used to produce waveforms from a file. It is possible to use WebAssembly to access files - but for now it is sufficient to compose this data in the browser. In general, I am choosing to manage most of the memory on the C++ side and having JavaScript ask for where to put data. Here we are allocating space for the LUTs we shall use. No prizes for style but it works for this purpose…

And on the JS side of things, for now just have it do the maths to figure out what the values would be for a sine waveform, and in this case - only do it for the 8 bit resolution. Here the general logic is that we get the memory that is being exported by the WASM program, and write to it from JavaScript. The wa_memory.buffer is a contiguous block of data, and so we must get the addresses within this that correspond to the variables that are declared in C. At these addresses (or offsets), we create a Uint32Array in JavaScript that we can manipulate - with its storage mapped onto the desired underlying location.

The function show_number now returns the value of the location in memory for that particular LUT.

Result for sin memory

System Calls

Now let’s start adding in the actual program. Simply adding the following headers and function code (truncated here for clarity) from the original source compiled fine but I started to get errors in the browser console, even when not exporting or calling this function anywhere.

#include <iostream>
#include <iomanip>
#include <string>
#include <fstream>

#define RISE_FALL_TIME 1e-9
#define LOW 0.0
#define HIGH 3.3

const unsigned int BITS_MAX_VAL[] = {0, 2, 4, ...

using namespace std;

float produce_pwl_single(
	const float frequency, const int resolution, ...
	// Writes a single 'pulse' to the output stram
	// returns the end time of the pulse
	
	float now = offset;
	out << setprecision(10) << now << "\t" << LOW << endl;
	float period = 1.f / frequency;
...

TypeError: import object field 'wasi_snapshot_preview1' is not an Object

This appeared to be being caused by the call to out <<. WASI is the WebAssembly System Interface and wasi_snapshot_preview1 is a version of this API. Using the wasm-objdump tool (brew install wabt) we can see that some fields have been added to the import section of the wasm binary:

calc.wasm:	file format wasm 0x1

Section Details:

Type[3]:
 - type[0] (i32) -> i32
 - type[1] (i32, i32) -> i32
 - type[2] () -> nil
Import[2]:
 - func[0] sig=1 <wasi_snapshot_preview1.environ_get> <- wasi_snapshot_preview1.environ_get
 - func[1] sig=1 <wasi_snapshot_preview1.environ_sizes_get> <- wasi_snapshot_preview1.environ_sizes_get

The definition of these functions are described in the above API as:

wasi_snapshot_preview1.env definitions

So it looks as if the implementation for writing to an ostream makes calls to access environment variables. imports to the loaded WASM module are passed in the second argument of WebAssembly.instantiateStreaming, so it is here that we should define the implementation of environ_get and environ_sizes_get. At this point I also changed the project to import the memory from JavaScript, rather than export it from the WASM compiler. This was done by adding the -Wl,--import-memory linker option, and also changing the INITIAL_MEMORY and MAXIMUM_MEMORY to 128KB. wasm-objdump then shows

- memory[0] pages: initial=2 max=2 <- env.memory

in the import section, so we must also add this to our import section.

Here is a basic implementation of the code necessary to copy some env vars (the my_env object) to the locations determined by the WASM module.

Note that in order for these newly created functions to actually be called at runtime, we must call the exports._initialize() function. For testing there is also the imported function show_string implemented in JavaScript, and marked as extern in the C code:

extern "C"{
	extern void show_string(char*, int);

	void message(){
		char *str = getenv("HELLO");
		int len = strlen(str);
		show_string(str,len);
	}
}

And the build command has been updated to:

em++ -s INITIAL_MEMORY=128KB -s MAXIMUM_MEMORY=128KB -s ALLOW_MEMORY_GROWTH=0 -s TOTAL_STACK=8kb -s STANDALONE_WASM -s EXPORTED_FUNCTIONS="['_show_number','_get_lut_ptr','_message']" -s ERROR_ON_UNDEFINED_SYMBOLS=0 -Wl,--import-memory -Wl, --no-entry calc.cpp -o calc.wasm -Os

Result of env var

Streaming Strings & Bringing it All Together

Now that we’re setting up the environment and happily putting data into the right place from both C and JavaScript, there’s not much to do to get the orignal PWL generation code running. A simple wrapper function is needed in order to create a std::ostringstream to use instead of cout, and to copy the string to the heap and return a pointer to it to JavaScript. Raw pointers flying about everywhere is not ideal and so we must be careful with memory management!

All the extra UI elements are not much more involved than the one in the original HTML here. The entire project is available on GitHub - and you can try it out here: