InfoQ HomepagePresentationsHow WebAssembly Components Enable Safe and Portable Software Extensions
How WebAssembly Components Enable Safe and Portable Software Extensions
Summary
Alex Radovici explains the shift from C-ABI and scripting to the Wasm Component Model (WASI Preview 2). He shares how to build secure plugin systems that run at near-native speed across Rust, TypeScript, and C++. Architects will learn about Wasm Interface Types (WIT), resource management, and the practical lessons learned from deploying sandboxed extensions in safety-critical environments.
Bio
Alex Radovici, PhD. - specializes in Operating Systems and Compiler and is a core contributor to Tock OS. He has published the first book on Tock OS kernel & application development. He has 20 years of experience in software engineering with a focus on embedded systems and IoT, with products delivered to P3, Intel, Cisco, Telekom, and OMV.
About the conference
Software is changing the world. QCon London empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.
INFOQ EVENTS
Transcript
Alex Radovici: My name is Alex. I'm going to talk about WebAssembly components and how they allow us to build software extensions. I have a PhD in computer engineering, mostly operating systems and compilers. I teach at the university in Bucharest. I've been using WebAssembly for compiler classes since 2018. I've used Rust in operating systems and embedded systems since 2022. I also run a small software services company that does training and software development, and we started deploying WebAssembly and Rust since 2020. I'm also the co-founder of OxidOS Automotive. It's our try to get Rust and WebAssembly into the automotive safety critical field.
Plugins - Extensions to Existing Applications
Let's talk about plugins. Plugins are extensions to applications. If we build an application, it would be very nice if we could allow the community to build extensions and extend our application. What are the requirements for allowing the community to build extensions? First of all, it would be great if the community could use its own language, programming languages in this case, to write the extension. If I have a C app, I don't want to force users to use C. They should run really fast. They should run ideally sandboxed. Because what we're actually doing is I'm shipping an application which users trust because they know my name, but all the extensions are not controlled by myself. It would be great if the application was not a backdoor for his computer. The provider of the extension might not want to share the code.
If the extension does some interesting algorithms and we had this problem, the writer might not want to share the code. Basically, everything boils down to, we want to run third-party code in our application. In order to do this, we have a few options. One is, if we don't want to share the code, we need to compile the extension. This is the C-ABI. The C-ABI is the most standard way of library interaction. It was built in the '70s. It's fast, but not very rich. It's super error-prone. It's definitely not sandboxed. Incidentally, every shared library in your system uses more or less the C-ABI. The second option is to use a scripting language. Lua is the champion here. More recently, we have embedded JavaScript engines. They're sandboxed, but slow.
Then, if our application is written in Rust, we could see, Rust is a nice programming language. It's safe, has a safe API, but doesn't have an ABI. There's no way to write extensions in Rust. Unless the compiler sees the whole source code, it won't compile. The only way to write extensions in Rust is to use the C-ABI or a scripting language, which is awful. The question is, who runs untrusted code in a really efficient manner? The answer is the web. They have been doing this for more than 30 years. They're running JavaScript, millions of lines of JavaScript, super-fast, in your browser, which is third-party code.
WebAssembly
JavaScript was not fast enough. The web standardization organism thought about WebAssembly. WebAssembly is a standard bytecode. It's like Java or C#, just that it's an open standard. It's not controlled by the company. It has been made with safety and efficiency in mind. Think of it as assembly code for a processor that knows what a function is, what the variable is, what the stack is, and what the memory limit is. Exactly what your CPU doesn't actually know. WebAssembly comes in two flavors now. We have WebAssembly since 2016. At the beginning, we had WebAssembly or Wasm Core. This was originally called WebAssembly. It was built to optimize JavaScript. Before WebAssembly, they would translate modules into some subset of JavaScript that can be optimized by the compiler, and they figured out, this is nice, but it's not flexible enough. They transformed the limited JavaScript code into its own binary language.
The idea behind was, we have a lot of C libraries, crypto, and algorithms, and so on. How can we run them safely in the browser? The answer was WebAssembly. If you had a library with more or less no dependencies, the compilers would be able to dump WebAssembly code that the browser can execute fast. WebAssembly Core provides a C-ABI inspired. It only knows numbers, 32 bits, 64-bit numbers, integer, and floating points. These are all the data that you can exchange between the container of WebAssembly and the WebAssembly module. It's super strongly sandboxed, and it needs to be formally proofed before running.
The WebAssembly executor, the browser in this case, is able to formally prove that the code is correct. Who doesn't like running DOOM in a browser, or QEMU in a browser? If we run DOOM in a browser, can't we use this to run WebAssembly on a computer? This question popped out in 2018 when WebAssembly was working nicely in the browser. We thought, but why are we doing it in the browser? It's more or less the story of Node.js.
Now we can meet WASI, WebAssembly System Interface Preview 1. The WebAssembly consortium called the Bytecode Alliance, said, we need some kind of standard similar to the libc that executors can provide to WebAssembly modules to be able to run them outside the browser. This means, how do you represent a string? How do you allocate memory? What's the memory layout, and so on? Using WASI, we can build safe Rust extensions. They're sandboxed. They still have more or less the C-ABI, because that's what the WebAssembly Core knows, but it can't memory overflow and it can't touch my computer.
The Rust compiler has two targets for WebAssembly Core, wasm32 bare metal, no interface to the platform, or wasm32 WASI Preview 1. You can basically build a Rust application for WASI32 Preview 1, which then you can run with a normal WebAssembly runner, and it runs in a controlled manner. You can decide what it can access on your computer or what it can't access on your computer. These languages here are the languages that support WASI 1. Rust, because it's LLVM, C and C++ through Clang. Kotlin does this. Java actually does this through TeaVM. Go does this, and JavaScript through SpiderMonkey. I'm pretty sure that there are several other languages that can do this. I think Swift can do this as well.
In order to run WebAssembly on our computer, we need an executor. The executor will load the WebAssembly binary, either interpret it, instruction by instruction, either compile it ahead of time and run it on my platform. The most common executor is the browser. All modern browsers know how to run WebAssembly Core modules, most of them by doing ahead-of-time compilation. They load the module, compile it, inject it into the browser code. Node.js and Deno, which are JavaScript engines outside the browser, know this as well. Wasmtime is a Rust crate. How many of you have used Rust? A crate in Rust is like a package in Java. Wasmtime is an executor built in Rust. It can be used as a command line application or as a library that you integrate into your code. It's based on Cranelift. Cranelift is the bytecode translator. It works on x86, ARM32, and ARM64 for now.
The competition of Wasmtime is Wasmer, more or less the same. WAMR is the reference implementation for interpreters. This one works on every platform, including the small Arduino microcontroller. It does interpretation, so it's rather slow, but it works. For Rust, we will use Wasmtime.
This is a case study of how Zellij uses it. Zellij is a terminal multiplexer. If you know tmux or screen, this is something similar. It can split your screen horizontally, vertically into several terminals. It's written in Rust, and they allow third-party extensions in WebAssembly Core. What they do is they can transfer data reliably between the extension and the program, but they can use a pipe. They basically load the extension in its own thread or process and pipe it to the host program, the host program being Zellij. It just sends commands over stdin and stdout. This is how the trait looks like if you want to develop a plugin or an extension for Zellij. It looks like a really nice Rust trait. This is what they do in the back. These are the functions that the actual extension needs to export. These are more or less C functions. You can see it's bools and numbers. They use protobuffers written and read from stdin and stdout to communicate in between. It's not super-fast, but it works. It really works. Please keep in mind this was done three years ago.
The Wasm Component Model
The question is, is there another way to do this, better than sending text messages over stdin and stdout? The answer is the Wasm Component Model. This is also called WASI Preview 2. The Bytecode Alliance realized after shipping the first system interface that this is becoming increasingly a problem. How do you link modules together? How do you make libraries out of modules? How do you share strings? The simple string is a problem. Is it C encoded, Java encoded, Rust encoded? How do we send it? Basically, the question that the component model answers is, is there a way to safely share data and consistently between Wasm modules? These are the challenges. The Wasm Component Model, first of all, they define an interface definition language, which is called WIT, WebAssembly Interface Types. It defines a set of data types that can be shared between modules, and it allows developers to define interfaces for these things. It also defines a set of linking rules. Which interface is dependent on which other interface?
The goal is write once in any language, link the components together, and run it everywhere. You should be able to write a component in C, another one in C++, another one in Rust, one in Java, JavaScript, then just link the components together, have an application, and it just works. This has the potential to become the de facto way of shipping applications. WASI Preview 2, the component model, is actually usable for building safe Rust plugins. It's sandboxed, and it looks like it was inspired by Rust, and actually was. The target is called wasm32, WASI Preview 2, wasip2. The Rust compiler since 1.82 is able to compile directly to this one, with a caveat, it actually doesn't compile directly to this one. It still uses some libraries and the shim, but it's going to get better in time.
Wasm Interface Type (WIT)
Let's see how we can build an extension using WebAssembly components, the WebAssembly Interface Types. This is how we define an interface. Interface, the name, and all the contents of the interface. All the names in WebAssembly Interface Type, in WIT, are kebab case, meaning small letters, a dash, small letters, a dash, small letters, a dash. This was intentional, because Java uses camel case. Rust uses snake case for functions, but camel case for structures. C++ has no standard. C usually makes use of snake case, but it's not defined.
In order to be compatible with every possible language, kebab case is completely agnostic. If we implement the interface in Java, it's going to be camel case. The first data type that we can have is the enumeration. The enumeration is just a list of items, usually transformed into a number, and it looks something like this. My example will be a file manager. For a file manager, I have an enumeration which is called kind, and it will tell me if an item is a file, a folder, a link, or something else that I don't know about. Then we have a record. This is a structure in most languages, but without methods. It's the C structure. Record, and every field of the record.
Then we have the variant. The variant is Rust's enum, and I think this applies to Swift as well. It's an enum with a payload. You have several variants, but the variants can have tuples as payloads or structures as payloads. Then we have resources. These are opaque objects that provide functionality methods. We define a resource. The resource has a name and a list of methods. The methods that you see over there are all non-static ones. Resources can be owned or borrowed. Can you see the resemblance with Rust? When you ship a resource as a parameter, the WebAssembly Interface Type actually tells you if you're responsible for unallocating the resource or you're not. As you can see in my resource filesystem, I have two functions. They both borrow an absolute-path resource. When they return things, they actually return variants, a variant which is called result, which has a list, which has owned absolute-path resources.
The open function receives an absolute-path that is owned. Then we have the world. The world is what a component sees. We have several interfaces. Interfaces define enums, variants, records, and resources. A certain component needs a set of interfaces to run. It actually needs a set of interfaces that it imports because it uses other components, and it exports for other components a set of interfaces. In my case, it makes use of vfs-host, and it exports vfs.
Building an Extension (Using Wasm Component Model)
How do we use this to build an extension? This is what we are going to do. If you remember Norton Commander in the old times, this is the example application. I call it Junkyard, and you can see the GitHub link, https://github.com/alexandruAlex Radovici/junkyard. You can find the whole example there and the slides as well. First of all, what are the tools that we need to build these extensions? One is wit-bindgen. Wit-bindgen generates the guest interfaces for the plugin. In WebAssembly components, the guest is the component. The host is your software. You're writing the WIT file and then you have to implement the extension. Wit-bindgen will generate the boilerplate code for your language. For now, it can generate Rust, Java, TeaVM actually, C, TinyGo, and this changes a lot.
Next, we need an executor that you can embed into your application. The executor will be able to load the extension and run it. In our case, I'm going to use Wasmtime. Wasmtime, as I said, can be used as a command line application, Wasmtime component name parameters, or can be embedded into your application. Cargo-component is the tool that you will use to build the component. If you use Rust, which is below 1.82, it doesn't know how to build the component by default. You will build your extension, it will make a Wasm Core module, and then you need cargo-component to transform it into a component. If you use newer Rust, like newer than 1.82, you can compile it directly to wasm32-wasip2. Finally, I'm going to build a component, an extension in JavaScript as well, and for this we will use jco.
Just a reminder, the host is always your application, the guest is the extension. Step number one, I have the application, I want to support extensions. I need to tell my users, how can they build an extension? What are the functionalities that they need to export? First, I'm going to define a package. The package needs to be a namespace and the name of the package. In my case, junkyard-vfs:vfs-plugin. Next, I'm going to define my interfaces. What I'm going to do is I'm going to do extensions that provide filesystem information. I'm going to call the interface vfs. I will define a variant, which is called seek, an enum, which is called kind, a record, which is called stat, to give me information about the file, and the resource, which is the file. I will define another resource, which is the filesystem, which provides one single function, in this case, read directory. I'll give it a path. It needs to return a list of files within that path. I'll have an init function, which will initialize my extension and return a filesystem resource.
The interface makes use of the interface called vfs-host, which will define immediately, and from it, it will use the resource, absolute-path. The vfs-host interface, this will be the interface that the host, my application, exposes to a component. It will expose a resource, which is called absolute-path, because I'm going to ask my extension, list the files at this absolute-path. It will expose another function, which is called create-absolute-path from a string, because my resource might want to get a path from a string. Once I define the two interfaces, I will define the world for my extension. This world is called vfs-plugin. Any plugin that I will write will make use of the vfs-host interface, so the host application is responsible for providing to the component a vfs-host interface. Me as an extension, I will export vfs, which the host will make use of. This is a simple example. We have only the host and one component. Extensions in WebAssembly can use other components as well. I can link them together. As long as the interface is fit, that's ok.
This is the full example here. Once I have this, and this is completely language agnostic, I can start building my extension. I will build my extension in Rust. The first step that I need to take is to create an API crate. This is necessary, because both my native code, so the code that I have written in Rust, which is my application, needs to use the data types that the WIT interface defined. It needs to use the stat, the kind record, and the seek record. My Wasm code that will run my extension needs to use these data types as well. The problem is I have a Rust macro, which is called bindgen, which receives my WIT file, and deploys and creates the code for my Rust host.
If I use this in two places, it will duplicate the code. Every single structure that it creates, structure enum that it creates, will be a different type in different places. Every time I interact with my extension, I will get a structure, and I will need to transform it into an identical structure by copying or moving the items. To avoid this, I suggest you have a special crate, which does nothing else than uses this macro to generate the data types. Be careful to derive all the interfaces, traits in this case, that you need. Think that these data types that bindgen will create, you will use in your native software. Anything that you need to do with them, make sure they support the functionality, because you can't add other standard traits to it. You need to be careful at this.
Then, this was tricky. Unless you actually define one of your data types, for resources, Rust and bindgen will define the resource type as something that you cannot construct. It's an enum without any variants. The resource in WebAssembly interface is just a set of methods. The actual data type is not known for it. You'd better make sure you put a data type of your own and tell it, the resource, absolute-path will have to implement these methods, but this is the actual data type that I use in my host. This was not very clear for me in the documentation. I spent a week and a half adapting this and then realizing, but these guys in the example had this line. What does this line do? I figured it out. It was so cumbersome to do this. This is actually the whole crate. Then you just need to export every data type that you defined in the interface so other crates in your host application and in the WebAssembly runner can use them.
Step number three. You still need to write manually a trait for your plugin engine for the host. The WebAssembly Component Model tools are not able to generate symmetric interface. For the guest, starting from the WIT file, it will generate a really nice trait. For the host, it's absolutely horrible. The code is horrible. Not even the method names are the same. Because your host application will have to use the extension, for now, you will have to define a trait that looks really nice, and you will be responsible to make sure that this trait actually follows your WIT file. I know Christof Petig is working on a solution here.
In my case, I defined the file trait for the file resource, read, write, and seek in a file, and I defined the vfs trait, which is open, unlink, stat, read_dir, create_dir, and so on. This is the trait, if you're coming from the Java world, the interface that you will use throughout your application to interact with an extension. Every extension will be some data type that will export vfs. This is how it looks in the runner. This is the implementation of the vfs that I just defined here. This is what I need to do in the runner, use a datastore, obtain a resource, and then call the actual read_dir function. This is the name that it actually generates from your WIT, call_ the name of the function. Your function was just getting a self parameter, which is this in Java, and an AbsolutePath. This one gets a store, gets a self, and then gets a resource like this. As I said, it's horrible here. This is how the whole read_dir function looks like.
Then you need to manage your resources. Every resource that you have defined in your WIT file is more or less an object that lives within the executor. Plugins can create objects, and plugins can destroy them, but you are responsible for manually implementing the code that actually does this. For instance, if I need to get a file name from a resource, so this is the resource, AbsolutePath. One of the functions of AbsolutePath is give me the file name as a string. I will get something like this, and I need to get a string.
WebAssembly, mostly Wasmtime, has some API with a table of resources where you can register resources, get the resources, and deallocate them. You need to do this manually. Most importantly, you need to handle the drop. Every time you have a resource, the WebAssembly code generator will add a drop method for you. This is a method that the WebAssembly engine calls whenever it knows it needs to drop a resource. You need to implement this. Unless you do this, you will leak memory significantly. I completely forgot about this when I was building the example. I was leaking memory really high.
Then I want to define an API crate. I have everything set up in my host. I have a crate in my host that exports the data type. My host can use it. My plugin executor can use it. I have everything hooked up. Now it's time to write the extension. For the extension, someone that writes your extension can use the WIT file directly. It's not going to generate really nice interfaces. There's some boilerplate code that he needs to add, which you might not want to expose to developers. For your developers, you need a really nice interface. For this, usually define an API crate. This is a crate that you define and publish to crates.io. Every single developer who wants to build an extension for you needs to pull out this crate from crates.io, include it in his extension, and use the API there. It's mostly boilerplate code. You import the bindings. Wit-bindgen will generate a file, bindings.rs, with the whole interface from the WIT file. You need to import it.
Then, make sure you export the data types. Whatever bindgen has generated for you, make sure you export them. The developer, which will use this crate to build an extension, has really nice data types. As you can see, it generates something like GuestFile, GuestFilesystem, and so on. In the WIT file, these are called File and Filesystem, not GuestFile and GuestFilesystem. Usually, it's a good idea to rename them, because extension developers will look at the WIT file and then look at the API and will say, why guest? Then there's a bug. They have a macro, which is called bindgen export, which actually builds your component and hooks it up together. The macro is rather large. The problem is the macro is private. You can't export the macro in another crate. For the time being, you need to use wit-bindgen and then patch whatever it writes. Hopefully this will change in the future.
The last step is, let's write the plugin. Whoever wants to write an extension or a plugin for an app is going to pull the crate, which is called wasm-vfs. It will just import the really nice types, and then implement the interface. He needs to implement the read_dir and all the other functions that the interface provides. Next, it needs to export the plugin. This is how the plugin looks like. From the extension point of view, this is really nice. This is the plugin in Rust. This one is in TypeScript. For the same application, I can use GCO. jco will take the WIT file and generate all the TypeScript interfaces that I need to build an extension.
Then when the extension is ready, I will use GCO to bundle everything into a Wasm file. What it actually does, it takes this really efficient JavaScript engine, which is still probably interpretation, preloads it, so initializes that with everything so all the data is frozen as if it was started today, appends your JavaScript code, and bundles everything into a binary. Here I'm importing the interfaces. I'm defining a class that implements the interface. I do the read_dir function, and it's pure JavaScript. Of course, in the case of JavaScript, I don't have WASI support, so my filesystem will be a fake one, it's just a dictionary. This is the read_dir function. I'll just iterate over the dictionary and create a list of file paths. Then I'll just export the init function, the file resource, and the filesystem resource. That's it. This is my extension in TypeScript.
Step number eight. For this extension, it takes about 30 to 45 seconds to load. Whenever I ship an extension, the first time it loads the extension, it needs to build it. Wasmtime takes the Wasm binary and builds it for the platform. Depending on your system, it can vary in between 5 seconds on a really powerful computer, to 30, 45 seconds on a slower one. One lesson learned is you need to cache it. If you have a software application that uses extensions in WebAssembly, when you load the extension the first time, you can get the binary, the precompiled binary. Make sure you save it somewhere. Make sure you verify the SHA-256 is correct. Possibly digitally sign it somehow. Load that one the next time you start your application. It's going to be really fast to just load the pre-cached extension.
This opens up Pandora box a little bit, because WebAssembly is safe as long as it is in the form of WebAssembly, because that's the binary code that the execution engine can verify and formally prove that it is correct. Once it has compiled it for your platform, that code is not verifiable, and that code is not obeying all the things that the WebAssembly format requires. As soon as you load a cached binary, it's as if you load a static library into your code. If someone gets to your cache and switches that binary, you're in big trouble. This is a really important topic. If you really care about safety, make sure you compile those extensions every time. As you can see, we need to use the unsafe keyword to actually initialize a pre-compiled binary.
Summary
As a summary, these are the two pieces of software. This is my application. Within my application, which is called Junkyard, I have a plugin host trait, which is a trait that the rest of my application will use to interact with extensions. Below that trait, I have the Junkyard's wasmtime engine, which will actually load an extension and execute it. Both Junkyard, the whole application, and this runtime depend on a Wasm Component API crate, so that I can get the same data types in my native application and in my executor. Then I have the extension, which is a Wasm binary. This one will have a plugin API crate, which a developer will pull in, in Rust, in TypeScript, it's just a WIT file. TypeScript is nicer. It generates really nice TypeScript out of it. Then I have my extensions, which make use of this. This is more or less in a nutshell how this looks like.
Lessons Learned
What are the lessons learned by doing this experiment? One, tooling is changing a lot. WASI Preview 2, so the component model is now stable, because they shipped one of the standards, but it's still Preview 2. Tooling is still changing. Tooling that was available two months ago might not be exactly the same, so syntax might be a little bit different. The documentation is really outdated. Working with Wasmtime is not straightforward, so you need to know what you're doing, and the documentation is decent for now. Make sure you cache your plugins, otherwise loading 30 plugins will be really bad.
The biggest problem that we have, Wasm Components are not yet native to Rust. You still need some boilerplate code, and you still need to do some workarounds, so you can get some really nice data types. For TypeScript and JavaScript, this is not true. The data types are really nice. The TypeScript extension was written in 10 minutes. Documentation lacks, but other than that, it's fine. Not even the Rust target actually generates nice data types. Tools are not symmetric. They don't generate symmetric traits. I need a tool for the guest, and I need a tool for the host. I can't use the same tool for generating the code. Resource management is manual, so if you have resources, make sure you actually care about the drop statement. You need to do memory management yourself for the resource. You can either implement a garbage collecting system, or you can drop them immediately. That's up to you. You need to know when a resource has to be dropped. This was my experience in building a demo component.
Questions and Answers
Participant 1: This is embedding lots of different WebAssembly bits of code into your process. This is isolated from the rest of the process because of WebAssembly. Are there any tricks for stopping the WebAssembly code from running indefinitely, in the extension, if it goes crazy or something like that?
Alex Radovici: The question was if there are any tricks to stop the WebAssembly code. It depends on the executor. Usually, this WebAssembly extension will be a separate thread. WebAssembly executors have several ways to stop it, either by doing gas counting. If it's ahead-of-time compilation, just inserting some instructions that will stop it from time to time. This is something that you would need to handle. If it's a separate thread, you can figure out it just works continuously and stop it. It's not automatic, though. You need to do some steps in the executor. It depends from executor to executor. Wasmtime has an API. Wasmer has another one.
Feb 20, 2026
Related Sponsors
Related Sponsor
%2ffilters%3ano_upscale()%2fsponsorship%2ftopic%2f5aab5793-1aa2-43a6-9086-318627c6365a%2fPayaraLogo-1763716038782.png&f=jpg&w=240)
Move from complexity to control. Run and scale your Jakarta EE, Spring, and Quarkus applications on a unified platform that replaces infrastructure chaos with deployment simplicity and total autonomy.Learn More.
This content is in theWeb Development topic
Related Topics:
Related Editorial
Popular across InfoQ
Agoda’s API Agent Converts Any API to MCP with Zero Code and Deployments
OpenAI Publishes Codex App Server Architecture for Unifying AI Agent Surfaces
Architecting Agentic MLOps: A Layered Protocol Strategy with A2A and MCP
LocalStack for AWS Drops Community Edition Raising Developer Concerns
Nuxt Studio Released: Open Source CMS for Content Editing in Production
[Video Podcast] Building Resilient Event-Driven Microservices in Financial Systems with Muzeeb Mohammad

