Chapter 10. WebAssembly modules in Node.js
This chapter covers
- Loading a WebAssembly module using Emscripten’s generated JavaScript code
- Using the WebAssembly JavaScript API to load a WebAssembly module
- Working with WebAssembly modules that call into JavaScript directly
- Working with WebAssembly modules that use function pointers to call into JavaScript
In this chapter, you’ll learn how to use WebAssembly modules in Node.js. Node.js has some differences compared with a browser—for example, having no GUI—but, when working with WebAssembly modules, there are a lot of similarities between the JavaScript needed in a browser and in Node.js. Even with these similarities, however, it’s recommended that you test your WebAssembly module in Node.js to verify that it works as expected on the versions that you want to support.
Definition
Node.js is a JavaScript runtime built on the V8 engine—the same engine that powers the Chrome web browser. Node.js allows for JavaScript to be used as server-side code. It also has a large number of open source packages available to help with many programming needs. For a book dedicated to teaching you about Node.js, see Node.js in Action, Second Edition (Manning): www.manning.com/books/node-js-in-action-second-edition.
This chapter aims to demonstrate that WebAssembly can be used outside the web browser. The desire to use WebAssembly outside the browser has led to the creation of the WebAssembly Standard Interface, or WASI, to ensure that there’s consistency in how hosts implement their interfaces. The idea is that a WebAssembly module will work on any host that supports WASI, which could include edge computing, serverless, and IoT (Internet of Things) hosts, to name a few. For more information about WASI, the following article has a good explanation: Simon Bisson, “Mozilla Extends WebAssembly Beyond the Browser with WASI,” The New Stack, http://mng.bz/E19R.
Let’s briefly revisit what you know. In chapters 4 through 6, you learned about the code-reuse advantages that WebAssembly brings by exploring a scenario in which a company had an existing desktop point-of-sale application written in C++ that it wanted to port to an online solution. Being able to reuse code in multiple environments reduces the chances of bugs being introduced accidently when compared with having to maintain two or more versions of the same code. Code reuse also leads to consistency, where the logic behaves exactly the same across all systems. In addition, because there’s only one code source for the logic, fewer developers need to maintain it, freeing them up to work on other aspects of systems, which brings higher productivity.
As figure 10.1 shows, you learned how to adjust the C++ code so that it could be compiled into a WebAssembly module using Emscripten’s compiler. This allowed you to use the same code for both the desktop application and in a web browser. You then learned how to interact with the WebAssembly module in a web browser, but the discussion about server-side code was left until now.
Figure 10.1. The steps for turning the existing C++ logic into a WebAssembly module for use by a browser and the server-side code. I discuss the server aspect in this chapter.
In this chapter, you’ll learn how to load a WebAssembly module in Node.js. You’ll also learn how the module can call into JavaScript directly or by using function pointers.
Suppose the company that created the online version of its point-of-sale application’s Edit Product page now wants to pass the validated data to the server. Because it’s not difficult to get around client-side (browser) validation, it’s critical that the server-side code validate the data it receives from the website before it’s used, as figure 10.2 shows.
The web page’s server-side logic will use Node.js and, because Node.js supports WebAssembly, you won’t need to re-create the validation logic. In this chapter, you’ll use the exact same WebAssembly modules that you created for use in the browser in the previous chapters. This allows the company to use the same C++ code in three locations: the desktop application, a web browser, and Node.js.
Similar to when working in a browser, in Node.js, you still use Emscripten to generate the WebAssembly and Emscripten JavaScript files. Unlike when working in a browser, however, you don’t create an HTML file. Instead, as step 4 of figure 10.3 illustrates, you create a JavaScript file that loads the Emscripten-generated JavaScript file, which will then handle loading and instantiating the module for you.
Figure 10.3. Emscripten is used to generate the WebAssembly and Emscripten JavaScript files. You then create a JavaScript file that loads the Emscripten-generated JavaScript file, which will in turn handle loading and instantiating the module for you.
The way you let the Emscripten-generated JavaScript wire itself up is different in Node.js compared to in a browser:
- In a browser, the Emscripten JavaScript code is wired up by including a reference to the JavaScript file as a script tag in the HTML file.
- In Node.js, to load JavaScript files, you use the require function, passing in the path to the file that you want to load.
Using the Emscripten-generated JavaScript file is convenient because the JavaScript code has checks that detect whether it’s being used in a browser or in Node.js; it loads and instantiates the module appropriately for the environment it’s being used in. All you need to do is have the file load, and the code will do the rest.
Let’s take a look at how you include Emscripten’s generated JavaScript file.
In this section, you’re going to learn how to load in Emscripten’s generated JavaScript file so that it can then download and instantiate your WebAssembly module for you. In your WebAssembly\ folder, create a Chapter 10\10.3.1 JsPlumbingPrimes\backend\ folder for the files that you’ll use in this section. Copy the js_plumbing.wasm and js_plumbing.js files from your Chapter 3\3.4 js_plumbing\ folder to your newly created backend\ folder.
In your backend\ folder, create a js_plumbing_nodejs.js file, and open it with your favorite editor. In your js_plumbing_nodejs.js file, you’ll add a call to Node.js’s require function, passing in the path to the Emscripten-generated JavaScript file js_plumbing.js. When loaded by Node.js, the Emscripten JavaScript code will detect that it’s being used in Node.js and will automatically load and instantiate the js_plumbing.wasm WebAssembly module for you.
Add the code from the following snippet to your js_plumbing_nodejs.js file:
require('./js_plumbing.js'); #1
To instruct Node.js to run JavaScript, you need to use the console window to run the node command, followed by the JavaScript file that you want it to execute. To run the js_plumbing_nodejs.js file that you just created, open a command prompt, navigate to the Chapter 10\10.3.1 JsPlumbingPrimes\backend\ folder, and then run the following command:
node js_plumbing_nodejs.js
As figure 10.4 shows, you can see that the module was loaded and run because the console window displays the output from the module: “Prime numbers between 3 and 100,000,” followed by the prime numbers that were found within that range.
Now that you know how to load Emscripten’s generated JavaScript file in Node.js, let’s look into how you call the functions in the WebAssembly module when using Node.js.
In chapter 4, you went through a series of steps (figure 10.5) to extend a desktop point-of-sale system to the web. Once the web page has verified that the data the user entered is valid, the data is sent to the server-side code so that it can be saved to a database or processed in some way. Before the server-side code does anything with the data received, it needs to make sure the data is valid, because there are ways to get around browser validation. In this case, your server is Node.js, and you’ll use the same WebAssembly module that you were using in the browser to handle validating the data received.
Figure 10.5. The final step of the process in reusing the C++ code is the server aspect, which is Node.js, in this case. You’ll copy the generated WebAssembly files to where your Node.js files are and then build the JavaScript code to interact with the module.
You’re now going to implement the final step of the process for extending the desktop point-of-sale system to the web by implementing the server-side aspect of it. You’ll copy the generated WebAssembly files to where your Node.js files are and then create a JavaScript file to interact with the module.
In your WebAssembly\ folder, create a Chapter 10\10.3.2 JsPlumbing\backend\ folder to hold the files that you’ll use in this section, and then complete the following:
- Copy the validate.js, validate.wasm, and editproduct.js files from your Chapter 4\4.1 js_plumbing\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and then open it with your favorite editor.
Rather than receive data from the web page, you’ll simulate having received the data by using the InitialData object, but you’ll rename the object to clientData. In your nodejs_validate.js file, rename the InitialData object to clientData as follows:
const clientData = { #1 name: "Women's Mid Rise Skinny Jeans", categoryId: "100", };
Overall, the JavaScript that Node.js needs is similar to what you had in the browser. The main difference with the Node.js code is that there’s no UI, so there are no input controls that need to be interacted with. Consequently, some of the helper functions aren’t needed. Delete the following functions from the nodejs_validate.js file:
- initializePage
- getSelectedCategoryId
Because there’s no UI, there’s no element to display error messages received from the module. Instead, you output the error messages to the console. Adjust the setError-Message function to call console.log, as shown in the following snippet:
function setErrorMessage(error) { console.log(error); } #1
One difference between working with Emscripten’s generated JavaScript file in Node.js compared to working in the browser is that, in the browser, your JavaScript code has access to a global Module object, but many of the helper functions are also in the global scope. In the browser, functions like _malloc, _free, and UTF8ToString are in the global scope and can be called directly without prefixing them with Module, like Module._malloc. In Node.js, however, the return object from the require call is the Module object, and all the Emscripten helper methods are available only through this object.
Tip
You can name the object that gets returned by the require function call anything you want. Because you’re using the same JavaScript code here that you had in the browser, it’s easier to use the name Module so that you don’t have to modify as much of the JavaScript. If you do choose to use a different name, you’ll need to modify the spots that do Module.ccall, for example, to use your object name instead of Module.
In the nodejs_validate.js file, after the setErrorMessage function, add a call to the require Node.js function to load Emscripten’s generated JavaScript file (validate.js). Name the object received from the require function Module. Your line of code should look like this:
const Module = require('./validate.js'); #1
The instantiation of the WebAssembly module happens asynchronously, both in the browser and in Node.js. To be notified when Emscripten’s JavaScript code is ready for interaction, define an onRuntimeInitialized function.
In your nodejs_validate.js file, convert the onClickSave function to be a function on the Module object’s onRuntimeInitialized property. Also, revise the code in the function to no longer try to pull the name and categoryId from the controls but rather use the clientData object. Your onClickSave function in your nodejs_validate.js file should now look like the code in the following listing.
Listing 10.1. onClickSave adjusted to now be onRuntimeInitialized
... Module['onRuntimeInitialized'] = function() { #1 let errorMessage = ""; const errorMessagePointer = Module._malloc(256); if (!validateName(clientData.name, errorMessagePointer) || #2 !validateCategory(clientData.categoryId, #3 errorMessagePointer)) { errorMessage = Module.UTF8ToString(errorMessagePointer); } Module._free(errorMessagePointer); setErrorMessage(errorMessage); if (errorMessage === "") { #4 } } ...
No other changes are needed in the nodejs_validate.js file.
If you run the code right now, there are no validation issues reported because all the data in your clientData object is valid. To test the validation logic, you can modify the data in the clientData object by clearing the value from the name property (name: ""), saving the file, and running the code.
To run your JavaScript file in Node.js, open a command prompt, navigate to your Chapter 10\10.3.2 JsPlumbing\backend\ folder, and then run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.6.
Now that you know how to load Emscripten’s generated JavaScript file in Node.js and call functions in the WebAssembly module, let’s look into how the module can call into the JavaScript file when running in Node.js.
As you saw in the previous section, a function can call into the module and wait for a response. While this approach works, there are times when a module might want to call the JavaScript directly once it finishes doing some work—perhaps to obtain more information or to provide an update.
The WebAssembly module that you’ll be using in this section included a function in Emscripten’s generated JavaScript file. The module will call that function if there was an error passing a pointer to the error message. The function will read the error message from the module’s memory and then pass the string to the setErrorMessage function in your main JavaScript.
In your WebAssembly\ folder, create a Chapter 10\10.3.3 EmJsLibrary\backend\ folder to hold the files that you’ll use in this section, and then complete the following:
- Copy the validate.js, validate.wasm, and editproduct.js files from your Chapter 5\5.1.1 EmJsLibrary\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and then open it with your favorite editor.
In your nodejs_validate.js file, rename the InitialData object to clientData, as shown in the following code snippet:
const clientData = { #1 name: "Women's Mid Rise Skinny Jeans", categoryId: "100", };
- initializePage
- getSelectedCategoryId
As it turns out, this particular use case for including your own JavaScript in Emscripten’s generated JavaScript file isn’t ideal when using Node.js. This is because the require function that’s used to load a JavaScript file puts the code within that file into its own scope, meaning the code in Emscripten’s generated JavaScript file can’t access any of the functions in the scope of the parent (the code that loaded it). JavaScript code loaded by the require function is expected to be self-contained and to not call into the scope of the parent.
If the module needs to call into the scope of the parent, a better approach is to use a function pointer that the parent passes in, which you’ll see in an upcoming section. But in this case, to get around the issue of the validate.js-generated code being unable to access the setErrorMessage function that it needs to call, you’ll need to create the setErrorMessage function on the global object rather than as a normal function.
More Info
In browsers, the top-level scope is the global scope (the window object). In Node.js, however, the top-level scope isn’t the global scope but is rather the module itself. By default, all variables and objects are local to the module in Node.js. The global object represents the global scope in Node.js.
To make the setErrorMessage function accessible to the Emscripten-generated JavaScript, you need to adjust the function to be part of the global object, as the following code snippet shows. To output the error message to the console, replace the function’s contents with a call to console.log:
global.setErrorMessage = function(error) { #1 console.log(error); #2 }
After the setErrorMessage function, add a call to the require Node.js function to load Emscripten’s generated JavaScript file (validate.js). Your line of code should look like this:
const Module = require('./validate.js'); #1
In your nodejs_validate.js file, convert the onClickSave function to be a function on the Module object’s onRuntimeInitialized property. Then, revise the code in the function to no longer call the setErrorMessage function or try to pull the name and categoryId from the controls. Finally, use the clientData object to pass the name and categoryId to the validation functions.
Module['onRuntimeInitialized'] = function() { #1 if (validateName(clientData.name) && #2 validateCategory(clientData.categoryId)){ #3 #4 } }
No other changes are needed for the nodejs_validate.js file.
To test the validation logic, you can adjust the data in the clientData object by changing the name or categoryId property to a value that’s invalid. For example, you could change the categoryId to hold a value that isn’t in the VALID_CATEGORY_IDS array (categoryId: "1001") and save the file.
To run your JavaScript file in Node.js, open a command prompt, navigate to your Chapter 10\10.3.3 EmJsLibrary\backend\ folder, and run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.7.
Using the Emscripten JavaScript library with code that calls into an application’s main JavaScript isn’t ideal if you plan on using Node.js, owing to scope issues with the require function. If you add custom JavaScript to Emscripten’s generated JavaScript file that will be used in Node.js, the best approach is for the code to be self-contained and not call into the parent code.
If a WebAssembly module needs to call into the application’s main JavaScript, and you want to support Node.js, function pointers are the recommended approach, and you’ll learn about them next.
Being able to call into the JavaScript directly is useful, but your JavaScript needs to provide the function during the module’s instantiation. Once a function has been passed to the module, you can’t swap it out. This is fine in most cases, but there are times when being able to pass a module the function to call on an as-needed basis is useful.
In your WebAssembly\ folder, create a Chapter 10\10.3.4 EmFunctionPointers\backend\ folder to hold the files that you’ll use in this section, and then do the following:
- Copy the validate.js, validate.wasm, and editproduct.js files from your Chapter 6\6.1.2 EmFunctionPointers\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and then open it with your favorite editor.
In your nodejs_validate.js file, rename the InitialData object to clientData, as the following code snippet shows:
const clientData = { #1 name: "Women's Mid Rise Skinny Jeans", categoryId: "100", };
Delete the following functions from the nodejs_validate.js file:
- initializePage
- getSelectedCategoryId
Modify the setErrorMessage function to call console.log, as shown in the following snippet:
function setErrorMessage(error) { console.log(error); } #1
After the setErrorMessage function, add a call to the require Node.js function to load the validate.js file. Your line of code should look like the following snippet:
const Module = require('./validate.js');
In your nodejs_validate.js file, convert the onClickSave function to be a function on the Module object’s onRuntimeInitialized property. Revise the code in the function to no longer call the setErrorMessage function or to try and pull the name and categoryId from the controls. Then, use the clientData object to pass the name and categoryId to the validation functions.
Your modified onClickSave function should now look like the code in the following listing.
Listing 10.2. onClickSave adjusted to now be onRuntimeInitialized
... Module['onRuntimeInitialized'] = function() { #1 Promise.all([ validateName(clientData.name), #2 validateCategory(clientData.categoryId) #3 ]) .then(() => { #4 }) .catch((error) => { setErrorMessage(error); }); }
No other changes are needed in the nodejs_validate.js file.
To test the validation logic, you can adjust the data in the clientData object by changing the name property to a value that exceeds the MAXIMUM_NAME_LENGTH value of 50 characters (name: "This is a very long product name to test the validation logic.") and saving the file.
To run your JavaScript file in Node.js, open a command prompt, navigate to your Chapter 10\10.3.4 EmFunctionPointers\backend\ folder, and run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.8.
By this point in the chapter, you’ve learned how to work with WebAssembly modules in Node.js when those modules were built with Emscripten’s generated JavaScript code. In the rest of this chapter, you’ll learn how to use WebAssembly modules in Node.js when the modules have been built without generating Emscripten’s JavaScript file.
When using the Emscripten compiler, production code typically includes the generated Emscripten JavaScript file. This file handles downloading the WebAssembly module and interacting with the WebAssembly JavaScript API for you. It also contains a number of helper functions to make interacting with the module easier.
Not generating the JavaScript file is useful for learning because it gives you a chance to download the .wasm file and work with the WebAssembly JavaScript API directly. You create a JavaScript object holding the values and functions that the module is expecting to import, and then you use the API to compile and instantiate the module. Once it’s instantiated, you have access to the module’s exports, allowing you to interact with the module.
As WebAssembly’s use increases, it’s likely that many third-party modules will be created to extend a browser’s abilities. Knowing how to work with modules that don’t use the Emscripten JavaScript code will also be useful if you ever need to use a third-party module that’s been built using a compiler other than Emscripten.
In chapters 3 through 6, you used Emscripten to generate only the .wasm file by using the SIDE_MODULE flag. This created a module that didn’t include any standard C library functions and didn’t generate Emscripten’s JavaScript file. Because the JavaScript file wasn’t generated, it’s now up to you to create the JavaScript needed to load and instantiate the module by using the WebAssembly JavaScript API, as step 4 of figure 10.9 shows.
Figure 10.9. Using Emscripten to generate only the WebAssembly file. You’ll then create the JavaScript to load and instantiate the module using the WebAssembly JavaScript API.
To load and run your side_module.wasm file from chapter 3 in Node.js, you’ll need to load and instantiate the module using the WebAssembly JavaScript API.
The first thing that you need to do is create a folder for the files you’ll use in this section. In your WebAssembly\ folder, create a Chapter 10\10.4.1 SideModuleIncrement\backend\ folder, and then do the following:
- Copy the side_module.wasm file from your Chapter 3\3.5.1 side_module\ folder to your newly created backend\ folder.
- Create a side_module_nodejs.js file in your backend\ folder, and then open it with your favorite editor.
Because Node.js is already running on the server, you don’t need to fetch the .wasm file because it’s sitting on the hard drive in the same folders as the JavaScript files. Instead, you’ll use the File System module in Node.js to read in the WebAssembly file’s bytes. Then, once you have the bytes, the process of calling WebAssembly .instantiate and working with the module is the same as in a browser.
You include the File System module by using the require function, passing in the string 'fs'. The require function returns an object that gives you access to various File System functions, such as readFile and writeFile. In this chapter, you’ll use only the readFile function, but if you’re interested in learning more about the Node.js File System object and the functions that are available, you can visit https://nodejs.org/api/fs.html.
You’re going to use File System’s readFile function to read in the contents of the side_module.wasm file asynchronously. The readFile function accepts three parameters. The first parameter is the path of the file to read. The second is optional and allows you to specify options like the file’s encoding. You won’t use the second parameter in this chapter. The third parameter is a callback function that will receive either an error object—if there was an issue reading in the file’s contents—or, if the read was successful, the file’s bytes.
More Info
If you’d like to read more about the File System module’s readFile function and the optional second parameter, you can visit http://mng.bz/rPjy.
Add the following code snippet to your side_module_nodejs.js file to load the File System object ('fs') and then call the readFile function. If an error is passed to the callback function, then throw the error. Otherwise, pass the bytes that were received to the instantiateWebAssembly function that you’ll create next:
const fs = require('fs'); #1 fs.readFile('side_module.wasm', function(error, bytes) { #2 if (error) { throw error; } #3 instantiateWebAssembly(bytes); #4 });
Create an instantiateWebAssembly function that accepts a parameter called bytes. Within the function, create a JavaScript object called importObject with an env object holding the __memory_base property of 0 (zero). You then need to call the WebAssembly .instantiate function, passing in the bytes received as well as the importObject. Finally, within the then method, call the exported _Increment function from the WebAssembly module, passing in a value of 2. Output the result to the console.
The instantiateWebAssembly function in your side_module_nodejs.js file should look like the code in the next listing.
Listing 10.3. The instantiateWebAssembly function
function instantiateWebAssembly(bytes) { const importObject = { env: { __memory_base: 0, } }; WebAssembly.instantiate(bytes, importObject).then(result => { const value = result.instance.exports._Increment(2); console.log(value.toString()); #1 }); }
To run your JavaScript file in Node.js, open a command prompt, navigate to your Chapter 10\10.4.1 SideModuleIncrement\backend\ folder, and run the following command:
node side_module_nodejs.js
You should see the result of the _Increment function call, as shown in figure 10.10.
The final step of the process, shown in figure 10.11, is to copy the WebAssembly file, validate.wasm (generated in chapter 4, section 4.2.2) to a folder where you’ll host your Node.js files. You’ll then create a JavaScript file that will bridge the gap between interacting with the data received from the browser and interacting with the module.
Figure 10.11. The final step of the process is to copy the generated WebAssembly file to where your Node.js files are and build the JavaScript code to interact with the module.
In your WebAssembly\ folder, create a Chapter 10\10.4.2 SideModule\backend\ folder, and then do the following:
- Copy the editproduct.js and validate.wasm files from your Chapter 4\4.2 side_module\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and open it with your favorite editor.
The JavaScript in the nodejs_validate.js file was written to work in a web browser, so you’ll need to make a few modifications for it to work in Node.js.
Your JavaScript uses the JavaScript TextEncoder object to copy strings to the module’s memory. In Node.js, the TextEncoder object is part of the util package. The first thing that you’ll need to do in your JavaScript file is add a require function for the util package at the beginning of the file, as the following snippet shows:
const util = require('util'); #1
Next, rename the initialData object to clientData:
const clientData = { #1 name: "Women's Mid Rise Skinny Jeans", categoryId: "100", };
In your nodejs_validate.js file, just before the initializePage function, add the following code to have the bytes from the validate.wasm file read in and passed to the instantiateWebAssembly function:
const fs = require('fs'); fs.readFile('validate.wasm', function(error, bytes) { #1 if (error) { throw error; } instantiateWebAssembly(bytes); #2 });
Your next steps are to make the following modifications to the initializePage function:
- Rename the function to instantiateWebAssembly, and give it a parameter called bytes.
- Remove the line of code setting the name, as well as the category code that follows, so that the first thing in the instantiateWebAssembly function is the module-Memory line of code.
- Replace WebAssembly.instantiateStreaming with WebAssembly.instantiate, and replace the fetch("validate.wasm") parameter with bytes.
- Last, within the then method of the WebAssembly.instantiate call, and following the moduleExports line of code, add a call to the validateData function, which you’ll create in a moment.
The modified initializePage function in your nodejs_validate.js file should now look like the code in the next listing.
Listing 10.4. initializePage renamed to instantiateWebAssembly
... function instantiateWebAssembly(bytes) { #1 moduleMemory = new WebAssembly.Memory({initial: 256}); const importObject = { env: { __memory_base: 0, memory: moduleMemory, } }; WebAssembly.instantiate(bytes, importObject).then(result => { #2 moduleExports = result.instance.exports; validateData(); #3 }); } ...
In your nodejs_validate.js file, delete the getSelectedCategoryId function. Then replace the content of the setErrorMessage function with a console.log call for the error parameter, as in the following snippet:
function setErrorMessage(error) { console.log(error); } #1
The next adjustment that you need to make to the nodejs_validate.js file is to rename the onClickSave function to validateData so that it will be called once the module has been instantiated. Within the validateData function, remove the two lines of code above the if statement that get the name and categoryId. In the if statement, prefix the name and categoryId variables with your clientData object.
The validateData function in your nodejs_valdiate.js file should now look like the code in the following listing.
Listing 10.5. onClickSave renamed to validateData
... function validateData() { #1 let errorMessage = ""; const errorMessagePointer = moduleExports._create_buffer(256); if (!validateName(clientData.name, errorMessagePointer) || #2 !validateCategory(clientData.categoryId, #3 errorMessagePointer)) { errorMessage = getStringFromMemory(errorMessagePointer); } moduleExports._free_buffer(errorMessagePointer); setErrorMessage(errorMessage); if (errorMessage === "") { #4 } } ...
The final area that you need to modify is the copyStringToMemory function. In a browser, the TextEncoder object is global; but in Node.js, the object is found in the util package. In your nodejs_validate.js file, you need to prefix the TextEncoder object with the util object that you loaded earlier, as the following code snippet shows:
function copyStringToMemory(value, memoryOffset) { const bytes = new Uint8Array(moduleMemory.buffer); bytes.set(new util.TextEncoder().encode((value + "\0")), #1 memoryOffset); }
No other changes are needed to the JavaScript in the nodejs_validate.js file.
To test the logic, you can adjust the data by changing the value for the categoryId property to a value that isn’t in the VALID_CATEGORY_IDS array (categoryId: "1001"). To run your JavaScript file in Node.js, open a command prompt, navigate to your Chapter 10\10.4.2 SideModule\backend\ folder, and run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.12.
In this section, you learned how to modify the JavaScript to load and instantiate a WebAssembly module that your code calls into. In the next section, you’ll learn how to work with a module that makes calls into your JavaScript.
As an example, a module calling into JavaScript directly would be useful if your module needs to perform a long-running operation. Rather than the JavaScript making a function call and waiting for the results, a module could periodically call into the JavaScript to get more information or provide an update on its own.
When not using Emscripten’s generated JavaScript, which you won’t be doing here, things are a bit different because all the JavaScript code is in the same scope. As a result, a module can call into the JavaScript and have access to the main code, as figure 10.13 shows.
Figure 10.13. How the callback logic will work when not using Emscripten’s generated JavaScript code
In your WebAssembly\ folder, create a Chapter 10\10.4.3 SideModuleCallingJS\backend\ folder, and then do the following:
- Copy the editproduct.js and validate.wasm files from your Chapter 5\5.2.1 SideModuleCallingJS\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and then open it with your favorite editor.
You’re going to modify the nodejs_validate.js file to work in Node.js. The code uses the TextEncoder JavaScript object in the copyStringToMemory function; in Node.js, the TextEncoder object is part of the util package. You’ll need to include a reference to the package so that your code can use the object. Add this code at the beginning of your nodejs_validate.js file:
const util = require('util'); #1
Rename the initialData object to clientData. Then, in your nodejs_validate.js file, before the initializePage function, add the code from the following snippet to read in the bytes from the validate.wasm file and pass them to the instantiateWebAssembly function:
const fs = require('fs'); fs.readFile('validate.wasm', function(error, bytes) { #1 if (error) { throw error; } instantiateWebAssembly(bytes); #2 });
Next, you need to modify the initializePage function by doing the following:
- Rename the function to instantiateWebAssembly, and add a bytes parameter.
- Remove the lines of code that appear before the moduleMemory line of code.
- Change WebAssembly.instantiateStreaming to WebAssembly.instantiate, and replace the fetch("validate.wasm") parameter value with bytes.
- Add a call to the validateData function after the moduleExports line of code in the then method of the WebAssembly.instantiate call.
The modified initializePage function in your nodejs_validate.js file should now look like the code in the next listing.
Listing 10.6. initializePage renamed to instantiateWebAssembly
... function instantiateWebAssembly(bytes) { #1 moduleMemory = new WebAssembly.Memory({initial: 256}); const importObject = { env: { __memory_base: 0, memory: moduleMemory, _UpdateHostAboutError: function(errorMessagePointer) { setErrorMessage(getStringFromMemory(errorMessagePointer)); }, } }; WebAssembly.instantiate(bytes, importObject).then(result => { #2 moduleExports = result.instance.exports; validateData(); #3 }); } ...
In your nodejs_validate.js file, delete the getSelectedCategoryId function. Then, replace the contents of the setErrorMessage function with a console.log call for the error parameter, as shown in the following snippet:
function setErrorMessage(error) { console.log(error); } #1
Revise the onClickSave function by completing the following steps:
- Rename the function to validateData.
- Remove the setErrorMessage(), const name, and const categoryId lines of code.
- Add the clientData object prefix to the name and categoryId values in the if statements.
The modified onClickSave function in your nodejs_validate.js file should now look like this:
function validateData() { #1 if (validateName(clientData.name) && #2 validateCategory(clientData.categoryId)) { #3 #4 } }
The last item that you need to adjust is the copyStringToMemory function. You need to prefix the TextEncoder object with the util object that you loaded earlier.
Your copyStringToMemory function in your nodejs_validate.js file should look like the code in the following snippet:
function copyStringToMemory(value, memoryOffset) { const bytes = new Uint8Array(moduleMemory.buffer); bytes.set(new util.TextEncoder().encode((value + "\0")), #1 memoryOffset); }
No other changes are needed in the nodejs_validate.js file.
To test the validation logic, you can adjust the data in clientData by changing the name property to a value that exceeds the MAXIMUM_NAME_LENGTH value of 50 characters (name: "This is a very long product name to test the validation logic.").
Open a command prompt, navigate to your Chapter 10\10.4.3 SideModule-CallingJS\backend\ folder, and run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.14.
In this section, you learned how to load and work with a WebAssembly module that calls into your JavaScript code directly. In the next section, you’ll learn how to work with a module that calls JavaScript function pointers.
Being able to pass a module a JavaScript function pointer adds flexibility to your code compared to calling into JavaScript directly, because you’re not dependent on a single specific function. Instead, the module can be passed a function as needed, as long as the function signature matches what’s expected.
Also, depending on how the JavaScript is set up, calling a function may require multiple function calls to reach your JavaScript. With a function pointer, the module is calling your function directly.
WebAssembly modules can use function pointers that point to functions that are within the module, or the functions can be imported. In this case, you’ll be using the WebAssembly module that you built in section 6.2 of chapter 6, which is expecting the OnSuccess and OnError functions to be specified, as figure 10.15 shows. When the module calls either function, it’s calling into the JavaScript code.
Figure 10.15. A module that has imported the onSuccess and onError JavaScript functions at instantiation. When the ValidateName module function calls either function, it’s calling into the JavaScript code.
You’re now going to modify the JavaScript code that you wrote for use in the browser in chapter 6 so that it can work in Node.js. In your WebAssembly\ folder, create a Chapter 10\10.4.4 SideModuleFunctionPointers\backend\ folder, and then do the following:
- Copy the editproduct.js and validate.wasm files from your Chapter 6\6.2.2 SideModuleFunctionPointers\frontend\ folder to your newly created backend\ folder.
- Rename the editproduct.js file to nodejs_validate.js, and then open it with your favorite editor.
Your JavaScript code uses the TextEncoder JavaScript object. Because the object is part of the util package in Node.js, the first thing that you’ll need to do is include a reference to the package. Add the code in the following snippet at the beginning of your nodejs_validate.js file:
const util = require('util'); #1
Rename the initialData object to clientData.
In your nodejs_validate.js file, before the initializePage function, add the following code to read in the bytes from the validate.wasm file and pass them to the instantiateWebAssembly function:
const fs = require('fs'); fs.readFile('validate.wasm', function(error, bytes) { #1 if (error) { throw error; } instantiateWebAssembly(bytes); #2 });
Modify the initializePage function by doing the following:
- Rename the function to instantiateWebAssembly, and add a bytes parameter.
- Remove the lines of code that appear before the moduleMemory line of code.
- Change WebAssembly.instantiateStreaming to WebAssembly.instantiate, and replace the fetch("validate.wasm") parameter value with bytes.
- Add a call to the validateData function in the then method of the WebAssembly .instantiate call after the last addToTable function call.
The modified initializePage function in your nodejs_validate.js file should now look like the code in the next listing.
Listing 10.7. initializePage renamed to instantiateWebAssembly
... function instantiateWebAssembly(bytes) { #1 moduleMemory = new WebAssembly.Memory({initial: 256}); moduleTable = new WebAssembly.Table({initial: 1, element: "anyfunc"}); const importObject = { env: { __memory_base: 0, memory: moduleMemory, __table_base: 0, table: moduleTable, abort: function(i) { throw new Error('abort'); }, } }; WebAssembly.instantiate(bytes, importObject).then(result => { #2 moduleExports = result.instance.exports; onSuccessCallback(validateNameCallbacks); }, 'v'); validateOnSuccessCategoryIndex = addToTable(() => { onSuccessCallback(validateCategoryCallbacks); }, 'v'); validateOnErrorNameIndex = addToTable((errorMessagePointer) => { onErrorCallback(validateNameCallbacks, errorMessagePointer); }, 'vi'); validateOnErrorCategoryIndex = addToTable((errorMessagePointer) => { onErrorCallback(validateCategoryCallbacks, errorMessagePointer); }, 'vi'); validateData(); #3 }); } ...
The next change you need to make in your nodejs_validate.js file is to delete the getSelectedCategoryId function. Then replace the contents of the setErrorMessage function with a console.log call for the error parameter:
function setErrorMessage(error) { console.log(error); } #1
Modify the onClickSave function by completing the following steps:
- Rename the function to validateData.
- Remove the setErrorMessage(), const name, and const categoryId lines of code.
- Add the clientData object prefix to the name and categoryId values that are passed to the validateName and validateCategory functions.
The modified onClickSave function in your nodejs_validate.js file should now look like the code in the following listing.
Listing 10.8. onClickSave renamed to validateData
... function validateData() { #1 Promise.all([ validateName(clientData.name), #2 validateCategory(clientData.categoryId) #3 ]) .then(() => { #4 }) .catch((error) => { setErrorMessage(error); }); } ...
Finally, you need to modify the copyStringToMemory function to prefix the TextEncoder object with the util object. Your copyStringToMemory function in the nodejs_validate.js file should look like this:
function copyStringToMemory(value, memoryOffset) { const bytes = new Uint8Array(moduleMemory.buffer); bytes.set(new util.TextEncoder().encode((value + "\0")), #1 memoryOffset); }
No other changes are needed in the nodejs_validate.js file.
To test the validation logic, you can adjust the data in the clientData object by clearing the value from the name property (name: "") and saving the file. Open a command prompt, navigate to your Chapter 10\10.4.4 SideModuleFunctionPointers\backend\ folder, and run the following command:
node nodejs_validate.js
You should see the validation message shown in figure 10.16.
Now: how can you use what you learned in this chapter in the real world?
The following are some possible use cases for what you’ve learned in this chapter:
- As you saw in this chapter, Node.js can be run from the command line, which means you can use your WebAssembly logic locally on your development machine to help you with your day-to-day tasks.
- With web sockets, Node.js can help implement real-time collaboration in your web application.
- You could use Node.js to add a chat component to your game.
You can find the solutions to the exercises in appendix D.
What Emscripten Module property do you need to implement in order to be informed of when the WebAssembly module is ready to be interacted with?
How would you modify the index.js file from chapter 8 so that the dynamic linking logic works in Node.js?
- WebAssembly modules in Node.js are possible, and the JavaScript needed is quite similar to what you used when working in a web browser.
- Modules that include the Emscripten JavaScript code can load and instantiate themselves when you load the JavaScript using the require function. Unlike in the browser, however, there are no global Emscripten helper functions available. All functions within the Emscripten-generated JavaScript file need to be accessed through the return object from the require function.
- Node.js doesn’t support the WebAssembly.instantiateStreaming function. Instead, you need to use the WebAssembly.instantiate function. If you’re writing a single JavaScript file to use a WebAssembly module in both a web browser and Node.js, then you’ll need the feature detection you learned about in chapter 3, section 3.6.
- When loading a WebAssembly file manually in Node.js, you don’t use the fetch method because the WebAssembly file is on the same machine as the JavaScript code that’s being executed. Instead, you read in the WebAssembly file’s bytes from the File System, and then pass the bytes to the WebAssembly.instantiate function.
- Due to scope issues between the code that calls the require function and the generated Emscripten JavaScript, if you add custom JavaScript to Emscripten’s JavaScript file, it should be self-contained and not try to call into the parent code.