01. Concepts
01. Concepts ź“ė Ø
Examples that express some of the major underlying concepts in WebAssembly. Some of these examples are not the most convenient or productive way for building projects with WebAssembly. However, these minimal examples are great for learning, or developing straightforward / lower-level parts of an application.
Introduction
Let's do a brief introduction into major concepts of WebAssembly:
- WebAssembly is a compile-targeted language for running bytecode on the web.
- Relative to Javascript, WebAssembly offers predictable performance. It is not inherently faster than Javascript, but it can be faster than JavaScript in the correct use case. Such as computationally intensive tasks, like nested loops or handling large amounts of data. Therefore, WebAssembly is a complement to JavaScript, and not a replacement.
- WebAssembly is extremely portable. WebAssembly runs on: all major web browsers, V8 runtimes like Node.js, and independent Wasm runtimes like Wasmtime,
bytecodealliance/lucet
, andwasmerio/wasmer
. - WebAssembly has Linear Memory, in other words, one big expandable array. And in the context of Javascript, synchronously accessible by Javascript and Wasm.
- WebAssembly can export functions and constants, And in the context of Javascript, synchronously accessible by Javascript and Wasm.
- WebAssembly, in its current MVP, only handles integers and floats. However, tools and libraries exist to make passing high-level data types convenient.
With that, let's take a look at our Hello World to see some of the concepts in action.
Hello World!
Before getting started, be sure to check out all of the languages available, by clicking the "languages" dropdown in the header.
Overview
For our first program, we will be doing a "Hello world" type of program in Rust and rustwasm/wasm-pack
.
To keep things simple with Wasm's limitations mentioned in the introduction example, instead of displaying a string, we will add two numbers together and display the result. Though, it is good to keep in mind, in later examples, a lot of these limitations will be abstracted away by your WebAssembly Language of choice (In this case, Rust). It is also highly recommended you take a look at the wasm-pack QuickStart Guide, as it will be referenced a lot in this example.
Project Setup
So first, Let's get rust installed, which includes cargo. Then, using cargo, let's install wasm-pack, which we will need later:
cargo install wasm-pack
Next, let's create our rust crate in our current directory using cargo:
cargo init
Then, let's edit our new Cargo.toml
to implement wasm-pack as mentioned in their quickstart:
[package]
name = "hello-world"
version = "0.1.0"
authors = ["Your Name <your@name.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
Lastly, let's take a quick peek inside at the src/
directory. Since we are building a library (lib
) to be used by a larger application, we need to rename the src/
main.rs
to src/
lib.rs
. Go ahead and do that now before moving forward.
Now that we have our project and environment setup, let's go ahead and start the actual implementation.
Implementation
Let's go ahead and replace src/
lib.rs
with the required use
call as mentioned in the quickstart, as well as our add function:
// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.
// Import the wasm-bindgen crate.
use wasm_bindgen::prelude::*;
// Our Add function
// wasm-pack requires "exported" functions
// to include #[wasm_bindgen]
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
return a + b;
}
Then, let's compile our crate using wasm-pack, into a wasm module. Then run the following command, taking note of the --target web
. The wasm-pack tool has support for a lot of different output types, especially for bundlers like Webpack or Rollup. But, since we want an ES6 module in our case, we use the web
target below:
wasm-pack build --target web
This will output a pkg/
directory containing our wasm module, wrapped in a js object. Next, lets create an index.js
JavaScript file, and import the outputted ES6 module in our pkg/
directory. Then, we will call our exported add()
function:
Note
In this example, we are using the exported function from the wasm module directly to help highlight the WebAssembly API. wasm-bindgen
generates JavaScript bindings code that can be imported as an ES6 import, and is the recommended way to work with your Rust Wasm modules. These JavaScript bindings are shown in the "Passing High Level Data Types with wasm-bindgen
" example.
// Import our outputted wasm ES6 module
// Which, export default's, an initialization function
import init from "./pkg/hello_world.js";
const runWasm = async () => {
// Instantiate our wasm module
const helloWorld = await init("./pkg/hello_world_bg.wasm");
// Call the Add function export from wasm, save the result
const addResult = helloWorld.add(24, 24);
// Set the result onto the body
document.body.textContent = `Hello World! addResult: ${addResult}`;
};
runWasm();
Lastly, lets load our ES6 Module, index.js
Javascript file in our index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World - Rust</title>
<script type="module" src="./index.js"></script>
</head>
<body></body>
</html>
And we should have a working Wasm Add (Hello World) program! Congrats!
You should have something similar to the demo (Source Code) below:
Demo
<html>
<head>
<meta charset="UTF-8">
<title>Hello World - Rust</title>
<link rel="preload" href="./index.js" as="script">
</head>
<body>Hello World! addResult: 48</body>
</html>
Next let's take a deeper look at WebAssembly Exports.
Exports
Overview
In our Hello World Example, we called a function exported from WebAssembly, in our Javascript. Let's dive a little deeper into exports and how they are used.
Implementation
If you haven't done so already, you should set up your project following the steps laid out in the Hello World Example example.
First, let's add the following to our src/
lib.rs
file:
// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.
// Import the wasm-bindgen crate.
use wasm_bindgen::prelude::*;
// This exports an add function.
// It takes in two 32-bit integer values
// And returns a 32-bit integer value.
#[wasm_bindgen]
pub fn call_me_from_javascript(a: i32, b: i32) -> i32 {
return add_integer_with_constant(a, b);
}
// A NOT exported constant
// Rust does not support exporting constants
// for Wasm (that I know of).
const ADD_CONSTANT: i32 = 24;
// A NOT exported function
// It takes in two 32-bit integer values
// And returns a 32-bit integer value.
fn add_integer_with_constant(a: i32, b: i32) -> i32 {
return a + b + ADD_CONSTANT;
}`
Then, let's compile that using rustwasm/wasm-pack
, which will create a pkg/
directory:
wasm-pack build --target web
Next, lets create an index.js
file to load and run our wasm output. Let's import the wasm initialization module from pkg/
exports.js
that was generated by wasm-pack. Then, let's call the module passing in the path to our wasm file at pkg/
exports_bg.wasm
that was generated by wasm-pack. Then, let's go ahead and call out exported functions, and explore what functions were NOT exported:
Note
In this example, we are using the exported function from the wasm module directly to help highlight the WebAssembly API. wasm-bindgen
generates JavaScript bindings code that can be imported as an ES6 import, and is the reccomended way to work with your Rust Wasm modules. These JavaScript bindings are shown in the "Passing High Level Data Types with wasm-bindgen
" example.
import wasmInit from "./pkg/exports.js";
const runWasm = async () => {
// Instantiate our wasm module
const rustWasm = await wasmInit("./pkg/exports_bg.wasm");
// Call the Add function export from wasm, save the result
const result = rustWasm.call_me_from_javascript(24, 24);
console.log(result); // Should output '72'
console.log(rustWasm.ADD_CONSTANT); // Should output 'undefined'
console.log(rustWasm.add_integer_with_constant); // Should output 'undefined'
};
runWasm();
Lastly, lets load our ES6 Module, index.js
Javascript file in our index.html
. And you should get something similar to the demo (Source Code) below!
Demo
<html>
<head>
<meta charset="UTF-8">
<title>Hello World - Rust</title>
</head>
<body>
<script type="module" src="./index.js"></script>
<pre style="border: 1px solid black;">
DOM Console:
72
undefined
undefined
</pre>
</body>
</html>
Next let's take a look at WebAssembly Linear Memory.
WebAssembly Linear Memory
Overview
Another feature of WebAssembly, is its linear memory. Linear memory is a continuous buffer of unsigned bytes that can be read from and stored into by both Wasm and Javascript. In other words, Wasm memory is an expandable array of bytes that Javascript and Wasm can synchronously read and modify. Linear memory can be used for many things, one of them being passing values back and forth between Wasm and Javascript.
In rust, tools like rustwasm/wasm-bindgen
, which is part of wasm-pack workflow, abstracts away linear memory, and allows using native data structures between rust and Javascript. But for this example, we will use simple byte (Unsigned 8-bit integer) buffers and pointers (Wasm memory array indexes) as a simple(r) way to pass memory back and forth, and show off the concept.
Let's see how we can use linear memory:
Implementation
First, let's add the following to our src/
lib.rs
file:
// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.
// Import the wasm-bindgen crate.
use wasm_bindgen::prelude::*;
// Create a static mutable byte buffer.
// We will use for passing memory between js and wasm.
// NOTE: global `static mut` means we will have "unsafe" code
// but for passing memory between js and wasm should be fine.
const WASM_MEMORY_BUFFER_SIZE: usize = 2;
static mut WASM_MEMORY_BUFFER: [u8; WASM_MEMORY_BUFFER_SIZE] = [0; WASM_MEMORY_BUFFER_SIZE];
// Function to store the passed value at index 0,
// in our buffer
#[wasm_bindgen]
pub fn store_value_in_wasm_memory_buffer_index_zero(value: u8) {
unsafe {
WASM_MEMORY_BUFFER[0] = value;
}
}
// Function to return a pointer to our buffer
// in wasm memory
#[wasm_bindgen]
pub fn get_wasm_memory_buffer_pointer() -> *const u8 {
let pointer: *const u8;
unsafe {
pointer = WASM_MEMORY_BUFFER.as_ptr();
}
return pointer;
}
// Function to read from index 1 of our buffer
// And return the value at the index
#[wasm_bindgen]
pub fn read_wasm_memory_buffer_and_return_index_one() -> u8 {
let value: u8;
unsafe {
value = WASM_MEMORY_BUFFER[1];
}
return value;
}
Then, let's compile that using rustwasm/wasm-pack
, which will create a pkg/
directory:
wasm-pack build --target web
Next, lets create an index.js
file to load and run our wasm output. Let's import the wasm initialization module from pkg/
webassembly_linear_memory.js
that was generated by wasm-pack. Then, let's call the module passing in the path to our wasm file at pkg/
webassembly_linear_memory_bg.wasm
that was generated by wasm-pack. Then, let's go ahead and read and write to memory from both Wasm and JS. Please read through the commented code for context. And be sure to read the note at the bottom of this code example. Let's dive into our resulting index.js
:
Note
In this example, we are using the exported function from the wasm module directly to help highlight the WebAssembly API. wasm-bindgen
generates JavaScript bindings code that can be imported as an ES6 import, and is the reccomended way to work with your Rust Wasm modules. These JavaScript bindings are shown in the "Passing High Level Data Types with wasm-bindgen
" example.
const runWasm = async () => {
const rustWasm = await wasmInit("./pkg/webassembly_linear_memory_bg.wasm");
/**
* Part one: Write in Wasm, Read in JS
*/
console.log("Write in Wasm, Read in JS, Index 0:");
// First, let's have wasm write to our buffer
rustWasm.store_value_in_wasm_memory_buffer_index_zero(24);
// Next, let's create a Uint8Array of our wasm memory
let wasmMemory = new Uint8Array(rustWasm.memory.buffer);
// Then, let's get the pointer to our buffer that is within wasmMemory
let bufferPointer = rustWasm.get_wasm_memory_buffer_pointer();
// Then, let's read the written value at index zero of the buffer,
// by accessing the index of wasmMemory[bufferPointer + bufferIndex]
console.log(wasmMemory[bufferPointer + 0]); // Should log "24"
/**
* Part two: Write in JS, Read in Wasm
*/
console.log("Write in JS, Read in Wasm, Index 1:");
// First, let's write to index one of our buffer
wasmMemory[bufferPointer + 1] = 15;
// Then, let's have wasm read index one of the buffer,
// and return the result
console.log(rustWasm.read_wasm_memory_buffer_and_return_index_one()); // Should log "15"
/**
* NOTE: if we were to continue reading and writing memory,
* depending on how the memory is grown by rust, you may have
* to re-create the Uint8Array since memory layout could change.
* For example, `let wasmMemory = new Uint8Array(rustWasm.memory.buffer);`
* In this example, we did not, but be aware this may happen :)
*/
};
runWasm();
Lastly, lets load our ES6 Module, index.js
Javascript file in our index.html
. And you should get something similar to the demo (Source Code) below!
Demo
<html>
<head>
<meta charset="UTF-8">
<title>Hello World - Rust</title>
<link rel="preload" href="./index.js" as="script">
</head>
<body>
<script type="module" src="./index.js"></script>
<pre style="border: 1px solid black;">
DOM Console:
Write in Wasm, Read in JS, Index 0:
24
Write in JS, Read in Wasm, Index 1:
15
</pre>
</body>
</html>
Next let's take a look at importing JavaScript functions into WebAssembly.
Importing Javascript Functions Into WebAssembly
Overview
When you are instantiating Wasm modules, you are able to pass in an importObject
. This importObject
can be used to call host (Javascript) functions within Wasm!
In rust, tools like rustwasm/wasm-bindgen
, which is part of wasm-pack workflow, abstracts away the importObject
.
In this example, we will import and implement a simple console.log
which is called within Wasm. This example is inspired by the rustwasm/wasm-bindgen
- console_log
example , but simplified. So let's jump into the example:
Implementation
First, let's add the following to our src/
lib.rs
file:
// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.
// Import the wasm-bindgen crate.
use wasm_bindgen::prelude::*;
// Let's define our external function (imported from JS)
// Here, we will define our external `console.log`
#[wasm_bindgen]
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
// `log(..)`
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// Export a function that will be called in JavaScript
// but call the "imported" console.log.
#[wasm_bindgen]
pub fn console_log_from_wasm() {
log("This console.log is from wasm!");
}
Then, let's compile that using wasm-pack, which will create a pkg/
directory:
wasm-pack build --target web
Next, lets create an index.js
file to load and run our wasm output. Let's import the wasm initialization module from pkg/
importing_javascript_functions_into_webassembly.js
that was generated by wasm-pack. Then, let's call the module passing in the path to our wasm file at pkg/
importing_javascript_functions_into_webassembly_bg.wasm
that was generated by wasm-pack. Then, we will go ahead and call our exported wasm function, that will call the "imported" console.log
JavaScript function:
Note
In this example, we are using the exported function from the wasm module directly to help highlight the WebAssembly API. wasm-bindgen
generates JavaScript bindings code that can be imported as an ES6 import, and is the reccomended way to work with your Rust Wasm modules. These JavaScript bindings are shown in the "Passing High Level Data Types with wasm-bindgen
" example.
const runWasm = async () => {
// Instantiate our wasm module
const rustWasm = await wasmInit(
"./pkg/importing_javascript_functions_into_webassembly_bg.wasm"
);
// Run the exported function
rustWasm.console_log_from_wasm(); // Should log "This console.log is from wasm!"
};
runWasm();
Lastly, lets load our ES6 Module, index.js
Javascript file in our index.html
. And you should get something similar to the demo (Source Code) below!
Demo
And that's it for the basics! Next, lets took a look at some "Advanced Web Demos", with an example of Reading and Writing Graphics with WebAssembly.
<html>
<head>
<meta charset="UTF-8">
<title>Hello World - Rust</title>
<link rel="preload" href="./index.js" as="script">
</head>
<body>
<script type="module" src="./index.js"></script>
<p><b>Please check your JavaScript console for the results.</b></p>
</body>
</html>
console.log('This console.log is from wasm!')