 Hey, everyone. So my name is Saul. And I'm here to talk about Polyglot Web Assembly or exploring composition through a component model. So Polyglot is a very overloaded word, like runtimes and other words that we use in computer systems. So I'm going to define what I mean by Polyglot later on so we can have a common base to work on. But first, I'm going to introduce more of myself. So I work at Shopify, mainly on the Shopify VM and Watson Foundations team. So at Shopify, we are building a composable and scalable Web Assembly platform for customizations. That's basically user-defined functions. And on the Watson Foundation side, we work mostly on open source. And we've been lately doing some work helping the Biker Alliance folks move forward some of the tooling around the component model because that's crucial to Shopify's use case. Also, we have done some experiments with bringing dynamic languages to Web Assembly. And the main example of that is Javi, which is our JavaScript Web Assembly tool chain. And the idea is very basic. We compile something like QuickJS into Wasm3 to Wasi, and then we execute your JavaScript code. So we've also been experimenting with other runtimes like SpiderMonkey, which is Mozilla's JavaScript engine. And even though this is heavier and a bit more complex, we think that there is an opportunity here to have faster JavaScript on top of Web Assembly. So we don't have specific results yet, but we've been working on something really exciting and probably going to do a different talk once we have more information here. So now, on to the definition of polyglot. This is not a definition that's mine. It's more like a wrangling of things on the internet until something made sense. But what I mean by this is multiple programs written in different program languages communicating seamlessly between each other. This picture might be more of what I'm looking for. So you have something written in Rust, and you can easily communicate that program with something written on JavaScript on Web Assembly, and then the same thing with another Rust program. So even though this idea is not new, I think it's a very powerful one. And other runtimes have explored to this idea. The main example that I have for this is Growl VM. So Growl is Oracle's VM, which is built on top of the JVM, and uses a framework called Truffle to optimize your programs. And the idea is that you can have multiple implementations of your programs, like, for example, Ruby has Truffle, has Truffle Ruby, and then you can run all those programs on top of the Growl VM, and then they can communicate between each other. So their polyglot characteristic is defined as leverage the best features and libraries of popular programming languages in single apps with no overhead. So that's for the definition of polyglot. Now, in order to understand why this talk even exists, it's important to give some context on how we use WebAssembly at Shopify. And the best definition for that is this slide. So we use WebAssembly for synchronous execution from trusted code in performance-sensitive contexts. So there are a couple of things that we can extract from this definition. The first one is synchronous execution. We don't allow any side effects in our WebAssembly execution. So that means no HTTP, no async, nothing. That's just pure execution. And the second important concept here is in performance-sensitive context. So you can think of this in a context where delays are not allowed, or they should be minimized. So a performance-sensitive context, or a good example of that, is a checkout process. If you want to run some customization of some piece of code that a user submitted to your platform, you want to make sure that it's fast and reliable. Because if not, that can have some consequences on that conversion for a specific merchant. Now, I don't want to make this talk Shopify-specific, so I've extracted two main characteristics that I think are important for this talk to make sense. And the first one is that we are talking about platforms that have tight execution limits. You can think of five milliseconds at max. If a user submits a function to your platform that takes longer than this, it's going to get interrupted. So basically, your platform won't be functional for what the user is trying to do. And then the second important characteristic here is we want the smallest binary size possible. So we've been working with sizes of around 256 kilobytes per binary. If someone submits something bigger than this, we won't be able to accept that as a valid program. So some of you might ask, why is this size constraint so important? And the answer to that is that we want to have highly available modules. And the bigger they are, the harder it is to store them, to cache them, to transfer them over the network. And so the smaller they are, the better. With this idea in mind, it seems like the traditional approach of statically linking a single program and running that in your runtime might not be as scalable as we might expect. So it seems like what we want instead is submitting a single binary that requests some functionality. And that functionality is provided to you at runtime. Basically, dynamic linking that functionality. So you can think of this as having a set of modules that act as standard libraries. At the WebAssembly layer, and then any WebAssembly program that requests this functionality is going to have access to it. So you can think of this, the perfect mental model for this would be when you request something from the operating system. You expect it to be there, because the operating system can be seen as a platform for running programs too. So here is where concepts from the component model start making sense to us for our use case. If you have questions about the component model, don't ask me. Ask Luke. So yeah, he might be able to answer better. But I want to highlight three main pillars of the component model that are crucial to enable this composition of programs. And they are module linking the canonical ABI and interface types. So I'm going to start with module linking. And the idea here is that you can link modules at runtime without having to write blue code on your host. So your API, your runtime needs to provide a native API to allow you to do this. And then we have interface types, which are types that describe high-level values. So as some of you might know, WebAssembly right now only supports low-level values that are basically ints float. And so with this proposal, we can have access to high-level values like a string. And then we have the canonical ABI, which is pretty important to what we're doing, because it describes the relationship between a high-level value and a core WebAssembly value. So how do I go from an N32 to a string or the other way around? So that's important. Now, I don't want to get too theoretical here, but I want to do touching to a practical use case. So let's assume that the user defined functions that you're accepting in your platform need access to a date format in library. Your users need, for some reason, to format some dates. So I've created this repository, which you can visit where I have all the code for this presentation. And first, I want to visit the static linking approach. So how you would do this if you wanted to create a single program that is going to run and is going to format some dates. So the main thing that you would do, for example, is have a project that looks like this. You will have an index.js, and then you would have a package.json that will look like this. And potentially, you would import a date formatting library from the NPM ecosystem. So this date formatting library is going to take two strings and give you a formal string in human form, depending on the lengths of those dates. And in your build process, you're going to use a tool like Javi, for example, pass it in your JavaScript, and then get a final web assembly module. And your code probably is going to look something like this. When you run NPM run build, you're going to invoke webpack or whatever build you use and execute Javi to get your final web assembly. Now, I have an entry in my make file in that project that executes that. And I want to execute that here. So just one second, have my terminal speak enough. So if I execute make a standalone, that's going to invoke wasm time, and then we get about one year, which is what we were expecting. Now, that's the easy use case. Now, let's go to the dynamic linking approach. The first thing that we want to do since the date format in functionality is what's common for all the user defined functions is factor out the common functionality. And that basically means that we are going to have some JavaScript that is going to request that date functionality. And we want to link it against a performance date library, which is going to be written in Rust. So we create a crate that is going to look like this. And then we have our code to do that. The important part here is the top-level part. So we are using, here I'm using a tool called Widb Engine, which is a binding generator for interface types. This basically takes in an interface definition and creates the code that you need to go from high-level types to low-level types, and then from low-level types to high-level types. So that process is called lifting and lowering. And that file looks like this. And then the Widb Engine tool for Rust expects us to fulfill this contract by implementing a trait. And the trait implementation basically looks like this. You get two dates, and you pull in Chrono and Chrono Humanize, which are Rust's libraries, which will be the equivalent for JavaScript dependency, and then you execute that on top. When we compile this to Wasm3 to Wasi, we get the following export. So we export a function called Format that is going to be in low-level types. It's going to receive four params, which represent two strings, and then gives us another pointer. So up until now, we have extracted the common functionality that we want to link at runtime. Now we need to implement the consumer module. For us to implement the consumer module in JavaScript, we need to create bindings for our JavaScript runtime. In this case, I'm using QJS. So the idea is that I'm going to let the JavaScript runtime QJS know that at some point in the future, this functionality is going to be present. And the usage is going to be like this. I'm going to have, in the global, access to an object called date, and to a key that is going to be called Format that is going to represent a function. And then in the last line, I'm going to call Format Difference, and I'm going to pass two strings. Keep in mind that these two strings are high-level JavaScript values that we are going to need later on. So that's the important part. And the way this is done in QJS or in Java's code base is just creating a callback that is going to be a function that is going to look like this in terms of Rust. But the important part here is how this is implemented. So the important part here is the external block that we have at the top that it's instructing that we are expecting this functionality to be available somewhere in the future. This functionality, though, is represented in low-level WebAssembly types. So in this callback, we get access to two JavaScript strings that we want to lower into pointers to pass in to the function that is defined in that extern C block. Once we convert those two values, those two JavaScript strings into pointers in the process that's called lowering, we can call that function. One thing to notice, though, is that we have five params here. Because in the last param, it's something that we call the return pointer area in which we're going to encode what that function is returning, basically the length up until where we can read memory and the pointer. So I think this has been a standardizing canonical ABI. And then the last part that we do here is basically from that return area pointer, we read that specific memory, and then we create a JavaScript string. So this is the process that's called lifting. So we return this string to the user, and then we can have access to that functionality. When we compile this WebAssembly, we can see that we have an import that's called date, has a key format, and has a function of the type that we defined earlier with the five parameters. Now we have the consumer module, and we have the producer module, or the API. And one export looks like this, and the other import looks like the following. We can see that these signatures don't necessarily match, and here is where we need to introduce something else, like adapting the functions between the two modules to be for them to communicate effectively. And here is where we started using a tool called Wassamlink. And Radu, who was here, has written a blog post. So if you don't know how this works, you can read the blog there, and it might give you more insight. So the idea behind Wassamlink is that you give Wassamlink a consumer module, like our JavaScript module, a set of interface types, and then another implementation of the date of the module that it's exporting the functionality that you want. And then it creates a statically linked module that contains all that functionality, and both modules communicating together. I had to modify Wassamlink because if I kept it the way it was, it was counterintuitive to the size benefits that we can get by dynamic linking at runtime. So in my modification, you give Wassamlink a consumer module and just the interface type definitions of the modules that you want to consume. This gives us a final module that is thinner, and it's smaller, and it doesn't require the code of all the other modules that this module is trying to communicate with. So the way I invoke this is use Wassamlink. I give it the consumer module, and then I get the result is a module that looks like this. So at the top, we have a type of an instance, which is a concept introduced by the module linking proposal. And then the important part is the last line, in which we import a date as an instance, as it was defined above. And then that instance is required to export the format functionality and some other functions to be able to allocate and free memory from this module. So yeah, this is what I was explaining before. And we have the two pieces now. We have the consumer module and the producer one. And we have adapted the modules that calls between those two modules. How do we do this in the host? Well, here I have a wrapper around Wassamtime, which I created sort of like a STD API in which we load the date functionality that we want. We instantiate that standard library piece, and then we create a guest module from the one that was given to us from the command line. And then we create an instance and grab the start function, and we execute that. So the important part here is the instantiate STD part, which basically looks like this. So Wassamtime, up until version dot 35.3, provided a way to instantiate a module and then registering that instance as an instance that would be resolved if someone else asked for it. So in our case, if our JavaScript module asks for an instance that we registered, Wassamtime will be able to resolve that. So I have, again, in my make file, a shared implementation, which is going to run our host crate and pass in the dynamic loading web assembly module. So let's take a look at that. Put around that. It's going to take some time to compile. And then we get the same functionality that we had with JavaScript. All right, so let's go to some statistics. In the static version, we have a binary size of 2.2 megabytes. Let's keep in mind that we are embedding a JavaScript engine. And we have a performance of around 323 microseconds. Now in the dynamic approach, we have a size of 900 kilobytes and a performance of 160 microseconds. So this means that, in general, we have saved 50% in both size and performance by going through that dynamic loading approach. So just to do a recap, we have gotten gains in performance and in code size. And we have also enabled composability at the runtime layer. And the other advantage of this approach is that everything is Wassam-based. If you have a platform in which you have developers submitting third-party developers submitting user-defined functions and then first-party developers enabling those APIs, you certainly want to have that functionality sandboxed. Because you could do some of this in the host, too, but you're getting out of the sandbox at that point. Now let's see some of the cons that we have seen so far. So the first one is debugability. When you have a composition and when you are dealing with multiple binaries compiled by different tool chains, the debugability story is not fully sorted out. So that has been a challenge for us. And then the other piece is memory consumption. So the approach that I've shown here is something that's called a shared-nothing approach in which each module keeps access to its own memory. And so if you have multiple of this, you would have this, like in this graph, for example, here in this diagram, we have eight modules. You would have at least eight linear memories. So that means that your memory consumption can go up depending on how you architect this. Yeah, so that's everything that I have. I would say that this is highly experimental. All the tools are changing, but this has been something that has proven to be very useful for our use case in which we want to keep performance under control and also module size under control. So this is my GitHub interior handle in case you want to reach out or take a look at any of the code that I've shown here. Thanks.