4 mains or nothing at all.
In my previous post, I explored how Binaryen's Asyncify helped me implement setjmp in WebAssembly, successfully working around WASI libc's dependency on the 'exception handling proposal' - a feature still unsupported by the majority of WebAssembly runtimes.
While this approach got ExifTool running in the browser, there is a limitation: files larger than 2GB couldn't be processed.
When running WASI programs in the browser, you need a file system. The straightforward approach is a memory-based implementation; a dictionary mapping file paths to their contents. This means loading the contents of a file into memory, which becomes an issue with larger files. Browser memory limitations prevent allocating more than 2GB without throwing errors1.
So, in our memory file system, instead of the value of that dictionary being a buffer, could we have a “pointer” to a file? Yes! The File API lets us do just that, and the underlying Blob type provides methods for "slicing" files to access specific ranges without loading the entire file into memory.
There's just one complication: web I/O APIs are asynchronous, while system languages typically use synchronous I/O. When compiling to WebAssembly, bridging these different paradigms becomes necessary. Fortunately, this is what Asyncify is meant for, so theoretically we should just need to tell wasm-opt which of our imports are asynchronous, and it should work seamlessly:
What is going on?
We’re in a war of attrition against our tooling.
Clang is sneaking around
The first problem is that if the Clang linker driver sees -O,
it automatically uses wasm-opt if it’s found on your system path. However applying wasm-opt optimization before the asyncify pass causes misoptimization. This wasn’t an issue when our build of Perl executed synchronously and imports didn’t change asyncify state, but once one did, everything collapsed on itself. Perl is a complex project, and in many places -O
gets passed for you; so to workaround this we need to create a fake wasm-opt that lives on the path:
#!/bin/sh
set -e
input=
output=
while [ $# -ne 0 ]; do
case "$1" in
-o)
shift
output=$1
;;
-*)
# ignore other options
;;
*)
input=$1
;;
esac
shift
done
if [ -z "$input" ]; then
echo "missing input binary"
exit 1
fi
if [ -z "$output" ]; then
echo "missing output binary"
exit 1
fi
if [ "$input" != "$output" ]; then
cp "$input" "$output"
fi
This way whenever the Clang linker driver invokes wasm-opt, the input file just gets copied as the output.
O stack, where art thou?
To support setjmp, we have a runtime that drives it, and we wrap our entry point to look like this:
int main(int argc, char **argv)
{
return asyncjmp_rt_start(real_pmain, argc, argv);
}
Asyncify assumes that calling `asyncjmp_rt_start` won't change the asyncify state as it handles all unwinds and rewinds within its call. However, it's not true when using asyncify-wasm because `asyncjmp_rt_start`
doesn't handle unwinds from imported functions and they are handled by the asyncify-wasm wrappers.
The wrong assumption makes the `main`
function and its callers including crt's `__main_void`
and `_start`
non-unwindable.
So we need to trick the wasm-opt Asyncify pass to make to make a call of `asyncjmp_rt_start`
unwindable by indirecting the call through a volatile function pointer. This way, Asyncify has to conservatively insert unwinding/rewinding code around the call.In the end, we end up with something like this:
And like magic, it works:
Closing
With these changes implemented in zeroperl now has full support for asynchronous web APIs; it should serve as a great foundation for any developers who need a portable Perl runtime. You can also find an NPM package for ExifTool that uses it here.
I also could not have gotten this over the finish line without the insight of Yuta Saito; if you love WebAssembly please sponsor him on Github.
Except in Safari?