WebAssembly, part II

In the previous post we setup a tool chain to build WebAssembly code and created our first Hello World application.

In this step we will learn how to call WebAssembly from JavaScript and how call JavaScript from WebAssembly.

Imagine you create a web page which require some user input. But before you send that data to the server for processing you would want to validate it first. We will do validation in WebAssembly.

Let’s start with web page. It has two input fields: edit box and selector. To create a good looking UI we will use JavaScript library – Bootstrap.

Source code could be downloaded from my GitHub repository.

<!DOCTYPE html>
<html>
	<head>
		<!-- Required meta tags -->
		<meta charset="utf-8"/>
		<meta name="viewport" content="width=device-width, initial-scale=1">

		<!-- Bootstrap CSS -->
		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">

		<!-- JavaScripts: jQuery, Popper.js, and Bootstrap JS -->
		<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
		<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
		<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>

		<title>WA Test Page</title>
	</head>
	<body onload="initPage()">
		<div class="container">
			<h1>Hello, world!</h1>

			<div id="errorMessage" class="alert alert-danger" role="alert" style="display:none;"></div>

			<div class="form-group">
				<label for="name">Make:</label>
				<input type="text" class="form-control" id="name">
			</div>
			<div class="form-group">
				<label for="type">Type:</label>
				<select class="custom-select" id="type">
					<option value="0"></option>
					<option value="10">SUV</option>
					<option value="11">Coupe</option>
					<option value="12">Sedan</option>
				</select>
			</div>

			<button type="button" class="btn btn-primary" onclick="onSave()">Save</button>

			<script src="editcar.js"></script>
			<script src="verify.js"></script>
		</div>
	</body>
</html>

And corresponding JavaScript which will hold init function and validation calls:

const MAX_NAME_LENGTH = 20;
const VALID_CATEGORY_IDS = [10, 11, 12];

const defaultValues = 
{
	name: "Toyota Highlander",
	typeId: "10",
};

function initPage() 
{
	document.getElementById("name").value = defaultValues.name;

	const type = document.getElementById("type");
	const count = type.length;
	for (let index = 0; index < count; index++) 
	{
		if (type[index].value === defaultValues.typeId) 
		{
			type.selectedIndex = index;
			break;
		}
	}
}

function onSave() 
{	
	let errorMessage = "";
	const errorMessagePointer = Module._malloc(256);

	const name = document.getElementById("name").value;
	const typeId = getSelectedTypeId();

	if (!validateName(name, errorMessagePointer) || !validateType(typeId, errorMessagePointer)) 
	{
		errorMessage = Module.UTF8ToString(errorMessagePointer);
	}

	Module._free(errorMessagePointer);

	setErrorMessage(errorMessage);
	if ("" === errorMessage) 
	{
		// everything seems to be OK - pass data further to the server
		// ...
	}
	
	const result = Module.ccall('sqrt_int', // name of C function
		'number', 							// return type
		['number'], 						// argument type
		[typeId]); 							// argument
	console.log(result);
}

function setErrorMessage(error) 
{
	const errorMessage = document.getElementById("errorMessage");
	errorMessage.innerText = error; 
	errorMessage.style.display = ( "" === error ? "none" : "" );
}

function validateName(name, errorMessagePointer) 
{
	const isValid = Module.ccall('ValidateName', 			// name of C function
		'number',                                           // return type
		['string', 'number', 'number'],                     // argument type
		[name, MAX_NAME_LENGTH, errorMessagePointer]);      // argument
	return (1 === isValid);
}

function validateType(typeId, errorMessagePointer) 
{
	const arrayLength = VALID_CATEGORY_IDS.length;
	const bytesPerElement = Module.HEAP32.BYTES_PER_ELEMENT;
	const arrayPointer = Module._malloc((arrayLength * bytesPerElement));
	Module.HEAP32.set(VALID_CATEGORY_IDS, (arrayPointer / bytesPerElement));

	const isValid = Module.ccall('ValidateType', 						// name of C function
			'number',                                                   // return type
			['string', 'number', 'number', 'number'],                   // argument type
			[typeId, arrayPointer, arrayLength, errorMessagePointer]);  // argument

	Module._free(arrayPointer);

	return (1 === isValid);
}

function getSelectedTypeId() 
{
	const type = document.getElementById("type");
	const index = type.selectedIndex;
	if (-1 !== index) { return type[index].value; }

	return "0";
}

Now to C++ code:

#include <cstdlib>
#include <cstring>

// If this is an Emscripten (WebAssembly) build then...
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#ifdef __cplusplus
extern "C" { // So that the C++ compiler does not change function names
#endif

int ValidateInputValue(const char* value, const char* default_error_message, char* return_error_message)
{
	if ((value == NULL) || (value[0] == '\0'))
	{
		strcpy(return_error_message, default_error_message);
		return 0;
	}
	return 1;
}

int IsTypeIdInArray(char* selected_type_id, int* valid_type_ids, int array_length)
{
	int type_id = atoi(selected_type_id);
	for (int index = 0; index < array_length; index++)
		if (valid_type_ids[index] == type_id)
			return 1;
	return 0;
}

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
int ValidateName(char* name, int maximum_length, char* return_error_message)
{
	// 1: A name must be provided
	if (ValidateInputValue(name, "A Car Name must be provided.", return_error_message) == 0)
		return 0;

	// 2: A name must not exceed the specified length
	if (strlen(name) > maximum_length)
	{
		strcpy(return_error_message, "The Car Name is too long.");
		return 0;
	}

	// Everything is OK
	return 1;
}

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
int ValidateType(char* type_id, int* valid_type_ids, int array_length, char* return_error_message)
{
	// 1: A Type ID must be selected
	if (ValidateInputValue(type_id, "A Car Type must be selected.", return_error_message) == 0)
		return 0;

	// 2: A list of valid Type IDs must be passed in
	if ((valid_type_ids == NULL) || (array_length == 0))
	{
		strcpy(return_error_message, "There are no Car Type available.");
		return 0;
	}

	// 3: The selected Type ID must match one of the IDs provided
	if (IsTypeIdInArray(type_id, valid_type_ids, array_length) == 0)
	{
		strcpy(return_error_message, "The selected Car Type is not valid.");
		return 0;
	}

	// Everything is OK
	return 1;
}

int sqrt_int(int x) {
	
	int y = x * x;
	
	EM_ASM(	{ console.log('x = ' + $0); }, x);
	
	return y;
}

#ifdef __cplusplus
}
#endif

To compile it either use provided verify.bat file or just run
emcc verify.cpp -s EXPORTED_FUNCTIONS=['_malloc','_free','_sqrt_int'] -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','UTF8ToString'] -o verify.js

To test your code you can use Python Web Server: in the command prompt switch to the directory with the source code and then run:
python -m http.server

Then open your browser and go to new test page
http://localhost:8000/editcar.html

When the page is loaded initPage will fill it with default data:

One user click Save button onSave is called and it’s going to validate car name and validate car type (and also to a call to sqrt_int which we will discuss shortly).

If user enter incorrect information error message is displayed:

sqrt_int shows how to call C++ function which receives and returns simple data (such as int, double, char). No memory management is required. We use JavaScript helper functions which were auto generated by Enscripten compiler: verify.js
const result = Module.ccall('sqrt_int', // name of C function
'number', // return type
['number'], // argument type
[typeId]); // argument

sqrt_int also shows how to call JavaScript from C++.
EM_ASM( { console.log('x = ' + $0); }, x);

When we are dealing with a complex data we have to allocate memory for them by calling malloc and free:
const errorMessagePointer = Module._malloc(256);
...
Module._free(errorMessagePointer);

Leave a Reply