In my first post on WebAssembly I dodged the classic “Hello, World!” example as I thought it might be a bit tricky. Little did I realise how hard it was going to be!
In this post I’m going to look at how to solve it. The code for the project can be found here.
The Problem
In JavaScript strings are encoded with utf-8. This format encodes characters that cannot be represented in the ascii character set with more than one byte.
// Convert a pointer from the wasm module to JavaScript string. convertToString (ptr, length) { try { // The pointer is a multi byte character array encoded with utf-8. const array = newUint8Array(this.memory.buffer, ptr, length) const decoder = newTextDecoder() const string = decoder.decode(array) return string } finally { // Free the memory sent to use from the WebAssembly instance. this.free(ptr) } }
// Convert a JavaScript string to a pointer to multi byte character array convertFromString(string) { // Encode the string in utf-8. const encoder = newTextEncoder() const bytes = encoder.encode(string) // Copy the string into memory allocated in the WebAssembly const ptr = this.malloc(bytes.byteLength) const buffer = newUint8Array(this.memory.buffer, ptr, bytes.byteLength + 1) buffer.set(bytes) return buffer } }
Note how malloc and free and used to manage the memory in the WebAssembly module.
The C Side
The C standard library provides support for multi byte characters [1], so I coded up a function to count the letters (not the bytes) of a string, and a function to log “Hello, World!” in english and mandarin.
This was a complete failure!
Looking harder at the examples I noticed I was missing a called to setlocale, so I added that to my C code. The final code looks as follows.
Then I ran the program and BOOM I get an error complaining that wasi_snapshot_preview1 is not defined. I knew what this meant (the libc implementation needed a WASI module to implement system calls), but I didn’t think I needed one, as I’m only using strings.
It turns out that the first thing setlocale does is check some environment variables. To do that it needs some wasi functions to call out of the wasm sandbox.
Implementing a Minimal WASI
After a consulting the WASI API documentation I decided I needed to implement environ_get and it’s companion environ_sizes_get. I put these in a class called Wasi and passed them to the module on instantiation using the wasi_snapshot_preview1 key as follows:
1 2 3
const res = awaitWebAssembly.instantiate(buf, { wasi_snapshot_preview1: wasi, })
Now when I ran the program I got no errors about wasi_snapshot_preview1, but I get a new error saying that proc_exit was missing. I found that in the docs, and it gets called to terminate the process. As I’ve got nothing to terminate I created a stub for it. My Wasi class looks as follows.
// An implementation of WASI which supports the minimum // required to use multi byte characters. classWasi { constructor (env) { this.env = env this.instance = null this.wasiMemoryManager = null }
// Initialise the instance from the WebAssembly. init = (instance) => { this.instance = instance this.wasiMemoryManager = newWasiMemoryManager( instance.exports.memory, instance.exports.malloc, instance.exports.free ) }
staticWASI_ESUCCESS = 0
// Get the environment variables. environ_get = (environ, environBuf) => { const encoder = newTextEncoder() const view = newDataView(this.wasiMemoryManager.memory.buffer)
// Create the Wasi instance passing in some environment variables. const wasi = newWasi({ "LANG": "en_GB.UTF-8", "TERM": "xterm" })
// Instantiate the wasm module. const res = awaitWebAssembly.instantiate(buf, { wasi_snapshot_preview1: wasi, env: { // This function is exported to the web assembly. consoleLog: function(ptr, length) { // This converts the pointer to a string and frees he memory. const string = wasi.wasiMemoryManager.convertToString(ptr, length) console.log(string) } } })
// Initialise the wasi instance wasi.init(res.instance)
return wasi }
module.exports = setupWasi
The Example
The last thing to do was write the example. Here it is:
// Get the functions exported from the WebAssembly const { countLetters, sayHelloWorld, sayHelloWorldInMandarin } = wasi.instance.exports
let buf1 = null try { const s1 = '犬 means dog' // Convert the JavaScript string into a pointer in the WebAssembly buf1 = wasi.wasiMemoryManager.convertFromString(s1) // The Chinese character will take up more than one byte. const l1 = countLetters(buf1.byteOffset) console.log(`expected length ${s1.length} got ${l1} byte length was ${buf1.byteLength}`) } finally { // Free the pointer we created in WebAssembly. wasi.wasiMemoryManager.free(buf1) }
// Should log "Hello, World!" to the console sayHelloWorld() sayHelloWorldInMandarin() }
main().then(() =>console.log('Done'))
And it works! Here’s the output.
1 2 3 4
expected length 11 got 11 byte length was 14 Hello World 你好,世界! Done
Thoughts
I realise node already has a WASI module, but I wanted to be able to run the code in a browser. I would have preferred to use wasmer-js, but I had problems getting it to work, and it felt like a bit of a sledgehammer for the problem I was trying to solve.
This post describes how to use the C standard library with WebAssembly enabled DataFrames. You can find the source code here.
Introduction
In my previous post I avoided solving the problem of linking to standard library to provide memory allocation for my WebAssembly project by writing my own memory allocator. In order to use external C packages I will need a standard library.
wsi-libc
The wasi-libc project does what it says on the tin by providing a standard library for wasm.
I have changed my setup as my laptop required reinstallation. I’m now running Ubuntu 20.04 LTS.
I installed the latest (version 10) LLVM tool chain as a Debian package which I got from this page. I did the following.
1 2
wget https://apt.llvm.org/llvm.sh sudo ./llvm.sh
That installed clang et al with the suffix “-10”, such that clang could be found as /usr/bin/clang-10.
I cloned the wasi-libc library and built it as follows.
1 2 3
git clone git@github.com:WebAssembly/wasi-libc.git cd wasi-libc sudo WASM_CC=clang-10 WASM_NM=llvm-nm-10 WASM_AR=llvm-ar-10 INSTALL_DIR=/opt/wasi-libc make install
The C Source Code
In my previous post I had two source files: memory-allocation.c and array-methods.c. We can delete memory-allocation.c as we’ll use malloc and free from the standard library. I split he array methods into two files: array-methods-int.c and array-methods-double.c, included stdlib.h and changed the memory allocation to use the standard library.
Here is the start of array-methods-int.c.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include<stdlib.h>
__attribute__((used)) int* addInt32Arrays(int *array1, int* array2, int length) { int* result = (int*) malloc(length * sizeof(int)); if (result == 0) return0;
for (int i = 0; i < length; ++i) { result[i] = array1[i] + array2[i]; }
The compilation stage node sets --sysroot. This appears to set up the include paths correctly.
The link stage adds the path to the libraries -L$(SYSROOT)/lib/wasm32-wasi and and includes libc and libm -lc -lm.
The JavaScript Code
All I needed to do was to change the memory allocation imports to use malloc and free.
Everything just worked!
As a test I added a unary function logFloat64Array. This required sme minor changes to the javascript to allow ‘int’ type series to fall through to the double methods. Again it all just worked.
Things To Do
I need to understand what the crt1.o file is all about. I tried to link, but that gives me other errors.
In my previous posts [1] [2] [3] [4] I demonstrated how to write a simple pandas-style data frame in JavaScript using WebAssembly (a near native speed virtual machine available in every modern browser or server-side engine such as nodejs). This demonstrated the following features:
Elegant Syntax
Expressions can be written in the following manner.
All operations are written in C and are performed in a compiled WebAssembly module.
Portable.
The code runs in any browser or server side JavaScript engine (e.g. chrome, nodejs, etc.).
Uses standard tooling.
The C code was compiled with the current standard clang compiler which supports WebAssembly out of the box.
The Future of Data Science?
Clearly this is a bold claim!
There are currently two clear platform leaders in the data science eco-system: R, and Python using the scipy toolkit, and also a number of attractive outsiders. The choice of which platform to choose typically depends on language preference, but more compellingly what packages a particular language/version provides. It is not unusual for a project to be constrained to a legacy version of a platform simply because a key package has not been ported to a current version.
So how can JavaScript and WebAssembly help?
Elegant Syntax
Surprisingly, yes! The posts referenced above clearly demonstrate how JavaScript can be written in a manner which elegantly represents vector-arithmetic.
Speed
With WebAssembly there is no significant compromise in speed over natively compiled languages.
Bindings
With other languages a binding must be written to integrate a package into the language. In many cases this is completely unnecessary for WebAssembly.
Fortran
In my opinion this is the killer advantage. There are a huge number of key data science libraries implemented in many different flavours of Fortran. Binding to these libraries is an ongoing and fraught task.
Happily LLVM (a language compiler supported by the majority of operating systems) is currently incorporating flang (a Fortran compiler) into it’s toolchain, supporting all versions of Fortran up to and including 2018. This provides the possibility of compiling any Fortran package directly to WebAssembly.
The Catch
The first catch is operator overloading. I’m not sure if “catch” is the right word. My solution using the babel transpiler works fine, and transpiling is how React (one of the most popular web frameworks, which uses custom syntax in JavaScript) does and will always work. There is a proposal to incorporate operator overloading into JavaScript (currently at stage 1 where 3 means acceptance), which leads me to believe it is a feature which my become native to the language.
The second catch is the standard library. You’ll note if you’ve read the previous posts that I had to provide a memory allocator as this is not available in the WebAssembly environment. WebAssembly is just a virtual machine, and at present it doesn’t come with the usual support code which most libraries depend on. This is all being built out at the present in the form of wasi, but it’s not here yet.
Personally I don’t care what language I write in, as long as it doesn’t get in the way of the problem I’m trying to solve. Because of the low cost of entry, JavaScript has become ubiquitous, and so it’s level of documentation and support is huge, which I like.
If it provides seamless access to the packages I need I’m a buyer.
static fromObject (data, types) { const series = {} for (let i = 0; i < data.length; i++) { for (const column in data[i]) { if (!(column in series)) { series[column] = newSeries(column, newArray(data.length), types[column]) } series[column][i] = data[i][column] } } const seriesList = Object.values(series) returnnewDataFrame(seriesList) }
toString () { const columns = Object.getOwnPropertyNames(this.series) let s = columns.join(', ') + '\n' const maxLength = Object.values(this.series) .map(x => x.length) .reduce((accumulator, currentValue) =>Math.max(accumulator, currentValue), 0) for (let i = 0; i < maxLength; i++) { const row = [] for (const column of columns) { if (i < this.series[column].length) { row.push(this.series[column][i]) } else { row.push(null) } } s += row.join(', ') + '\n' } s += columns.map(column =>this.series[column].type).join(', ') + '\n' return s } }
The only changes are in the fromObject helper method which takes an extra argument for the types and the toString to print the types. A DataFrame is now constructed as follows.
if (output.byteOffset === 0) { thrownewRangeError('Failed to allocate memory') }
const result = Array.from(output)
return result } finally { // Ensure the memory gets freed. this.freeMemory(input.byteOffset) this.freeMemory(output.byteOffset) } }
invokeBinaryFunction(func, lhs, rhs, typedArrayType) { if (lhs.length !== rhs.length) { thrownewRangeError('Arrays must the the same length') } const length = lhs.length
let input1 = null let input2 = null let output = null
// Run the async main function. main().then(() =>console.log('Done')).catch(error =>console.error(error))
Thoughts
Clearly the DataFrame needs a lot of work. There is no concept of grouping, indices, dates and times, etc. However we have a working proof on concept that provides a syntactically elegant and efficient implementation.
Following on from my previous post regarding passing arrays between JavaScript and WebAssembly, I have found out how to remove the need to call back into JavaScript
I installed clang in /opt/clang, wabt in /opt/wabt, and I use nodenv to manage my nodejs environment.
Update your path.
1
export PATH=/opt/clang/bin:/opt/wabt/bin:$PATH
What was the problem?
In order to find the size of the memory and grow it, I was having to import functions from JavaScript. I now find there are two built in functions that do exactly what I want:
__builtin_wasm_memory_size(0)
__builtin_wasm_memory_grow(0, blocks)
This means my growMoreMemory function now looks like this.
In plain old JavaScript this is not possible. Let’s see how we can implement it.
The goal
So what is a DataFrame? To answer this we first need to look at a Series.
So What is a Series?
A Series is the building block of a DataFrame. I take a Series to be a named vector (array), which will be initialized as follows.
1
let s1 = newSeries('height', [1.82, 1.76, 1.72, 1.89])
The series should be indexable.
1
s1[0] === 1.82
The series should support vector arithmetic.
1 2 3
let height = newSeries('height', [1.82, 1.76, 1.72, 1.89]) let weight = newSeries('weight', [81.3, 73.2, 68.9, 92.1]) let score = height / weight
So What is a DataFrame?
I take a DataFrame to be a collection of Series.
1 2 3 4
let df = DataFrame([ height, weight ])
The problems to solve
There are two problems to solve in order to implement this in JavaScript:
Operator overloading
Property accessing
Operator Overloading
We want to support vector arithmetic (multiplying two arrays). Unfortunately JavaScript does not support operator overloading so we will have to pre-process the code. We can do this with the babel transpiler and a plugin. I’m using the @jetblack/operator-overloading plugin, which is a bag-of-bolts, but I wrote it so I know how it works!
Property accessing
In order for a series to have both a name and be indexable we need control over the property accessing. We can do that with a Proxy object. The Proxy object provides a layer of indirection between requests on the object, and the actions performed.
Setting up your environment
Lets write some code!
First install the node modules. I’m using babel, the operator overloading plugin, and standardjs as a linter and formatter.
1 2 3 4 5 6 7 8
# Initialise the package.json npm init -y # Install the babel tool chain. npm install --save-dev @babel/core@7.10.1 @babel/preset-env@7.10.2 @babel/cli@7.10.1 @babel/node@7.10.1 # Install the operator overloading plugin. npm install --save-dev git+https://github.com/rob-blackbourn/jetblack-operator-overloading.git#0.1.0 # Install standardjs for linting and formatting npm install --save-dev babel-eslint@10.1.0 standard@14.3.4
We configure standardjs by editing the package.json and adding the following.
1 2 3 4 5 6
{ ... "standard":{ "parser":"babel-eslint" } }
If you are using using vscode create the .vscode/settings.json as follows then restart vscode to start the standardjs server.
Configure babel by creating the .bablerc file with the usual preset and the operator overloading plugin. The operator overloading plugin requires arrow functions. Targeting node (or any modern browser) achieves this.
The constructor returns a Proxy object, which intercepts calls to the Series. It first checks if the property or function is provided by the Series itself. If not it delegates the action to the array. Note how the Proxy is returned from the constructor; this is a poorly documented feature.
The operators are provided by the [Symbol.for('+')] methods.
DataFrame Code
In the src directory create a file called DataFrame.js with the following content.
static fromObject (data) { const series = {} for (let i = 0; i < data.length; i++) { for (const column in data[i]) { if (!(column in series)) { series[column] = newSeries(column, newArray(data.length)) } series[column][i] = data[i][column] } } const seriesList = Object.values(series) returnnewDataFrame(seriesList) }
toString () { const columns = Object.getOwnPropertyNames(this.series) let s = columns.join(', ') + '\n' const maxLength = Object.values(this.series) .map(x => x.length) .reduce((accumulator, currentValue) =>Math.max(accumulator, currentValue), 0) for (let i = 0; i < maxLength; i++) { const row = [] for (const column of columns) { if (i < this.series[column].length) { row.push(this.series[column][i]) } else { row.push(null) } } s += row.join(', ') + '\n' } return s } }
This would be pretty short without the toString!
As with the Series class we use a Proxy object to control property accessing.
I decided to keep the constructor clean; it just takes an array of Series. However, in the real world we want a variety of constructors. The convenience class method DataFrame.fromObject provides a way of building the series from a list of objects.
--target=wasm32-unknown-unknown-wasm tells the compiler/linker to produce wasm.
--optimize=3 seems to ne necessary to produce valid wasm. I don’t know why, and I might be wrong.
-nostdlib tells the compiler/linker that we don’t have a standard library, which is very sad.
-Wl,--export-all tells the linker to export anything it can.
-Wl,--no-entry tells the linker that there’s no main; this is just a library.
-Wl,--allow-undefined tells the linked to allow the code to access functions and variables that have not been defined. We’ll need to provide them when we instantiate the WebAssembly instance. This won’t be used in this example, but we’ll need it later.
--output example1.wasm does what it says on the tin.
If all went well you now have an example.wasm file.
Write the JavaScript
Write a JavaScript file example1.js with the following content.
// Create a WebAssembly instance from the wasm. const res = awaitWebAssembly.instantiate(buf, {}) // Get the function to call. const { addTwoInts } = res.instance.exports // Call the function. const a = 38, b = 4 const result = addTwoInts(a, b) console.log(`${a} + ${b} = ${result}`) }
main().then(() =>console.log('Done'))
The code comments should make it pretty clear whats happening here. We read the compiled wasm into a buffer, instantiate the WebAssembly, get the function, run it, and print out the result.
The WebAssembly.instantiate function is a promise, and I have chosen to use the await syntax to make the code more readable, so I make an async main function which I call as a promise on the last line.
Lets run it.
1
node example1.js
If all went well you should see the following.
1 2
38 + 4 = 42 Done
What just happened?
The bit I want to talk about here is how the wasm got instantiated. The first argument to WebAssembly.instantiate was the buf holding the wasm. The second was an empty importObject. The importObject can configure the properties of the instance it creates. This might include the memory it uses, a table of function references, imported and exported functions, etc. So why does an empty object work?
We can look at the WebAssembly text (or wat) with the wabt tool wasm2wat. To produce this we need to do the following.
1
wasm2wat example2.wasm -o example2.wat
The example2.wat file should look like this (edited for readability).
Near the top you can see our addTwoInts function with a satisfyingly small amount of code which is almost understandable. Then there are mentions of table and memory. At the end wel can see memory exported along with our function.
What has happened is that the clang tool-chain has generated a bunch of stuff that we would otherwise need to put in the importObject. You can switch this off (see here) and control everything from the JavaScript side, but I quite like it.
Passing arrays to wasm
Now we’ve established our tools work, and seen some of the features clang provides we can get to arrays.
Write the C code
Write a file example2.c with the following contents.
1 2 3 4 5 6 7 8 9
__attribute__((used)) intsumArrayInt32(int *array, int length) { int total = 0;
for (int i = 0; i < length; ++i) { total += array[i]; }
return total; }
Compile it to wasm as we did before.
Write the JavaScript
Write a javascript file call example2.js with the following contents.
asyncfunctionmain() { // Load the wasm into a buffer. const buf = fs.readFileSync('./example2.wasm')
// Instantiate the wasm. const res = awaitWebAssembly.instantiate(buf, {}) // Get the function out of the exports. const { sumArrayInt32, memory } = res.instance.exports // Create an array that can be passed to the WebAssembly instance. const array = newInt32Array(memory.buffer, 0, 5) array.set([3, 15, 18, 4, 2])
// Call the function and display the results. const result = sumArrayInt32(array.byteOffset, array.length) console.log(`sum([${array.join(',')}]) = ${result}`)
// This does the same thing! if (result == sumArrayInt32(0, 5)) { console.log(`Memory is an integer array starting at 0`) } }
main().then(() =>console.log('Done'))
The first part is the same as the previous example.
The wasm instance has a memory buffer which is exposed to JavaScript as an ArrayBuffer in memory.buffer. We create our Int32Array using the first available “memory location” 0, and specify that it is 5 integers in length. Note the memory location is in terms of bytes, and the length in terms of integers. The set method copies the JavaScript array into the memory buffer.
Then we call the function.
1
const result = sumArrayInt32(array.byteOffset, array.length)
We pass in the offset in bytes to the array in the memory buffer and the length (in integers) of the array. This gets passed to the function we wrote in C.
1
intsumArrayInt32(int *array, int length)
The result gets passed back as an integer.
Passing output arrays to wasm
This gives us some of what we want, but what if we want to get an array back from wasm? The first approach is to pass an output array.
Write the C code
Write a file example3.c with the following contents.
1 2 3 4 5 6
__attribute__((used)) voidaddArraysInt32(int *array1, int* array2, int* result, int length) { for (int i = 0; i < length; ++i) { result[i] = array1[i] + array2[i]; } }
The function takes in three arrays, multiplying each element of the two input arrays and storing the output in the result array. Nothing need be returned.
Write the JavaScript
Write a file called example3.js with the following content.
// Call the function. addArraysInt32( array1.byteOffset, array2.byteOffset, result.byteOffset, length) // Show the results. console.log(`[${array1.join(", ")}] + [${array2.join(", ")}] = [${result.join(", ")}]`) }
main().then(() =>console.log('Done'))
We can see some differences here in the way we create the arrays. The code to create the first array looks the same, but what’s going on for the others?
Our first example was easy, as we only had one array. When we create subsequent arrays we need to lay them out in memory such that they don’t overlap each other. To do this we calculate the number of bytes required with length * Int32Array.BYTES_PER_ELEMENT and add it to the previous offset to ensure each array has it’s own space. Note again how the offset is in bytes, but the length is in units of integers.
This maps on to the prototype of our C implementation.
1
voidaddArraysInt32(int *array1, int* array2, int* result, int length)
Now we can get array data out of wasm!
But wait, something is missing. What if we want to create an array inside the wasm module and pass it out? What if our array calculation needs to create it’s own arrays in order to support the calculation?
To do that we need to provided some memory management.
Understanding wasm memory management
As we have seen, wasm memory is managed in JavaScript through the WebAssembly.Memory object. This has a buffer which is an ArrayBuffer. The buffer has a byteLength property, which gives us the upper bound of the memory.
It also has a grow method which can be used to get more memory.
I couldn’t find any documentation on how to get to this information from inside the wasm instance, but we can solve this problem by calling back to the JavaScript from wasm.
Calling JavaScript from wasm
We can call JavaScript from wasm by importing a function when we instantiate the wasm module.
Write the C code
Write a file example4.c with the following contents.
Here declare an external function someFunction that will be provided by JavaScript, and then export callSomeFunction that, when invoked, will run the imported function.
Write the JavaScript
Write a file called example4.js with the following content.
asyncfunctionmain() { // Load the wasm into a buffer. const buf = fs.readFileSync('./example4.wasm')
// Make the instance, importing a function called someFunction which // logs its arguments to the console. const res = awaitWebAssembly.instantiate(buf, { env: { someFunction: function (i) { console.log(`someFunction called with ${i}`) } } }) // Get the exported function callSomeFunction from the wasm instance. const { callSomeFunction } = res.instance.exports
// Calling the function should call back into the function we imported. callSomeFunction(42) }
main().then(() =>console.log('Done'))
Rather than passing in an empty object to WebAssembly.instantiate as we did previously, we now pass an object with an env property. This contains a property someFunction with it’s implementation (it just logs its argument to the console).
Calling the function callSomeFunction calls back to the function we provided to the wasm module someFunction.
Now we have a way of the wasm module finding out how much memory it has and asking for more.
Passing arrays from wasm to JavaScript
First we need some C code to perform memory management. I wrote a trivial implementation of malloc which can be found here. Copy this file to the work folder as memory-allocation.c The code provides three functions.
/* * A simple memory manager. */ classMemoryManager { // The WebAssembly.Memory object for the instance. memory = null
// Return the buffer length in bytes. memoryBytesLength() { returnthis.memory.buffer.byteLength }
// Grow the memory by the requested blocks, returning the new buffer length // in bytes. grow(blocks) { this.memory.grow(blocks) returnthis.memory.buffer.byteLength } }
// Create an object to manage the memory. const memoryManager = newMemoryManager()
// Instantiate the wasm module. const res = awaitWebAssembly.instantiate(buf, { env: { // The wasm module calls this function to grow the memory grow: function(blocks) { memoryManager.grow(blocks) }, // The wasm module calls this function to get the current memory size. memoryBytesLength: function() { memoryManager.memoryBytesLength() } } })
// Get the memory exports from the wasm instance. const { memory, allocateMemory, freeMemory, reportFreeMemory } = res.instance.exports
// Give the memory manager access to the instances memory. memoryManager.memory = memory
// How many free bytes are there? const startFreeMemoryBytes = reportFreeMemory() console.log(`There are ${startFreeMemoryBytes} bytes of free memory`)
// Get the exported array function. const { addArrays } = res.instance.exports
// Make the arrays to pass into the wasm function using allocateMemory. const length = 5 const bytesLength = length * Int32Array.BYTES_PER_ELEMENT
// Add the arrays. The result is the memory pointer to the result array. result = newInt32Array( memory.buffer, addArrays(array1.byteOffset, array2.byteOffset, length), length)
// Show that some memory has been used. pctFree = 100 * reportFreeMemory() / startFreeMemoryBytes console.log(`Free memory ${pctFree}%`)
// Free the memory. freeMemory(array1.byteOffset) freeMemory(array2.byteOffset) freeMemory(result.byteOffset)
// Show that all the memory has been released. pctFree = 100 * reportFreeMemory() / startFreeMemoryBytes console.log(`Free memory ${pctFree}%`) }
main().then(() =>console.log('Done'))
That’s a lot of code!
Let’s get started with the memory management. The script starts declaring a MemoryManagement class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
classMemoryManager { // The WebAssembly.Memory object for the instance. memory = null
// Return the buffer length in bytes. memoryBytesLength() { returnthis.memory.buffer.byteLength }
// Grow the memory by the requested blocks, returning the new buffer length // in bytes. grow(blocks) { this.memory.grow(blocks) returnthis.memory.buffer.byteLength } }
Once the memory property has been set it can provide the length in bytes of the memory, and it can grow the memory for a given number of blocks returning the new memory length in bytes.
We pass the functions when creating the wasm instance, and assign the memory object once to instance is created.
The memory gets freed with the complimentary function near the end of the script.
1
freeMemory(array1.byteOffset)
Finally we can call our function and get the results.
1 2 3 4
result = newInt32Array( memory.buffer, addArrays(array1.byteOffset, array2.byteOffset, length), length)
Note that what gets returned is the point in memory where the array has been stored, so we need to wrap it in an Int32Array object. And don’t forget to free it!
Outro
Well that was a lot of code.
Obviously we can wrap this all up in some library code to hide the complexity, but what have we gained. I can’t say for sure, but if the array calculations run at near native speed this could provide enormous performance gains.
I could (and probably should) have used a libc implementation instead of implementing malloc. I decided not to because I think it kept the focus on the array passing problem, and not on integrating with libraries, which is a whole other discussion.