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 runemcc 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 pagehttp://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.jsconst 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);