Memgraph, Client Adapters and WebAssembly. What?
It’s Kostas from the Memgraph the Core Team, and I am thrilled to share more about something cool I have been working on recently as a side project. As you probably have guessed from the title - it's all about WebAssembly. Since there are many details to cover, this will be a three-part article series that will gradually explain how and why we decided to use WebAssembly at Memgraph. But before I deep dive into the WebAssembly world, I would like to share a short story.
For most people, a database is nothing more than a service that stores data and handles connections. Clients interact with the database either interactively (for example,
mgconsole) or programmatically via a client adapter library (such as
pymgclient, etc). But for us, seasoned engineers, we know that there is much more under the hood. A database implementation requires a deep understanding of database internals (i.e., transactions, query execution, optimizations, etc.) and careful craftsmanship of the storage and query engines.
On top of that, a networking protocol is built, that effectively allows the database to communicate with the outside world. Specifically, for client adapter libraries, the aforementioned also pinpoints an implicit requirement: you either need to implement the networking stack and the data types all over for each supported programming language, or you can implement it once in C and call it via the bindings interface of the target language (e.g., Java JNI, node NAPI, etc.). For the latter, you could also go the extra mile and use a generator like SWIG. But in practice, there is no free lunch and SWIG comes with its own set of limitations, which I will explain later.
There are two key takeaways:
- Memgraph follows the second approach to implement client adapters without a generator like SWIG, and the clients are maintained mainly by the Core Team.
- Both approaches require the programmer to either reimplement a protocol natively, or use the esoteric bindings API offered by the targeted programming language - an explosive amount of cognitive overhead. Even more is true for the second approach, the binding APIs are not universal, that is, each of them depends heavily on the esoteric wonkery of each language.
And this is how my story begins. During the winter of 2022, I worked on the implementation of temporal types. For this, I also had to extend the networking stack of Memgraph's client libraries, which included modifying a broad range of libraries using a non-universal and non-standardized language, that is, the bindings. It was at that point I started thinking about the architecture and how I could simplify that layer of abstraction, reducing the cognitive overhead and the number of possible bugs. This is where I found out about SWIG but that, as I spoiled earlier, was not a panacea. Even though it simplified the process of building new clients, it was horrendous to debug a bug or a code generation failure/error. I figured that what is needed to solve this problem is a platform-independent intermediate representation of the C client that can be consumed by all other languages with the same semantics. Sounds familiar? If not, let me quote what WebAssembly is from their website:
WebAssembly(abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
In layman’s terms, WebAssembly is some sort of an IR that is either compiled fully or JITed based on the executed runtime implementation. Just like LLVL IR provides an architecture-independent pseudo assembly, WASM specifies an independent execution environment, meaning that any WASM runtime can consume and materialize the IR.
The novelty here is to compile
mgclient (our C client library) into a WASM module and use that to implement all of the other clients. This is lit because all that is required now is a universal runtime. Such runtime exists and it's called WASMER, which effectively cuts the ties of the binding reliance. There is also a neat bonus in using WASM as an intermediate representation - we get a very nice separation of concerns. Simply put, changes made to the networking stack are centralized inside the C implementation, and the clients become high-level API wrappers of a low-level implementation. That way, bugs introduced on the networking stack are self-contained. If there is an internal issue, it’s not with the bindings, it’s not with the API's, it’s with the implementation. We know where to look, and we can look fast. An additional benefit is that now we don’t need to understand JNI, NAPI, or any other binding library because everything is centralized around one technology: WebAssembly.
That's all folks - I hope you enjoyed the first part of the series!