Tinkering With WebAssembly
Working with PDFs on the web is annoying. Let's imagine a niche use case. We want to take a snapshot of a part of our web app. Also, export it as a PDF. Traditionally, if you are not relying on a 3rd party API, you send HTML document as a string to the server to convert it to the PDF format. Then download the PDF and render it on the client. Support of document conversion in the JS ecosystem is not as good as in Haskell, C or C++. Pandoc, written in Haskell, is a general purpose document conversion tool. Similarly, libwkhtmltox
is a set of C and C++ libraries that converts HTML to PDF format. Document conversion on the server works well but doesn't scale for a large number of clients because such conversions are computation expensive.
Since 2017 WASM ecosystem is maturing at a rapid pace. It is also gaining traction. Autocad is ported to run in the browsers. PSPDFKIT, a company that offers a document conversion SDK, have cross compiled their 500000 lines c++ pdf conversion codebase to target WASM. Products like Google Earth and Figma are using WebAssembly in production. I have seen some Haskell cross compilers out there that target WASM. So, in theory, Pandoc can be cross-compiled to target WASM for general purpose document conversion in the browser.
For the use case that I described above, I am trying to use libwkthmltox
, a C library, to convert an HTML document to PDF in the browser. I want to leverage this battle-tested C library with minimal glue code in C to solve my problem. The glue code exposes an API that takes in a pointer to a string (UTF-8 encoded HTML document) and returns another pointer that contains PDF formatted document. JS runtime and WASM runtime share memory indirectly using a linear memory model. We can use this feature to pass pointers around.
Emscripten is a compiler toolchain that does all the heavy lifting. It compiles C code first to LLVM bitcode using Clang then finally output a .wasm
file along with boilerplate HTML and js.
Forget about the document conversion for a minute. Then solving this problem becomes simply a matter of string manipulation. Here is a function in C that takes a string concatenate it with another string and returns the new string. EMSCRIPTEN_KEEPALIVE
macro comes from emscripten.h
header file. It tells Emscripten to preserve this function while tree shaking.
const char* EMSCRIPTEN_KEEPALIVE get_string(char* input_str) {
char str[] = "Hello, ";
return strcat(str, input_str);
}
After transpiling C to WASM using Emscripten, from the Browser (boilerplate HTML and js code generated by Emscripten takes care of loading the WASM module in the browser) we can call the get_string
function like this:
UTF8ToString(Module._get_string(allocateUTF8("World!")))
// "Hello, World!"
This contrived example is easy to write and run, but real challenge appears when I try to use the libwkthmltox
library for actual document conversion. Emscripten documentation says, in the "using libraries" section, that to use 3rd party libraries, you need to build them to LLVM bitcode and then link them. For libraries that have a straightforward build process, it is not hard to do. Here is an excellent example of compiling libwebp
, a c library for image compression, to WASM. However, compiling libwkhtmltox
is not straightforward. It even has a separate repository just for building for different architectures. It uses a patched version of Qt. During the build, It compiles Qt from the source too.
At this stage, I am stuck.