Compiling Clojure to WebAssembly

Post about compiling clojure to a web assembly module using GraalVM native image.

  ยท   3 min read

I recently watched The Future of Write Once, Run Anywhere: From Java to WebAssembly by Patrick Ziegler & Fabio Niephaus. This conference talked demo’d compiling a Java application in the browser, and running that locally. Not only that, but they also compiled Java bytecode to web assembly, and simpy ran it with node command.

I have a Clojure application, cljcc ( a toy C compiler ) which I always wanted a web version of. I was thinking of implementing the core library to be common across clojure/script, but after seeing the above talk wanted to try directly compiling to WASM. I have mentioned the steps I followed for the same in this post.

Compilation #

For compiling to WASM, it’s just about changing the backend target in the Graal VM native image tool. It builds on the setup I described in this post.

Reference Babashka Task Runner

;; added a new babashka task
lib:build:wasm {:doc "Builds native WASM image."
                  :depends [lib:build:jar]
                  :task (shell {:dir "target/lib"}
                               "native-image"
                               "--tool:svm-wasm"
                               "-jar" "cljcc-lib.jar"
                               "-o" "cljcc-lib-wasm"
                               "--features=clj_easy.graal_build_time.InitClojureClasses"
                               "--initialize-at-build-time"
                               "--verbose")}

It first builds the Clojure codebase to a jar file ( the :depends step ), and then use the option --tool:svm-wasm in the native image tool, which generates WASM.

Right now it’s only supported in the latest early access version of jdk.

# uses sdkman for managing jdk version
sdk use java 25.ea.18-graal # specified in the original conference talk
# might also need brew install binaryen

This generates three files.

# /target/lib
cljcc-lib-wasm.js      cljcc-lib-wasm.js.wasm cljcc-lib-wasm.js.wat  cljcc-lib.jar

bun run cljcc-lib-wasm.js # Runs the main function

Frontend #

I wanted to use the js and wasm files in simple frontend application. User can enter C source and specify the compilation stage, which will call the js file above and show the output.

At first, I was sort of confused. Java programs will have void main(args ... ) method as the starting point to binary. As this function doesn’t return anything, how should I capture the output of the program ?

Luckily this was simple, as output stdout, stderr are translated to the browser’s console.log, error. Simply copied all calls to console methods, and stored them in a local variable.

var originalConsoleLog = console.log;
var capturedLogs = [];
console.log = function() {
    var args = Array.from(arguments).map(arg =>
        typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
    );
    var message = args.join(' ');

    capturedLogs.push(message);
    originalConsoleLog.apply(console, arguments);
};

The other problem was how to actually use the generated js and wasm files. The js file is filled with IIFEs. Just loading the file immediately runs the entire compiler.

The generated js file has a top level GraalVM object. It has the main methods, such as run, which starts the WASM instantiation, loads the arguments etc. I commented out the IIFEs which execute the GraalVM.run method in the generated js file. Attached this GraalVM object to window, so I could access it from other files and then manually called the .run methods present on this object.

async function runCompiler(source, stage) {
    try {
        const args = [source, "linux", stage];
        const config = new GraalVM.Config();
        await GraalVM.run(args,config).catch(console.error);
    } catch (e) {
        // ...
    }
}

I don’t think this is the ideal way to go about this. But it works for now.

Added a few buttons to specify the compilation stage, code examples and it’s done ! The generated wasm file is 23 megabytes !, so the first execution takes a significant time as it pulls the file.

I have only implemented a subset of C’s features, ( following the book Writing a C Compiler by Nora Sandler).

You can play with the compiler at cljcc.shagunagrawal.me.