Shannon's Hatchet

something, but almost nothing

Friday, February 24, 2023

TypeScript modules With Emscripten and CMake, part 4

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.

Building with CMake

Just by screwing around on the command line, we were previously able to produce a more or less useful CommonJS module wrapping the real-valued FFT function from the Kiss FFT library (though not as useful as the existing one on npmjs.org). Now let’s look at how we can build a module with CMake as part of the library’s build system.

As a reminder, we configured CMake to build the library with:

emcmake cmake -S . -B jsbuild -DCMAKE_BUILD_TYPE=Debug \
    -DKISSFFT_TOOLS=OFF -DKISSFFT_STATIC=ON -DKISSFFT_TEST=OFF

When configuring using emcmake, the EMSCRIPTEN variable is defined, so if we want to make all of those flags the defaults, we can add this to CMakeLists.txt after the option definitions (line 54 in the current source):

if(EMSCRIPTEN)
  set(KISSFFT_TOOLS OFF)
  set(KISSFFT_STATIC ON)
  set(KISSFFT_TEST OFF)
endif()

Now let’s add a target to build our module. This is a bit “special” for two reasons:

  • The CMake functions for Emscripten treat any output (even a module) as an “executable”, so we have to make believe we’re linking a program.
  • Even though all of the C code is already in the libkissfft-float.a library, which CMake references with the kissfft target, it still expects to have at least one source file to link into our “executable”.

To satisfy CMake, we will first simply create an empty C file:

touch api.c

We may at some point want to add helper functions for our API, so this isn’t entirely useless - see the corresponding file in SoundSwallower for an example.

Now we will add the necessary CMake configuration to the end of CMakeLists.txt:

if(EMSCRIPTEN)
  add_executable(kissfft.cjs api.c)
  target_link_libraries(kissfft.cjs kissfft)
  target_link_options(kissfft.cjs PRIVATE
    -sMODULARIZE=1
    -sEXPORTED_FUNCTIONS=@${CMAKE_CURRENT_SOURCE_DIR}/exported_functions.txt)
  em_link_post_js(kissfft.cjs api.js)
endif()

A few things to note here:

  • em_link_post_js is not documented, but should be.
  • We have to add ${CMAKE_CURRENT_SOURCE_DIR} to the path to exported_functions.txt so that CMake can find it, since we are building in a separate directory.
  • We can’t use kissfft as the target name since that is already taken by the C library.

Emscripten will automatically append .js and .wasm to the target name, so, after adding this, if you run:

emcmake cmake -S . -B jsbuild -DCMAKE_BUILD_TYPE=Debug
cmake --build jsbuild

You should find the files kissfft.cjs.js and kissfft.cjs.wasm in the jsbuild directory.

Building an ES6 module

Up to this point we have built a CommonJS module, since they are simpler to use in Node.js, but in reality, all the cool kids are now using ES6 modules, and they are particularly preferred when using a bundler for the Web like Webpack or Esbuild. The latest versions of Emscripten do have built-in, if occasionally buggy, support for producing ES6 modules. So, we can add an extra target inside the if(EMSCRIPTEN) block at the end of CMakeLists.txt:

add_executable(kissfft.esm api.c)
target_link_libraries(kissfft.esm kissfft)
target_link_options(kissfft.esm PRIVATE
  -sMODULARIZE=1 -sEXPORT_ES6=1
  -sEXPORTED_FUNCTIONS=@${CMAKE_CURRENT_SOURCE_DIR}/exported_functions.txt)
em_link_post_js(kissfft.esm api.js)

Sadly, there is no way in the Emscripten CMake support to choose a different file extension for a specific target, so we can’t call this kissfft.esm.mjs. In addition, the boilerplate loader code that Emscripten gives us won’t allow us to share the WebAssembly (which is identical) between targets. For the moment we will end up with kissfft.esm.js and kissfft.esm.wasm in the jsbuild directory, and this is a Problem, as we will see soon.

Packaging with NPM

Now that everything is built, it is actually quite simple to package this as an NPM package. No other action is required on your part… well, not quite. First, let’s create a package.json file, which will have one big problem, that we’ll get to later:

{
  "name": "kissfft-example",
  "version": "0.0.1",
  "description": "A very simple example of packaging WebAssembly",
  "types": "./index.d.ts",
  "main": "./jsbuild/kissfft.cjs.js",
  "exports": {
    ".": {
      "types": "./index.d.ts",
      "require": "./jsbuild/kissfft.cjs.js",
      "import": "./jsbuild/kissfft.esm.js",
      "default": "./jsbuild/kissfft.esm.js"
    }
  },
  "author": "David Huggins-Daines <dhd@ecolingui.ca>",
  "homepage": "https://ecolingui.ca/en/blog/emguide",
  "license": "MIT",
  "scripts": {
    "test": "npx tsc test_realfft.ts && node test_realfft.js"
  },
  "files": [
    "index.d.ts",
    "jsbuild/kissfft.*.js",
    "jsbuild/kissfft.*.wasm"
  ],
  "devDependencies": {
    "@types/node": "^18.14.1",
    "typescript": "^4.9.5"
  },
  "dependencies": {
    "@types/emscripten": "^1.39.6"
  }
}

Of note above:

  • We use the exports field to supply different entry points for import and require (but note that this won’t actually work… more below).
  • We just package the stuff we built in place, by including only the files we need with the files field.
  • We point to the type definition file with the types field in two places, for good luck.
  • Although the node we get with emsdk includes @types/emscripten by default, others will not, so it is a package (and not dev) dependency.

Now, assuming you have you have previously created test_realfft.ts (if not, download it here), you should be able to run:

npm install
npm test

And you should see the same output we saw previously. But, did we say there was a problem? Yes. The nifty ES6 model built above won’t actually work in Node, because the Node developers somehow can’t agree to not depend on file extensions to select module systems. Since our package contains both CommonJS (loaded with require) and ES6 (loaded with import) modules, we have to change the file extension on at least one of them to satisfy Node’s simplistic view of the world.

The path of least resistance to fix this and still stay CMakically correct is to add a custom command that copies the built .js file for the ES6 module to a .mjs file:

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/kissfft.esm.mjs
  DEPENDS kissfft.esm
  COMMAND ${CMAKE_COMMAND} -E copy
  ${CMAKE_CURRENT_BINARY_DIR}/kissfft.esm.js
  ${CMAKE_CURRENT_BINARY_DIR}/kissfft.esm.mjs)
add_custom_target(copy-mjs-bork-bork-bork ALL
  DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/kissfft.esm.mjs)

Now we will modify package.json by changing kissfft.esm.js to kissfft.esm.mjs everywhere, and modifying files to specifically only include the files we need:

  "files": [
    "index.d.ts",
    "jsbuild/kissfft.esm.mjs",
    "jsbuild/kissfft.cjs.js",
    "jsbuild/kissfft.*.wasm"
  ],

You can download the updated version here. And now we can test that both import types work by creating a directory called kissfft-test alongside kissfft, creating the files index.mjs (download here) and index.cjs (download here) in it, then running:

npm link ../kissfft
node index.mjs
node index.cjs

Congratulations! You now have a WebAssembly module that will work as both ES6 and CommonJS, and can also be uploaded to NPM (but please don’t do that). To see what would be packaged, you can run:

npm publish --dry-run

In the next installment, we will see what we can do to make the module as small as possible.

Friday, February 24, 2023

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).

Friday, February 24, 2023

TypeScript modules With Emscripten and CMake, part 2

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.

Exporting functions

In the previous episode, we successfully built a CommonJS module and accompanying WebAssembly file, which we loaded in Node with require, but which contains no useful code. There are two reasons for this:

The first order of business, then, is to get some functions exported. There are exactly four of these that we care about, and while we could put them on the emcc command-line or declare them as exported in the code, it is better in the long run to list them in a text file. So, create the file exported_functions.txt containing:

_kiss_fftr_alloc
_kiss_fftr
_malloc
_free

You’ll notice we had to add a leading underscore to the names, which is an Emscripten convention, for some reason. Now, if you run:

emcc -o kissfft.js jsbuild/libkissfft-float.a -sMODULARIZE=1 \
    -sEXPORTED_FUNCTIONS=@exported_functions.txt

You should see a kissfft.wasm of a more impressive size. More to the point, you can, actually, import the module and run these functions. Create the script test_kissfft.js containing:

const assert = require("assert");
require("./kissfft.js")().then((kissfft) => {
  let fftr = kissfft._kiss_fftr_alloc(16, 0, 0, 0);
  assert.ok(fftr);
  console.log(`fftr is ${fftr}`);
});

When you run it, it will produce some quite useless output resembling:

$ node test_kissfft.js
fftr is 70216

Um, hooray? This takes a bit of explaining, which will also explain why kissfft.js exists and why it’s so incredibly huge (don’t worry, we will make it smaller eventually).

In the beginning, Emscripten would compile all your code into JavaScript, and exported functions would be attributes on a global module object called, obviously, Module. This is not so great if you want to make a CommonJS or ES6 module instead of just mashing everything into the global namespace like it’s 1995. Also, if your code is quite large, you might want to do other things (like display a web page) while it loads and compiles.

So, when you pass -sMODULARIZE=1 to emcc, what you get when you call require on the generated JavaScript is not the module itself, but rather a function returning a Promise to return that module. This is necessary because currently you can’t directly import WebAssembly from JavaScript code but must use the asynchronous-only WebAssembly API. The various code in kissfft.js handles the loading of the WebAssembly and exporting its functions into a module object.

Although we use this Promise directly above, it is usually more convenient to await on it, like this:

const createModule = require("./kissfft.js");
const kissfft = await createModule();

Calling C functions directly

With all that explanation out of the way, let’s get down to the business of actually using the functions we exported above.

As noted previously these become properties of the object that we get when Promise returned by calling the function that require returns resolves (what a mouthful!). They otherwise work just like they do in C, with the obvious exception that all of their arguments are simple JavaScript numbers with no type-checking whatsoever.

What does this mean if you have strings, pointers, etc? Well, you can use the Emscripten utilities ccall and cwrap for simple cases, especially involving C strings. The rest of that page is not worth reading (WebIDL, inlining JavaScript in C… who does that?) with the exception of the section on directly accessing memory, which is super important, because if you have any kind of interesting data, that is what you will have to do, but also super wrong (as of writing this), because it misses one extremely important detail. Read on to find out which one!

As a reminder, we are doing all this because we have some time-domain data, which we would like to transform into frequency-domain data. Specifically, we are using the real-valued FFT function kiss_fftr, which reads from an array of nfft real values (in this case, float which is 32 bits by definition) and writes to an array of nfft / 2 + 1 complex numbers, which are represented with a struct that looks like this:

typedef struct {
    kiss_fft_scalar r; // (NOTE: these are floats)
    kiss_fft_scalar i;
} kiss_fft_cpx;

No, it’s not really documented how Emscripten organizes and aligns structs in memory, but we can be reasonably sure that they are packed, and we can assume an array of 9 kiss_fft_cpx is equivalent to an array of 18 float.

So let’s create these arrays! We’ll do this by creating a Float32Array on the JavaScript side then copying it into the module’s memory space (sadly there is no more efficient way to do this). To keep things manageable, we’ll do a 16-point FFT of a waveform with a single component at half the Nyquist frequency (yes, really). You can see the equivalent C code here.

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]);

Now we’ll figure the length of the input and the output and allocate them in the module’s address space (you may be asking, but why aren’t we checking the return value of _malloc? By default, Emscripten is configured to panic if malloc fails instead of returning NULL):

const nfft = timedata.length;
const nfreq = nfft / 2 + 1;
const ctimedata = kissfft._malloc(nfft * 4); // float
const cfreqdata = kissfft._malloc(nfreq * 4 * 2); // complex

And we will copy the data to the address we allocated using set on the module’s HEAP8 array, first taking a view of it as a Uint8Array. THIS IS SUPER IMPORTANT! as otherwise JavaScript will helpfully convert each of your floating-point values to an 8-bit integer, which is definitely not what you want:

kissfft.HEAP8.set(new Uint8Array(timedata.buffer), ctimedata);

Great! Now we can just call the function with the “pointers” (which are really just indices into HEAP8) we created above:

kissfft._kiss_fftr(fftr, ctimedata, cfreqdata);

To get the output array, we use slice to make a copy (confusingly, this is the oppposite of what a “slice” does in every other programming language in existence) of the memory and then take a view of it as a Float32Array:

const freqdata = new Float32Array(
      kissfft.HEAP8.slice(cfreqdata, cfreqdata + nfreq * 4 * 2).buffer);

And finally, we should deallocate everything so that we don’t run out of memory:

kissfft._free(ctimedata);
kissfft._free(cfreqdata);
kissfft._free(fftr);

You can download the full test script here.

Obviously, it’s not a great idea to have an API full of functions with arbitrary number parameters and no type-checking, though this is JavaScript, after all, so perhaps some people consider that to be totally acceptable. In the next installment, we will find out how to make a safer and more programmer-friendly API!