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.