We demonstrate how to build a WebAssembly module that depends on malloc, linking in a pre-built malloc implementation at runtime, using a JS binding trick to handle the circular reference between the two WebAssembly modules. We then optimize the load process to ensure we are fetching these modules in parallel.
Malloc implementation: https://github.com/guybedford/wasm-stdlib-hack/blob/master/dist/memory.wasm
WASM Fiddle: https://wasdk.github.io/WasmFiddle/?1feerp
Demo Repo: https://github.com/guybedford/wasm-intro
[00:00] For a simple example of dynamic memory allocation in WebAssembly, let's consider that we have a record data type which has some arbitrary fields associated with it. Say, for example, an ID, an X, and a Y value.
[00:14] To dynamically create one of these records, we're going to use a createRecord function, which will take its fields as its arguments. Then we can use malloc within this function to create the memory for each record. We need to pass into malloc the size of the record in bytes so we can use the sizeof operator here.
[00:34] The value we get back is then the memory address of the new record, where is therefore a pointer to a record. We can now use this pointer to set the ID X and Y values directly into that new memory address. Finally, we will return the memory address from the createRecord function, so the function is therefore returning a pointer to a record.
[00:58] To create a deleteRecord function, this is going to be a function with no return value that takes a record address in memory, and then calls free on that memory address. Of course, there's more efficient and much better ways to do this, but this is just for an example. In order to use malloc and free, I'm going to include the standard library as well.
[01:21] Taking a look at the generated WebAssembly code, we can see that the free and malloc functions are both treated as imported functions. It's up to us to link them in ourselves. The memory is being exported, and then we've got our createRecord and deleteRecord functions defined.
[01:38] Looking at the deleteRecord function, we can see that it's taking a 32-bit integer as its argument, which is a memory address in WebAssembly. It's then calling the free function with that argument number from the local parameter.
[01:52] Briefly looking at the createRecord function, we can also see that it's calling malloc, storing it in a local variable, number three, which is defined over here. Then it's using the F32.store, I32.store, and F32.store calls to do our record saves in memory of the ID X and Y values that were passed as parameters before finally returning the memory address, which was stored in the local variable.
[02:16] Let's download the WebAssembly code, and get this wired in. In my local project, I've included the fetch and instantiate WASM helper, which I'm going to local program.wasm file that we just downloaded.
[02:28] For the imports object, I'm going to need to pass an environment import, which contains the malloc and free implementations. I'll get to that in a moment. This promise resolved to the WebAssembly module exports, which contains our createRecord function.
[02:43] I'm just going to call this with arbitrary values, and then log the results. For linking in malloc and free, I've actually already prebuilt from C into WebAssembly a malloc and free implementation, which I'm just going to load from the existing file at memory.wasm.
[02:59] The link to this file is included in the lesson description. This memory.wasm file takes a single environment import, which sets the memory. The reason for this is that malloc and free both need to act on the program's memory, which is defined in the program itself, because it exported its own memory.
[03:16] We can only work with a single memory at a time in WebAssembly, at least currently, so we want to pass that program's memory into the memory's memory. I'm going to access that from m.memory, where it was exported.
[03:30] The module value that we get back from memory.wasm then contains our malloc and our free functions bound to the program's memory. How do we get these malloc and free functions into the imports of the program.wasm?
[03:44] I'm going to use an indirection here by creating temporary placeholders for these functions called WASMmalloc and WASMfree. In the environment imports to program.wasm, I'm actually going to create a JavaScript implementation of malloc, and returns the WASMmalloc result applied to that length.
[04:04] The same for the free function, which will take an address, and return the WASMfree of that address. We now just need to assign the WASMmalloc from the version that we got back from the memory, and the same for the WASMfree implementation.
[04:19] Lastly, I'm just going to make sure that we're calling our createRecord function only once everything has been set up. When we run this, we can see that we're getting a dynamically allocated memory address.
[04:30] I have done performance measurements to see what the cost is of these JS bindings. Because we're not linking directly to the WebAssembly, we're jumping through JavaScript, there's a very, very small performance overhead on my tests of about five percent on otherwise very highly performing code.
[04:47] If that's something that worries you, a better thing to do here is to have program.wasm as well import its memory. There's another performance concern in this code, which is that we're fetching program.wasm, and then only once that is fetched and compiled are we fetching memory.wasm.
[05:03] What we really want to be doing is fetching in parallel so that we utilize the network as best as possible. We can adjust this fetch and instantiate WASM function to convert it into a parallel fetch and compile function that'll instead take an array of URLs to fetch and compile.
[05:20] We use this to fetch program and memory in parallel. Then we need to call WebAssembly.instantiate ourselves. This takes the imports object as its second argument, so the rest of the code is actually much the same. I'll just copy all that in.
[05:34] Then the final difference is that the return value of WebAssembly.instantiate is a module object, so we need to access the exports property on that directly. That gives us the parallel loader.