Reaching the Unix philosophy's logical extreme with WebAssembly

2023-08-2716:58229124xeiaso.net

Good morning Berlin! How're you doing this fine morning? I'm Xe and today I'm gonna talk about something that I'm really excited about: WebAssembly. WebAssembly is a compiler target for an imaginary…


Good morning Berlin! How're you doing this fine morning? I'm Xe and today I'm gonna talk about something that I'm really excited about:

WebAssembly. WebAssembly is a compiler target for an imaginary CPU that your phones, tablets, laptops, gaming towers and even watches can run. It's intended to be a level below JavaScript to allow us to ship code in maintainable languages. Today I'm gonna be talking about fun ways you can take advantage of WebAssembly, but first we need to talk about the other main part of this subject:

Unix. Unix is the sole survivor of the early OS wars. It's not really that exciting from a computer science standpoint other than it was where C became popular and it uses file and filesystem API calls to interface with a lot of hardware.

Dealing with some source code files? Discover them in the filesystem in your home directory and write to them with the file API.

Dealing with disks? Discover them in the filesystem and manage them with the file API.

Design is rooted in philosophy, and Unix has a core philosophy that all the decisions stem from. This is usually quoted as "everything is a file" but what does that even mean? How does that handle things that aren't literally files?

(Pause)

And wait, who's this Xe person?

Like the nice person with that microphone said, I'm Xe. I'm the person that put IPv6 packets into S3 and I work at Tailscale doing developer relations. I'm also the only person I know with the job title of Archmage. I'm a prolific blogger and I live in Ottawa in Canada with my husband.

I'm also a philosopher. As a little hint for anyone here, when someone openly introduces themselves as a philosopher, you should know you're in for some fun.

Speaking of fun, I know you got up early for this talk because it sold itself as a WebAssembly talk, but I'm actually going to break a little secret with you. This isn't just a WebAssembly talk. This is an operating systems talk because most of the difficulties with using WebAssembly in the real world are actually operating systems issues. In this talk I'm going to start with the Unix philosophy, talk about how it relates to files, and then I'm gonna circle back to WebAssembly. Really, I promise.

So, going back to where we were with Unix, what does it mean for everything to be a file? What is a file in the first place?

In Unix, what we call "files" are actually just kernel objects we can make a bunch of calls to. And in a very Unix way, file handles aren't really opaque values; they are just arbitrary integers that just so happen to be indices into an array that lives in the struct your kernel uses to keep track of open files in that process. That is the main security model for access to files when running untrusted code in Linux processes.

So with these array indices as arguments to some core system calls you can do some basic calls such as-

(Pause)

Actually, now that I think about it, we just spent half an hour sitting and watching that lovely talk on the Go ecosystem. Let's do a little bit of exercise. Get that blood flowing!

So how many of you can raise your hands? Keep them up, let's get those hands up!

(Pause)

Alright, alright, keep them up.

How many of you have seen one of these 3d printed save icon things in person? If you have, keep your hand up. If not, put it down.

(Pause)

How many of you have used one of them in school, at work, or even at home? If you have, keep it up, if not, put it down.

(Pause)

Alright, thanks again! One more time!

How about one of these audio-only VHS tapes? Keep it up or put it down.

Alright, for those of you with your hands up, it's probably time to schedule that colonoscopy. Take advantage of that socialized medicine! You can put your hands down now, I don't want to be liable.

(Audience laughs)

For the gen-zed in the crowd that had no idea what these things are, a cassette tape was what we used to store music on back when there were 9 planets.

(Audience laughs)

So when I say files, let's think about these. Cassette tapes. Cassette tapes have the same basic usage properties as files.

To start, you can read from files and play music from a cassette tape in all that warm analog goodness. You can write to files and record audio to a cassette tape. Know the term "mixtape"? That's where it comes from. You can also open files and insert a cassette tape into a tape player. When you're done with them, you can close files and remove tapes from a tape player. And finally you can fast-forward and rewind the tape to find the song you want. Imagine that Gen Z, imagine having to find your songs on the tape instead of skipping right to them.

And these same calls work on log files, hard drives, and more. These 5 basic calls are the heart of Unix that everything spills out from, and this basic model gets you so far that it's how this little OS you've never heard of called Plan 9 works.

But what about things that don't directly map to files? What about network sockets? Network sockets are the abstraction that Unix uses to let applications connect to another computer over a network like the internet. You can open sockets, you can close them, you can read from them, you can write to them. But are they files?

Turns out, they are! In Unix you use mostly the same calls for dealing with network sockets that you do for files. Network sockets are treated like one of these things: an AUX cable to cassette tape adaptor. This was what we used to use in order to get our MP3 players, CD players, Gameboys, and smartphones connected up to the car stereo. This isn't a bit, we actually used these a lot. Yes, we actually used these. I used one extensively when I was delivering pizzas in high school to get the turn by turn navigation directions read out loud to me. We had no other options before Bluetooth existed. It was our only compromise.

(Audience laughs)

How about processes? Those are known to be another hallmark of the Unix philosophy. The Unix philosophy is also understood to be that programs should be filters that take the input and spruce it up for the next stage of the pipeline. Under the hood, are those files?

Yep! Turns out they're three files: input from the last program in the chain, output to the next program in the chain, and error messages to either a log file or operator. All those pipelines in your shell script abominations that you are afraid to touch (and somehow load-bearing for all of production for several companies) become data passing through those three basic files.

It's like an assembly line for your data, every step gets its data fed from the output of the last one and then it sends its results to the input of the next one. Errors might to go an operator or a log sink like the journal, but it goes down the chain and lets you do whatever you want. Really, it's a rather elegant design, there's a reason it's lasted for over 50 years.

So you know how I promised that I'm gonna relate all this back to WebAssembly? Here's when. Now that we understand what Unix is, let's talk about what WebAssembly by itself isn't.

WebAssembly is a CPU that can run pure functions and then return the results. It can also poke the outside world in a limited capacity, but overall it's a lot more like this in practice:

A microcontroller. Sure you can use microcontrollers to do a lot of useful things (especially when you throw in temperature sensors and maybe even a GSM shield to send text messages), but the main thing that microcontrollers can't easily do is connect to the Internet or deal with files on storage devices. Pedantically, this is something you can do, but every time it'll need to be custom-built for the hardware in question. There's no operating system in the mix, so everything needs to have bespoke code. Without an operating system, there's no network stack or even processes. This makes it possible, but moderately difficult to reuse existing code from other projects. If you want to do something like run libraries you wrote in Go on the frontend, such as your peer to peer VPN engine and all of its supporting code, you'd need to either do a pile of ridiculous hacks or you'd just need there to be something close to an operating system. Those hacks would be fairly reliable, but I think we can do better.

(Pause)

Turns out, you don't need an operating system to fill most of the gaps that are left when you don't have one. In WebAssembly, we have something to fill this gap:

WASI. WASI is the WebAssembly System Interface. It defines some semantics for how input, output, files, filesystems, and basic network sockets should be implemented for both the guest program and the host environment. This acts like enough of an "operating system" as far as programming languages care. When you get the Go or Rust compiler to target WASI, they'll emit binaries that can run just about anywhere with a WASI runtime.

So circling back on the filesystem angle, one of the key distinctions with how WASI implements filesystem access compared to other operating systems is that there's no expectation for running processes to have access to the host filesystem, or even any filesystem at all. It is perfectly legal for a WASI module to run without filesystem access. More critically for the point I'm trying to build up to though, there are a few files that are guaranteed to exist:

Standard input, output, and error files. And, you know what this means? This means we can circle back to the Unix idea of WebAssembly programs being filters. You can make a WebAssembly program take input and emit output as one step in a longer process. Just like your pipelines!

As an example, I have an overcomplicated blog engine that includes its own dialect of markdown because of course it does. After getting nerd sniped by Amos, I rewrote it all in Rust; but when I did that, I separated the markdown parser into its own library and made a little command-line utility for it. I compiled that to WebAssembly with WASI and now I think I'm one of the only people to have successfully distributed a program over the fediverse: the library that I use to convert markdown to HTML, with the furry avatar templates that orange websites hate and all.

Just to help hammer this all in, I'm going to show you some code I wrote between episodes of anime and donghua. I wrote a little "echo server" that takes a line of input, runs a WebAssembly program on that line of input fed into standard in, and then returns the response from standard out. The first program I'm gonna show off is going to be a "reply with the input" program. Then, I'm going to switch it over to my markdown library I mentioned and write out a message to get turned into HTML. I'm going to connect to it with another WebAssembly program that has a custom filesystem configuration that lets you use the network as a filesystem because WASI's preview 1 API doesn't support making outgoing network connections at the time of writing. If sockets really are just files, then why can't we just use the network stack as a filesystem?

Now, it's time, let's show off the power of WebAssembly. But first, the adequate prayers are needed: Demo gods, hear my cries. Bless my demo!

On the right hand side I have a terminal running that WebAssembly powered echo server I mentioned. Just to prove I didn't prerecord this, someone yell out something for me to type into the WebAssembly program on the left.

(Pause for someone to shout something)

Cool, let's type it in:

(Type it in and hit enter)

See? I didn't prerecord this and that lovely member of the audience wasn't a plant to make this easier on me.

(Audience laughs)

You know what, while we're at it, let's do a little bit more. I have another version of this set up where it feeds things into that markdown->HTML parser I mentioned. If I write some HTML into there:

(Type it in and hit enter)

As you can see, I get the template expanded and all of the HTML goodness has come back to haunt us again. Even though the program on the right is written in Go:

(I press control-backslash to cause the go runtime to vomit the stack of every goroutine, attempting to prove that there's nothing up my sleeve)

It's able to run that Rust program like it's nothing.

(Applause)

Thank Klaus that all that worked. I'm going to put all the code for this on my GitHub page in my x repo.

This technique of embedding Rust programs into Go programs is something I call crabtoespionage. It lets you use the filter property of Unix programs as functions in your Go code. This is how you Rustaceans in crowd can sneak some Rust into prod without having to make sacrifices to the linker gods. I know there's at least one of you out there. Commit the .wasm bytes from rustc or cargo to your repo and then you can still build everything on a Mac, Plan 9, or even TempleOS, assuming you have Go running there.

Most of the heavy lifting in my examples is done with Wazero, it's a library for Go programs that is basically a WebAssembly VM and some hooks for WASI implemented for you. The flow for embedding Rust programs into Go looks like this:

  • First, extract the subset of the library you want and make it a standalone program. This makes it easy to test things on the command line. Use arguments and command line flags, they're there for a reason.
  • Next, build that to WASI and fix things until it works. You'll have to figure out how to draw the rest of the owl here. Some things may be impossible depending on facts and circumstances. Usually things should work out.
  • Then import wazero into your program and set everything up by using the embed directive to hoist the WebAssembly bytes into your code. Set up the filesystems you want to use, and your runtime config and finally:
  • Then make a wrapper function that lets you go from input to output et voila!

You've just snuck Rust into production. This is how I snuck Rust into prod at work and nobody is the wiser.

(Pause)

Wait, I just gave it away, no, oops. Sorry! I had no choice. Mastodon HTML is weird. The Go HTML library is weirder.

There's a few libraries on GitHub that use this basic technique for more than just piping input to output, they use it to embed C and C++ libraries into Go code. In the case of the regular expressions package, it can be faster than package regexp in some cases. Including the WebAssembly overhead. It's incredible. There's not even that many optimizations for WebAssembly yet!

No C compiler required! No cross-compiling GCC required! No satanic sacrifices to the dark beings required! It's magic, just without the spell slots.

So while we're on this, let's take both aspects of this to their logical conclusions. What about plugins for programs? There's plenty of reasons customers would want to have arbitrary plugin code running, and also plenty of reasons for you to fear having to run arbitrary customer plugin code. If we can run things in an isolated sandbox and then define our own filesystem logic: what if we expose application state as a filesystem? Trigger execution of the plugin code based on well-defined events that get piped to standard input. Make open calls fetch values from an API or write new values to that same API.

This is how the ACME editor for Plan 9 works. It exposes internal application state as a filesystem for plugins to manipulate to their pleasure.

So, to wrap all of this up:

  • When you're dealing with Unix, you're dealing with files, be they source code, hard drives, or network sockets.
  • Like anything made around a standards body, even files themselves are lies and anything can be a file if it lies enough in the right way.
  • Understanding that everything is founded on these lies frees you from the expectation of trying to stay consistent with them. This lets you run things wherever without having to have a C compiler toolchain for win32 handy.
  • Because everything is based on these lies, if you control what lies are being used, you actually end up controlling the truths that users deal with. When you free yourself from the idea of having to stay consistent with previous interpretation of those lies, you are free to do whatever you want.

How could you use this in your projects to do fantastic new things? The ball's in your court Creators.

(Applause)

With all that said, here's a giant list of everyone that's helped me with this talk, the research I put into the talk, and uncountable other things. Thank you so much everyone.

(Thunderous applause)

And with that, I've been Xe! Thank you so much for coming out to Berlin. I'll be wandering around if you have any questions for me, but if I miss it, please do email me at crabtoespionage@xeserv.us. I'll reply to your questions, really. My example code is in the conferences folder of my experimental repo github.com/Xe/x. Otherwise, please make sure to stay hydrated and enjoy the conference! Be well!

This talk was posted on M08 27 2023. Facts and circumstances may have changed since publication Please contact me before jumping to conclusions if something seems wrong or unclear.


Read the original article

Comments

  • By p1mrx 2023-08-292:072 reply

    Reminds me of that time I wrote a StringToExecutableFile() function for running [e.g. a Rust binary] from C++, but that depended on several layers of build system horror to embed the executable file as a string, and it wasn't cross-platform.

    Imagine a utility function that dumps an embedded string to an unlinked temporary file, sets the +x permission, and returns a /proc/self/fd/N filename so you can exec() a subprocess. It's somewhat difficult because of write^execute limitations in Linux.

    Running WASM in process seems like a much saner idea.

    • By paulfurtado 2023-08-293:042 reply

      This is of course not the purpose of your post, but since you're interested in this topic, I wanted to mention that you can now create memory-backed files on linux using the memfd_create syscall without using any filesystem (nor unlink) and you can also execute them without the /proc/self/fd trick by using the execveat syscall. In glibc, there is fexecve which uses execveat or falls back to the /proc trick on older kernels.

      • By p1mrx 2023-08-294:401 reply

        Looks like memfd_create is from Linux 3.17 (2014), which was after I wrote the function. I sort of miss the days when simple stuff was hard.

        • By tester756 2023-08-296:282 reply

          >I sort of miss the days when simple stuff was hard.

          what? what's the point?

          for me it's the most annoying thing when the simple stuff is hard because why would it be?

          • By bheadmaster 2023-08-296:451 reply

            Hacking is the art doing things with software that don't seem possible.

            In other words, it's just fun :)

          • By IshKebab 2023-08-297:451 reply

            I agree. I was looking into how you start a child process in C++ recently and I was surprised and not at all surprised to find that the answer is still fork and execve. Ridiculous.

            • By cylemons 2023-09-0612:14

              Wouldn't that be OS specific anyway? Like, Windows has no concept of forking processes but instead it uses the CreateProcess function.

      • By intelVISA 2023-08-2911:10

        Depending on how hacky you can also just re-invent a non-relocating ELF loader

  • By fallat 2023-08-291:254 reply

    Neat talk but like... doesn't it all seem convoluted? What is the function of a Rube Goldberg web service? To allow it be made of anything?

    The problem with using Rust in Go is that you entirely miss all the parts you don't use Rust; you get the VM overhead of WASM so it kills Rust perf; you most likely introduce problems at the boundaries of Rust/Go.

    Again it's a neat idea but why in all things sane would anyone intentionally do this outside of puzzle-solving satisfaction?

    • By xena 2023-08-292:021 reply

      Author of the talk here. When I am doing conference talks to help explain abstract concepts or ideas, I typically prefer to employ a strategy called surrealist satire. This basically helps people understand where something fits into the stack by demonstrating how something fits into the mold and then by doing another completely impractical thing with that surrealist solution. The goal of this is to help people hook something into a greater set of context (due to the assumptions I made about the audience, I had to explain a bit more about the topic than I would have otherwise at say a WebAssembly conference) so that they can figure out how things that seem unrelated are actually quite related.

      In terms of performance numbers though, I have quite intentionally NOT included performance benchmarks in this talk because getting stable performance information is nontrivial. I plan to write something in the future about WebAssembly vs native code as a subprocess (the differences with windows may surprise you!), but that is not a thing for today.

      • By fallat 2023-08-292:07

        > I typically prefer to employ a strategy called surrealist satire

        Ah, gotchya.

    • By some_furry 2023-08-291:451 reply

      > Neat talk but like... doesn't it all seem convoluted? What is the function of a Rube Goldberg web service? To allow it be made of anything?

      I don't want to be the guy who explains the joke, but sometimes Xe creates elaborate shitposts that aren't entirely shitposts but contain a very fun element to them rather than being for fully practical sake. Shitposting being an anagram for top insights after all.

      That's the lens I used when interpretating this talk.

    • By tedunangst 2023-08-292:131 reply

      The current reference (only?) implementation of jpeg-xl is a c++ library, which I do not entirely trust to run in process in my go web server, and yet I would like to process images. Conveniently, the build system for jpeg-xl seems to support building to wasm, so if I can jam that into my process, I'd be a lot happier.

      • By fallat 2023-08-292:192 reply

        Ah, now THAT's an interesting aspect I wish someone would have brought up over the years I've seen WASM.

        I guess WASM as a target and embeddable VM really helps with security in those cases. Couldn't we also do the same though with any number of arch/vm pairings?

        I guess what WASM brings to the table is a compile target friendly enough for things like C and C++, i.e. low level code, and a reasonable VM implementation. It just has to be accepted by everything (both as an output format, and virtual machine impl in the language choice) to work, I think...

        Edit: Like why arent we using https://stackoverflow.com/questions/4221605/compiling-c-for-...

        • By aidenn0 2023-08-293:271 reply

          I don't know the current state, but the JVM had historically quite bad isolation. Javascript's isolation in browsers is actually pretty good. It's possible that the existing non-browser WASI implementations are terrible at isolation; since they exist specifically to allow access system resources they might be bad at denying access to system resources...

          • By kaba0 2023-08-296:47

            The JVM’s security in the Applet days were bad due to actually being capable at many functionality — it is trivial to properly sandbox something that has zero capabilities.

            There is nothing inherent in the JVM that would make it less secure, we just realized in the meanwhile that blacklisting is not the way ahead, but whitelisting is.

        • By IshKebab 2023-08-296:38

          Library sandboxing is a well known application of WASM. Search for the WebAssembly Component Model. There's also a way to use WASM to sandbox a C library - search for RLBox.

    • By nine_k 2023-08-291:331 reply

      This looks more than a bit like the Inferno OS, created by some of the same folks who created Unix.

      WASM allows to distribute cross-platform binary code written in memory-safe Rust, running on pretty compact VM. Think about a plugin architecture that could be built on top of that.

      • By fallat 2023-08-292:131 reply

        Java (like mentioned in the article) does the same though

        IIRC Inferno OS is like Java in this regard.

        How will WASM change what Java has already done?

        Why would I compile Rust to WASM when I can compile Rust natively to any number of platforms? And use FFI?

        I think WASM, while nice, doesn't bring much new to the table. It's been hyped for years, and I still only see it used here and there very sparingly.

        • By nine_k 2023-08-295:043 reply

          This is how.

          The installed size of OpenJDK 17 JRE on my machine is 186MB, according to the package manager.

          I suspect that the WASM VM embedded in the program demonstrated in the blog post is 1.5 to 2 orders of magnitude smaller.

          • By geokon 2023-08-295:58

            I'm no expert but the JVM is very modular these days and just the minimal modules also give you an order or two smaller runtimes. My guess is a set of minimal OpenJDK modules will be on the same order as a WASM VM. Would be curious to hear from someone more in the know

            Looking a the JRE size is a bit misleading bc it's been sort of deprecated. You're not really supposed to make Uberjars to run on a JRE anymore but are expected to bundle with the JVM modules you need. It can make very small bundles..

            But naturally an Uberjars would be smaller. I think small executables are possible but are also just a nongoal now in the JVM world. Meanwhile they're obviously still very relevant in the webspace and hence WASM

            you're not really gunna send JVM bundles dynamically over the wire.

            I do sort of agree with the parent that while the goals are slightly different it feels like WASM reinvented the JVM without really bringing any huge improvement (while you loose several decades of libraries)

          • By brabel 2023-08-296:55

            There are many JDKs available, some of which specializing in embedded, like this one: https://en.wikipedia.org/wiki/JamaicaVM

          • By kaba0 2023-08-296:49

            There is no such thing as a JRE anymore, the way to package a Java application for quite some time now is by using jlink/jpackage that creates a stripped “JRE” of only the used modules helping both the size and loading times.

  • By 38 2023-08-292:482 reply

    I thought the Unix philosophy was do one thing well:

    https://wikipedia.org/wiki/Unix_philosophy#Do_One_Thing_and_...

    how is running anything through a giant virtual machine (web browser) anywhere close to that? the browser is the monolith people. using a web browser to deliver an application is, always has been, and always will be the slowest, and most bloated way to do that. the benefit of course is, that the result is user friendly and cross platform. but lets not kid ourselves, this is as far from the unix ideals as you can get.

    • By ngc6677 2023-08-298:01

      see my comment in this thread about https://github.com/internet4000/find (for a summary); (web-)apps can be encapsultated, from the URI you access them, and the code that is served by the loaded page. It could also run client side only, local first (web apps manifest, service workers, sqlite/postgres wasm etc.)

    • By YoshiRulz 2023-08-296:38

      It's probably referring to the Markdown to HTML translator library, an example of how Wasm enables Unix-style composition.

HackerNews