Video Summary and Transcription
JavaScript code is converted to low-level binaries by JavaScript engines like MV8, Chakra, and SpiderMonkey. WebAssembly allows writing code in other languages and compiling it to run on a JavaScript engine. External functions can be imported in WebAssembly using the import statement. WebAssembly can run in non-browser scenarios with additional interfaces like WASI and provides memory isolation. The Jco toolchain is an experimental tool for componentizing JavaScript code into WASM modules.
1. Introduction to JavaScript and WebAssembly
I'm a principal lead for JavaScript developer experience on Azure, covering integrations, hosting services, and compute. JavaScript code goes through a flow of execution on the browser, from parsing to code generation. JavaScript engines like MV8, Chakra, and SpiderMonkey convert the code to low-level binaries. WebAssembly is a binary instructions format that allows us to write code in other languages and compile it to run on a JavaScript engine. It works based on imports and exports.
Hello, everyone. My name is Natalia Bendito and I'm a principal lead for JavaScript developer experience end-to-end across the many services and tools that can be used by JavaScript developers to build applications on Azure. That ranges from integrations like GitHub Actions to VS Code extensions, hosting services like Azure Static Web Apps, or compute like Azure Container Apps or Azure Functions.
When we build JavaScript applications, we know that our code or scripts are basically going to go through this flow of execution on the browser. Each instruction goes through this just-in-time journey. The parser or JavaScript engine generates a hierarchical structure called the abstract syntax tree or AST. For example, a variable declaration is a CAPS, variable CAPS declaration, node construct in AST, which the interpreter reads and generates high-level binary code or byte code from. Finally, the compiler generates low-level binary code that speaks to the CPU in a way that can be understood according to the processor or CPU architecture. All of this again happens just-in-time. This flow is mostly the work of the JavaScript engine. MV8 is one of the most popular, but there is also Chakra, there's SpiderMonkey, there are others. The generated machine code may be optimized for a specific set of instructions that are, again, optimal for a specific architecture, as we mentioned before. This means we are converting our high-level JavaScript code to low-level binaries. JavaScript engines can take care of the initialization of the environment or do take care of the initialization of the environment. They add built-in functions and they also handle or run our application code. The engine also provides the runtime, where the application is going to be running on. JavaScript scripts run on a JavaScript engine, obviously, but what does also run on a JavaScript engine, if you can think about it, at that low level, by the way? That's, of course, WebAssembly.
What is WebAssembly more concretely, or Wasm, as some may refer to? It's not really a language. It's a binary instructions format. Compilation target means that we write our code in the language we prefer as long as it has a Wasm tool chain or compiler that allows us to do so and we compile it to binary as Wasm to run at that low level that we were describing before. How does WebAssembly actually work? Let's imagine we have a program written in C, C++, RAS, or any other language that is supported, and we want to compile it to Wasm. We use its corresponding tool chain or a set of developer tools, and that's what I meant before by is supported. It has this set of developer tools and compilers, which will emit that code as a module, which imports and with imports, sorry, and exports. Then we can import it into our JavaScript to be run with a JavaScript engine using WebAssembly JavaScript bindings or the JavaScript API. Wasm does not really define any APIs. It works like we mentioned on the basis of imports and exports. This is an example of a module written in WebAssembly text. Let's walk through this cycle. If I was going to describe each one of the lines and things that are in this code, it would take a very long time, and it would be a little bit exhausting. But we can define the sections.
2. Using External Functions in WebAssembly
The import statement is used to import an external function, such as println from the environment module. The function section exports the function hello world. The memory section declares the size of the memory, and the data section initializes the memory with a string. When importing the module in JavaScript, we fetch the module, process the response, compile the binary data, and create an instance of the module. We can redirect the output of the print line function to console.log. WebAssembly allows us to run CPU-intensive operations at a lower level and compose user interfaces. Page find is an example of using WebAssembly for document ingestion, indexing, and full-text search on the client side. We can achieve this in a static website using WebAssembly.
I think that it's important to start with the import statement that is used to import an external function because we don't have anything in the Wasm native APIs. We are importing this external function named println from a module called environment. This function takes a single parameter of type I32 or 32 bit integer. The important function is given the name dollar println.
Now in the next section, we have the function section. The function is exported with the name hello underscore world. Then we have another section, the memory section, which basically declares the size of the memory. In this case, one page. Finally, we have the data section that initializes the memory, in this case at offset zero with a utf-8 string, hello world of JavaScript.
Now what I think is really interesting is what happens in the JavaScript that is importing this module or fetching this module. We're fetching the module we exported to a file named hello underscore world.wasm. Then the response is processed using response.arrive buffer to obtain the module's binary data. We then use the WebAssembly.compile function to compile the binary data we got from the previous step into a WebAssembly module. We use the WebAssembly.instance constructor to create an instance of the compiled WebAssembly module. As you can see, there is an environment object provided in the second parameter of the constructor, which includes an important function named print line. Do you remember the previous what file? This important function is mapped to console log. The purpose of this mapping is to redirect any output of the WebAssembly module's print line function, as defined in the WebAssembly text code that we provided before, to the JavaScript console.log function. And finally, the exported function hello underscore world is invoked from the WebAssembly module using instance.exports.hello world or hello underscore world.
Since we have the ability to fetch and run Wasm, as we just saw, as part of our JavaScript programs, we can compose user interfaces where we run CPU intensive operations at a much lower level or lower level than our typical JavaScript programs run in a more performant way. Let's explore a specific use case of this type of composition. Let's propose we have a static website, like one site created with any meta framework today, like Astro11t and so on, using static data fetching. And we want to run a search functionality for that static content. Well, we can do it with JavaScript client side, but we can also use WebAssembly. So let's use it. And page find is a great example of a Wasm feature that does, in this case, document ingestion, indexing, and runs full text search, filtering, sorting, et cetera, all on the client side at a very low level in WebAssembly. Let me show you now exactly how page find works. We're on the documentation site, but we will go to Starlight, a framework to build documentation sites with Astro. If we pay attention to the top left, we see we have a search bar. Although this is a static generated site, how does this work? Let's run a search for something like div. We notice we get immediate results.
3. WebAssembly in Non-Browser Scenarios
The search functionality in the static website is implemented using JavaScript and WebAssembly seamlessly. WebAssembly can run in non-browser scenarios with additional interfaces like WASI. WebAssembly with enabled WASI runs in a special runtime that can even run on bare metal. WebAssembly is secure and sandboxed. A demo of JavaScript code that runs in the browser and on WASM time is shown using the Javi tool chain and WASM time runtime.
It took a few milliseconds for the results to be shown. Now, let's look for something else. Let's look for bottom, for example. And we get even more results at the same exact speed.
Now, clicking on the results, we're immediately redirected to the corresponding page the result citation comes from. Let's now inspect what's happening under the hood in this example. Is this a JavaScript client side functionality bootstrapped to the static pages running against the database somewhere?
If we go to the sources right now with the inspector, let me bring it and focus the search bar, we will immediately see something happening here. Here. We see this wasm source and the contents of the module. We have demonstrated that we can seamlessly run and operate between JavaScript programs and WebAssembly modules. There are platform APIs that make it easier to transfer memory, to trigger events or update state between code running in the window object and that running on a worker.
And you may say, okay, that's very nice, Natalia, but that's not JavaScript. I know it's Rust. And you're right. So what if we want to use JavaScript end to end, including in non-browser scenarios where just in time flows may not be the best option or may be unsupported. Do we have the tools to do so? Can we make JavaScript portable from the browser to non-browser environments and back and forth for real? Can we do this?
Let's start by answering this question. If WebAssembly needs a JavaScript engine and those are usually part of a browser, can we run WebAssembly in non-browser scenarios? We can, as long as we provide additional interfaces. For example, those of WASI or the WebAssembly system interface. WebAssembly system interface is just another standard. It's a modular system interface built for WebAssembly outside of the browser. And it's fully compatible with POSIX systems. Just as WebAssembly uses the JavaScript virtual machine to run in the browser, WebAssembly with enabled WASI runs in a special runtime that can even run on bare metal.
Remember that WebAssembly has no access to any APIs and that we don't explicitly import. That is, they are entirely secure environments from that point of view. They are sandboxed. We'll talk more about these security features in a bit. Let's now see a demo of JavaScript code that was written once and runs in the browser and on WASM time using a tool chain called Javi. Welcome to my binary translator. Now, for this demo, I need some dependencies. I need Javi, a JavaScript WebAssembly tool chain, and since I'm not going to be running this only on the browser, I will need a standalone runtime for WASM, in this case, WASM time.
4. JavaScript to WASM Portability
The JavaScript code converts characters to ASCII numbers and binary. The code uses the Javi tool chain for read and write operations. It can be compiled to a WASM module and run with WASM time. The same code runs on the browser and outside the browser with minimal changes, demonstrating portability, reduced maintenance, and improved performance.
Now, going to the code, we can verify that this is JavaScript. We have a few functions in the bittranslate.js file. The script basically takes a word and converts each character to its ASCII number and then to binary. So, what we get after reading it, it's representation on binary format. So, like we mentioned before, it's just plain JavaScript except for a few lines where we're using some, this library, this tool chain, Javi, for some read and write operations. Whenever it's running on the browser, we will use standard JavaScript APIs, and when we're not on the browser, we will perform exactly the same operations with Javi.
Let's now compile our code to a WASM module using this tool chain. We will run Javi compile, passing the input file and the name of the output file, and when we have our code as a WASM module, we can go back to the terminal and use WASM time to run it. For that, we will pass it a string, in this case, hello world devops.js, and boom, we get the binary representation for it. Now we have demonstrated this JavaScript code runs outside of the browser as WASM, but does it still run on the browser as JavaScript? We start a simple feed server and pass exactly the same string to the HTML form that is going to then pass it to the script, and we get exactly the same result. It's running the same code in two different environments with minimum refactoring or minimum additional code, and we can see right off the bat the benefits for portability in terms of reduced maintenance and performance.
5. WebAssembly Security and Component Model
WebAssembly is secure and provides memory isolation. WASI-enabled WASM has additional layers for debugging and unstable API definitions. Not all interfaces are available for every language. WASI Preview 2 is based on the component model, providing opportunities for portable applications. Jco is a toolchain for JavaScript to WASI-enabled WASM, implementing the component model.
Before we try a different tool to demonstrate portability and composition, let me highlight some security aspects. Because WebAssembly is executed in a completely encapsulated sandbox, it is memory safe and much more secure than any other execution context within the JavaScript virtual machine. When composed within a page in the browser, WebAssembly will implement the same security and content policies as the origin that runs it. And that controlled environment provided by the host makes sure that WebAssembly code cannot access or modify memory outside its designated boundaries, resulting in memory isolation and limited direct interaction with other processes and components in the host.
Interactions are typically limited through well-defined interfaces, reducing the risk of malicious activities, reducing the risk of supply chain attacks, and reducing the vector of attack or the vector surface. Everything seems amazing, but everything has a trade-off in software engineering and development. If anybody tells you that's not the case, be suspicious. What is the catch with WASI-enabled WASM? There are more layers to debug to understand the source of an issue when you have one. These new technologies are in preview and hence unstable and prone to change API definitions. And, of course, not all interfaces are available for every language since every language has its own toolchain and they progress at different times in integrating new standards. So the WASI Preview 2 was recently released. We are now mid-February. That happened at the end of January 2024. And it was fully rebased on the component model instead of modules. A broad-reaching architecture to build interoperable applications and environments with WebAssembly.
How does the component model give us more opportunities to run portable applications? Well, there are great advantages for composability in the cloud. And remember, we were talking about composing JavaScript in WebAssembly. And this expands our opportunities with isomorphic JavaScript compiled to WASI-enabled WASM that can run in browser and in workers. This is already available or possible with some providers. And let me show you another toolchain, Jco. This is a toolchain for JavaScript to WASI-enabled WASM implementing the component model. And I'm going to show it in the next example demo. Now, in this last demo, we will use the Jco toolchain, as mentioned, a JavaScript to WASM toolchain specifically built for the component model. We're also going to run WASM with WASM time. But in this case, we will compile it with cargo for that purpose. We will compile WASM with cargo. And first of all, I have all my dependencies in place. I have the Jco toolchain. I have the Preview 2 shim. I will also be using componentize.js.
6. Using the Jco Toolchain for Componentizing
The Jco toolchain is experimental and not stable. Writing with files as interface contracts is not straightforward. Jco may evolve to offer a more intuitive way for JavaScript developers. The source file is the function hello. After componentizing to WASM, we can run it with node or use cargo and REST to build a release. The Jco toolchain can transpile the WASM component contents for running with node. Join microfrontend.dev for more about frontend, web standards, and composability. Thank you for tuning in!
The catch with this library is that it is experimental and not stable. And the other catch is with the toolchain, because you will need to write with files as interface contracts, which is not a very straightforward task in terms of developer experience. I am very sure Jco will evolve to offer a more intuitive way for JavaScript developers to output WASM component binaries from a JavaScript source. But right now, my source file is the function hello, which is so. And it has its corresponding with world.
Now, those two files are used to componentize to WASM. In this case, the output will be a component called hello.component.wasm. And after outputting it, we will be able to run it with node. Like I mentioned, we can run it with node, but we can also use cargo and REST to build a release and run the same code with WASM time. And after we get that component, or that WASM as component, we can use the Jco toolchain to transpile the contents of that WASM component so we can run it with node. This will create a folder with the components necessary to run this code on a Node.js worker.
Now, I am, in the demo, running it with the Node.js REPL, but this exact same output could be run in the cloud on a worker providing it supports the preview toolchain. Now, thank you very much. And join me at microfrontend.dev to know more about frontend, web standards, and composability. Thank you for being tuned. Bye.
Comments