TypeScript modules With Emscripten and CMake, part 3
When I set out to create an NPM package for SoundSwallower, I was unable to find much relevant information in the Emscripten documentation or elsewhere on the Web, so I have written this guide, which walks through the process of compiling a library to WebAssembly and packaging it as a CommonJS or ES6 module.
This is part of a series of posts. Start here to read from the beginning.
Creating an API wrapper
Where we left off, we were able to call
into the Kiss FFT library from JavaScript code, but the interface left
a lot to be desired, as we were messing around with arbitrary number
values pointing into the module’s memory space. What we would like to
do is to wrap these dangerous functions in easier to use
functions/methods, and also provide some type definitions so
TypeScript can complain when you try to do some Undefined Behaviour.
Despite what you may have been led to believe, it’s almost never a good idea to generate these sorts of wrappers automatically as it leads to un-idiomatic APIs and handling the special cases is usually more time-consuming than just writing (and testing) the necessary functions by hand.
Like most respectable C libraries, Kiss FFT encapsulates state using
an opaque pointer with functions to allocate and free this, um,
object. This easily lends itself to being wrapped in an
object-oriented TypeScript API. Let’s start by creating the type
definitions for that API, in index.d.ts
(you must give it this name
so that TypeScript’s inscrutably Byzantine lookup
rules
can find it):
/// <reference types="emscripten" />
export class RealFFT {
fft(timedata: Float32Array): Float32Array;
delete(): void;
};
export interface KissFFTModule extends EmscriptenModule {
RealFFT: {
new(nfft: number): RealFFT;
};
};
declare const createModule: EmscriptenModuleFactory<KissFFTModule>;
export default createModule;
Note the somewhat curious way in which the RealFFT
constructor is
declared. Because of the way we load the module object, we can’t
actually export any functions or classes (which are functions,
remember, this is JavaScript) from it directly, but must instead
define them as properties on its interface. Luckily, TypeScript gives
us at least one way to do this, which is shown above.
Also note that we have an explicit delete
method. THIS
WILL NOT BE CALLED AUTOMATICALLY because JavaScript is still
a defective language in 2023 and JavaScript programmers do not care
about memory leaks. The consequence of not calling delete
when your
RealFFT
object goes out of scope is that, eventually, the module’s
memory space (which is some finite amount, 4MB by default I think)
will be used up and its malloc
will panic. This is less bad than
crashing your browser, but still kind of bad, so please call delete
,
as shown in the example below. Alternately, you can make the API
stateless and allocate and deallocate the FFT state on each call to
the FFT code, which is what
kissfft-wasm does, and in
this case is probably quite acceptable.
You can now create a TypeScript file which uses the interface in
test_realfft.ts
:
const assert = require("assert");
require("./kissfft.js")().then((kissfft) => {
const fftr = new kissfft.RealFFT(16);
const timedata = new Float32Array([0, 0.5, 0, -0.5,
0, 0.5, 0, -0.5,
0, 0.5, 0, -0.5,
0, 0.5, 0, -0.5]);
const freqdata = fftr.fft(timedata);
for (let i = 0; i < freqdata.length / 2; i__) {
console.log(`${i}: ${freqdata[i * 2]} + ${freqdata[i * 2 + 1]}j`);
}
fftr.delete(); // please do this
});
And you can already test-compile it to make sure the types are good:
npm install --save-dev typescript @types/node
npx tsc test_realfft.ts
Cool! Now we just have to implement it ☺. We will use the
--post-js
flag to emcc
to attach some JavaScript code to
kissfft.js
at “link” time. In this case we will make a file called
api.js
containing this code which you can see closely resembles our
previous script that directly called the
module functions, with the exception that we don’t need to refer to
them as attributes on the module, since this code will be inserted
inside the module loading code:
class RealFFT {
constructor(nfft) {
this.fftr = _kiss_fftr_alloc(nfft, 0, 0, 0);
}
fft(timedata) {
const nfft = timedata.length;
const nfreq = nfft / 2 + 1;
const ctimedata = _malloc(nfft * 4);
const cfreqdata = _malloc(nfreq * 4 * 2);
HEAP8.set(new Uint8Array(timedata.buffer), ctimedata);
_kiss_fftr(this.fftr, ctimedata, cfreqdata);
const freqdata = new Float32Array(
HEAP8.slice(cfreqdata, cfreqdata + nfreq * 4 * 2).buffer);
_free(ctimedata);
_free(cfreqdata);
return freqdata;
}
delete() {
_free(this.fftr);
}
}
Module.RealFFT = RealFFT;
(You may once again be asking, but why aren’t we checking the return
value of _malloc
? Because Emscripten is configured to panic if
malloc
fails
instead of returning NULL
).
Nou you can recompile the module to include this code:
emcc -o kissfft.js jsbuild/libkissfft-float.a --post-js api.js \
-sMODULARIZE=1 -sEXPORTED_FUNCTIONS=@exported_functions.txt
And we can run our (transpiled) test code and verify that it produces the expected output:
$ node test_realfft.js
0: 0 + 0j
1: 0 + 0j
2: 0 + 0j
3: 0 + 0j
4: -4.898587410340671e-16 + -4j
5: 0 + 0j
6: 0 + 0j
7: 0 + 0j
8: 0 + 0j
Cool! We have successfully wrapped a C library in a more or less friendly TypeScript module. If you aren’t planning to publish this module anywhere and don’t care about testing, repeatable builds, or code size, you can stop here.
In the next installment, we will handle setting up CMake to build our module as part of the library build when run under Emscripten, and package the resulting module for NPM (but please don’t actually upload it, since, as mentioned before, an exsting, full-featured module already exists there).