 Hi, I'm Lynn Clark, and I make code cartoons. I also work at Fastly, which is doing a ton of cool things with WebAssembly to make better edge compute possible. And I also am a co-founder of the Byte Code Alliance, which is building a vision of a future WebAssembly ecosystem that extends beyond the browser. Before I start, I want to say thank you to the organizers for inviting me to speak here today. I was really excited when I was asked to give this talk. In the WebAssembly world, we've been looking at many of the same problems that all of you in the cloud native community have been looking at, too. For example, one problem we've been thinking through is how do you deal with code that depends on the concept of a file system in places where you don't actually have a file system available? Or another problem, how do you create a clear separation between the business logic of an app and the orchestration of that app? So we have a lot of goals in common, but our communities have been coming at these problems from different perspectives and bringing different contexts to the conversation. So I think that there are lots of conversations that we can have between our communities combining these perspectives and seeing what opportunities they open up for each other. Before I dive in more deeply into how we've been thinking about these things, I want to explain what made us start thinking about them in the first place. So why was WebAssembly created? The browsers wanted developers to be able to compile code bases that were written in languages like C++ and Rust to a single file and then have that code run at near native speeds in the browser. And they wanted it to run in a very secure way in a well-isolated sandbox, because you need that when you're running code that you just downloaded from the internet that you don't trust. To get that near native speed, the byte code for this WebAssembly binary needed to be as close as possible to the native instruction set architecture, or ISAs like x86 or arm64. But without specializing to any particular ISA. So this meant creating a really low-level abstraction over the various ISAs. This made it easy to run the same binary across a bunch of different machines with different machine architectures. And this got developers really excited, even those that were working outside, completely outside of the browser. But as these developers started bringing WebAssembly to the server and to other places, they left some of the key properties of WebAssembly behind. They were giving these WebAssembly binaries full access to the operating system, system call library. That compromised security, and it also compromised the portability, since now the binary was tied to a particular operating system. So with this, we realized we didn't just need an abstract ISA, we also needed an abstract operating system, one that made it possible to run the same binary across a bunch of different operating systems while preserving the effectiveness of the WebAssembly sandbox. So we started work on WASI, the WebAssembly System Interface. The goal of WASI is to create a very modular set of system interfaces. These include all of the low-level kinds of interfaces that you'd expect from a system interface layer. But it also includes some higher-level ones, like neural networks and crypto. And we expect many more of these higher-level APIs to be added over time. These interfaces need to follow capability-based security principles to ensure that we maintain the integrity of the sandbox. And for the most part, these interfaces also need to be portable across the major operating systems, although we are OK with system-specific interface modules for certainly narrowly-scoped use cases. It was when we started trying to make this portability work that we started getting to some of the problems that I mentioned before. These problems started coming to light when we were thinking about a pretty core concept in many operating systems, the file system. A lot of code to date depends on the file system. And the code uses the file system for many different tasks. So it's where you persist data. It's where you share data between two programs that are running in different processes. It's where you put the code for executables. It's where configuration lives. It's where assets get stored. Files are like these Swiss Army knives that are used for all of these different tasks. But as we were thinking about all of the places where we wanted to run wazzy, we started wondering whether or not this was the right abstraction to use. The file achieved its central position and system interfaces during a very different time in software development. There were a few operating systems that really entrenched the file in this privileged position. And these operating systems were first being developed in the 1970s and 80s. This is when you had the rise of the mini computer, and after that, the personal computer, mostly to help with office work, which, of course, was organized in real paper files. For these kinds of systems, having a file system and having direct access to that file system made a whole lot of sense. If you look at the systems that we're building now, though, things look a bit different. We're still building applications for the personal computer. But with things like browsers, we started running applications inside of other applications, places where you probably don't want the interapplication to have direct access to the file system. And then, as we started moving applications to the cloud and edge networks, and as IoT devices started proliferating, we suddenly had an entirely different landscape where direct access to a real file system was the exception, not the norm. And on top of all of that, as we've moved towards having more modular ecosystems of open source code that you plug together, like MPM or PyPy, these file systems are presenting maintainability and security problems. Because the way that these file systems are used is basically like having one big giant pile of global shared mutable state. Given all of this, files don't really feel like the right kind of universal abstraction anymore. If we're going to try and break out of this file-centric paradigm, we need to think about what a file actually is and what it does. So what exactly is a file? Well, a file consists of two things. Some bytes that encode content, and you can think of this as an array or stream. This is the data. And other bytes that contain metadata about that data. So this includes things like the name of the file, timestamps, permissions, and what underlying device the file is stored on. It's the second part here where we start to have problems. When you're working with this metadata, that's when you need to know about the conventions of the host system that you're running on. But when you think about what most programs are actually doing, what they actually care about, most of them only care about the data. They just want to get that array or stream of bytes and start working on that. They don't care about where the data lives. Now, there are, of course, some applications that do need to know the details of this metadata as well. So for example, if you're building a backup application, you, of course, do need to know the file names of those files. But most of the time, the metadata is unnecessary for what a program is trying to do. My colleague and the architect of much of WASI, Dan Goman, has called this distinction the difference between compute and meta compute. And he had the thought that what if we were to push as much of the meta compute out to the edges of the system as possible, either up to an orchestrating module or even better out to the host itself. To see exactly what this means, let's walk through an example. Let's say that you're writing a utility that shrinks an image down to a particular size. And you want to run this utility from a command line. How would this work in the file system-centric paradigm? We have the host system that's the outside the gray. And the WASI module is running inside of the host, inside of this white box. So the WASI module would be passed in an array of arguments that are all strings. And would take the string that's at a particular index and use that as a file name. And then that WASI module would use that string with the open syscall to get a handle to the file. The operating system would hand it back a handle. And then the WASI module would read the bytes from that file using the handle. With this, we're requiring the module to think about the file system. We're requiring it to think about the context that it's running in. But this module wouldn't really need to know about these details. All it really needs is the stream of bytes to come in so that it can operate on that stream. So now let's try moving this meta compute out of the module and over to the host. Now by convention, a program's main function takes in a very generic set of parameters. So for example, in C, it takes the arg count and then a pointer to the array of strings that are the args. But let's say that we introduced a convention and tooling support for more application-specific parameters. For example, let's say that the main function for this application accepts a stream and returns a result that contains either a stream or an error. When you run this on the command line, the host will be able to look at that string and also see that the type that was asked for by the program is a stream. And the host would know that it can convert a file to a stream. So instead of just passing in a string, the host would instead open the file itself and get the handle, which the host would then use to pass in the stream of bytes to the WASM module. And with this, we've moved all of that meta compute over to the host. So this module no longer has any concept baked into it of whether or not there's a file system. This makes the code more portable. And this architecture also makes things more secure because this way, we didn't actually need to give the open sys call to the WASM module either. And so if this WASM module were to get exploited or were subject to a supply chain attack, it wouldn't have access to open files willy-nilly on your machine. Of course, none of this matters if developers don't use it. We need to have a gradual adoption path. We need a way for everyone in the community to transition to this new paradigm at their own pace so that the whole community doesn't need to move and lock step. We have three different options for how to compile a module to use WASM. And these three options represent that gradual adoption path. So let's say that you already have some legacy code that you wanna compile. And this code makes extensive use of the not so good parts of traditional file system APIs, the ones that bake in expectations about the host environment. In that case, you would signal to the compiler that you wanna use the legacy file system interface. Now, this might be through a flag or through a target triple. This would link your code against a version of libc or whatever your languages standard library is that's implemented in terms of the WASI file system interface. And this is in many ways the same API as the file system API exposed by POSIX and other system interfaces. So your code can act like it has direct access to a file system, which it might in some cases or the host might provide a virtualized file system. Either way, this looks pretty much like the run of the mill file system APIs. So your code would not have to actually make any changes. You wouldn't have to make any changes to your code rather. Now this code would not work on hosts that didn't either provide direct access to the file system or that virtualized file system that I mentioned before. So it wouldn't provide the full portability but it would be an easy on ramp to moving code to use WebAssembly. But what if you want the portability and you also want the isolation between different modules that WebAssembly can give you where you aren't sharing that global file system between modules. For that case, we're providing a compatibility layer. The developer would still write their code using the languages normal file APIs but in this case, what we're currently thinking is that the host wouldn't be providing the file system itself. Instead the module would be virtualizing its own file system. And these quote unquote files would be in the linear memory of the WASM module. This means that we don't have the global shared mutable state problem that the file system introduces. Even though these look like files in the source code, under the hood, they would use a different set of types WASIO types. And this would give them that full portability. However, this virtualization would introduce some inefficiencies, including larger file sizes for the WASM module. So in the case where you want full portability and you want that efficiency all at the same time, you would use a different API in your source code. You would use the WASIO API directly. So that means that you would change your code. Instead of passing file handles around, you'd be passing IO types like streams and arrays of bytes around. And with this, the developer no longer even thinks in terms of files, it's all just these IO types. The developer doesn't think I have a file with this name in this directory. I'll open the file and read the bytes from the file. They just think I have a stream of bytes. So this means that the code really can run anywhere. It doesn't matter what the host system, all systems can represent these basic primitive types. And we've completely gotten rid of the potential for the global shared mutable state, while also eliminating the overhead of the per module virtualized file system. This path also potentially opens up opportunities for further optimizations because the engine now has more detailed type information. Now, in talking about these three options, there's something I want to be clear about. You don't need to make the same choice for all of the different modules in your app. Part of this gradual adoption path is having the ability to convert certain modules before others. With both the second and the third option that I just talked about, you're using the WASIO types either explicitly or implicitly. And in both cases, you're not expecting to share the file system between these two modules. So this means that you can use these two modules together and they can simply pass values back and forth between each other. Now, it's not quite as trivial to plug these modules up to ones that use WASIO file system, but it's still pretty easy. If you want a module that's using WASIO file system to call something from a module that uses WASIO, then you just need to have some code in between to extract a stream or array of bytes from the file's content and pass that into the WASIO module. There are some kinds of modules that will always require full WASIO file system that can't use only the portable parts of the file system. But we expect this to represent a very small fraction of the modules that developers are creating and we're hoping to see the rest of the ecosystem gradually migrate to only using WASIO. So this is the kind of thinking that we're applying as we're building out this new ecosystem. How can we move these details out to the edges so that orchestrating code or the host can take charge of them and potentially optimize them? And it's one of these potential host optimizations that I want to end with. Just to get everybody's mind to start turning on what this paradigm could potentially open up for Cloud Native. And this is just one opportunity that we see, but I'm sure that there are more that others who are more familiar with Cloud Native will also be able to see and we're excited to explore those more. Now this opportunity has to do with requests between containers and how to make those faster. So let's walk through what happens when you make a request. Though I want to be clear here, this is just based on conversations I've had. I haven't actually set this up myself and stepped through it. So there's a chance that I've gotten some of the details wrong here, but I think that this is at least directionally correct. So let's say that you want to make a request to another service in another pod. What does it look like? The data that you're sending over gets serialized using a format like protobufs and this is saved into memory in user space. And then the system makes a syscall. The memory is copied over into kernel space memory. So that's two copies already. And now let's say that you're using a sidecar for something like a service mesh. So that sidecar is another container in the same pod. The data gets sent over to this container as an incoming packet. The data gets copied again into kernel space memory by network drivers. The data is then copied into user space of the sidecar proxy. And then the system deserializes the data into objects that it can use. And only then does the service mesh policy actually run on this data. We haven't even gotten the data out of the pod yet. We still have to go through steps one through four again to get the data out to the network. And then when it gets to the other side this whole process happens in reverse. Now two thirds of the steps here were actually to make a request on the same machine. They were to pipe the data into that sidecar. And you'll see that the documentation about the sidecar pattern calls this out as a trade-off. These docs suggest that you ask yourself whether the isolation is really worth the additional overhead that the sidecar is going to introduce in your use case. But this overhead isn't inherent to the problem. We can actually eliminate this as a trade-off. Since we can do fine-grained sandboxing and wasm we can actually make this relationship between the container and the sidecar much more efficient even running them in the same process. But we still get all of the isolation between the two and possibly more depending on how they were using a file, system or volume. And because of this we don't need the socket to be our interface between the isolated units of code. Instead our interface is just typed function calls. To communicate between these two we simply do a synchronous function call on a single thread stack. We do direct register copies and potentially direct memory copies if they're needed. There's no intermediate serialization and deserialization step here. No heavyweight calls to the kernel or inter-process communication. This actually puts us in the nanosecond range for calls between these two. And this would be much faster than the call we just looked at from container to sidecar. However, sometimes you actually do need things to be on different machines across the network. And it would be inconvenient to have different APIs for representing that and to have to change which API you're using in your source code based on whether or not the other container is on the same machine or not. But we don't have to do that. In this paradigm, we've moved all of the decision-making related to where the code is running out to the edges. So the module that you'd write would import the callee, specifying a function signature that's appropriate for cross network calls. So for example, allowing for various network failure modes and supporting non-blocking. Now in the case where the callee is on a different machine, the host would take care of serializing the data and streaming it over the socket. But if a service mesh is being used, the host would instead supply a proxy module that's on the same machine using the much cheaper calling convention described before. Now the important thing is that it's the host that handles this distinction, not your code. And so in this way, you can get the optimal performance when you're talking to a container on the same machine while not sacrificing the ability to communicate with a container that's across the network. We don't have all of these pieces in place right now, but once we do have these foundational primitives in place, we think that someone could build this kind of efficiency into the existing cloud native ecosystem. And we're excited to explore this further. We'll be writing about all of this more over the coming months as we push these standards forward. And we'd be very interested in hearing from people in the cloud native ecosystem about your use cases and about what you see this kind of architecture opening up. Thank you again to the organizers for inviting me and thank you all for listening.