I've been quite frustrated with the state of WebAssembly. It still feels like a platform that's in beta and not ready for production. Partially this is the slow pace of standardization, partially this is the lack of investment into tooling by various stakeholders.
This plays out precisely as the blog post details with the split between WASI and Web Platform. Say you want to compile a Rust + C codebase to Wasm and run in the browser. You have three targets: wasm32-unknown-emscripten, wasm32-unknown-unknown, and wasm32-wasi. Emscripten is relatively old and not maintained. It'll work but you get some old JS that doesn't play well with newer stuff. wasm32-unknown-unknown has an ABI incompatibility which means you cannot interoperate between C compiled to Wasm and Rust compiled to Wasm. wasm32-wasi works, but now you have to have a WASI implementation in the browser and that's still very immature. Tools like wasm-bindgen or wasm-pack don't work with wasm32-wasi either.
Basically you have the Web Platform backend (wasm32-unknown-unknown) that plays well with the browser part, but does not play well with the interoperability across languages part. And you have the WASI backend (wasm32-wasi) that plays well with the interoperability across languages part but does not play well with browser part.
It's not your imagination, the 1.0 version of the spec was/is widely referred to as MVP - "Minimum Viable Product" - and it was an ordeal just getting that across the finish line. It's why so many things are missing. The rest of the ecosystem naturally inherited that state of readiness.
EDIT: I have to disagree though - emscripten is actively maintained. Not sure why you get the impression otherwise.
We're using emscripten in production right now. New stuff is committed to its github repository every week and there are new releases. I think you're confusing "wasm32-unknown-emscripten" for "emscripten", the latter is a product you can use to compile software.
I think the article summarizes well the issue but completely misses the point on why this is happening.
First let me provide some context: I'm Syrus, CEO of Wasmer (a WebAssembly-powered company).
There has been actors in the industry (I'm not naming them purposefully) that are using their corporate power (that is, their seats of influence over the standards committee) to move things forward as specifications even though their usage is minimal and not strong enough to yet become a standard.
The issue about that strategy is that incentivizes creating new "standards" to gain control over them, even though there are already viable solutions for things that will solve people current needs. Let me put some examples of this:
1. Creating Wasi-NN instead of pushing for WebNN standard API [1]
2. Creating WebAssembly Interfaces instead of pushing for WebIDL (specially important for the browser, if we ever want to get DOM access from Wasm, this is probably the fastest way atm)
3. Creating WASI-fs instead of pushing towards the Filesystem spec [1]
4. And so on
The issue is not on the technical advances per se, which I personally think they are great.
But on the fact that those advancements start creating a breach that is different and competitive with the browser instead of being cohesive and complementary, and by doing that they are putting their governance power interests over the user needs.
Are these companies doing this to control the new standards or are they doing this because adhering to the web standard (both that you linked, I think?) puts them in a position where they'll be captive to an even larger committee (any web standards body), making it so any changes will take longer to push through, and implementation costs will be higher?
Arguably these are sort of the same thing. Being subject to the whims of the committees is a competitive disadvantage, since the committees are largely controlled by big corporate players. So if you create a new competing standard you control, it's 2-for-1: Not only are you no longer subject to the whims of big companies, but you get to set the rules.
One of the upsides to using the big committee standards is that the big companies often have a reference implementation you can use, which is a time-saver. But then you can hit sharp edges or limitations.
Sometimes is better to put things into perspective to see if certain arguments hold.
In this case, you just need to analyze how fast they have been moving to see if iteration speed was the reason for pushing for own-governance outside of the web-standards (note: WASI, coined by those entities, was launched 4 years ago)
Good discussion. There are still real tensions but I do tend to believe the component-model makes a huge amount of these questions moot.
Quite comprehensive, but I think there's a pretty informative question to ask. If I write a Go wasm and you write a Rust wasm module, how do they work together? How can I call your thing? I could be mistaken on these but I think I one really does this now, and it's the macro-focus of component-model, to build a stable inter-module binary interface.
Today... on the web there's not really a good solution other than hand writing more glue code in js to relay activity. On the server side wasi defines syscall like things you can call out & do, but I think it's just limited pre-baked operations, not a generic way to consumer a variety of external resources. Component-model is here to let us define exports & imports at a higher level & to let us stitch modules together via these interfaces.
The article paints a very good very real story of how modules & runtimes interact, but that's a somewhat insufficient question, for the real goal of of interoperable modules working together. Asking how modules work with each other (not just the upper system) highlights how the systems question here is broader.
> for the real goal of of interoperable modules working together
CORBA, Beans etc predate the modern web but its telling that no one has really bothered to try to recreate them...what is the demand? server-side wasm just doesn't seem to be anything that is strongly needed and looks a lot like completists just scratching an itch
two languages that only communicate at the wasm level...isn't this just a variation on "DLL Hell"?
as to a Go program accessing a Rust module...on a practical level you would almost always be better off taking the thing in Rust that you want to use in Go and just doing a best-effort source translation or find some library out there that provides substitute functionality
I'd rather have source translation tech that just gets functionality into the language I am using
Maybe I'm missing the mark, but isn't server-side wasm basically solving the need for source translation?
The semantics of golang and rust are so different that any translation runs the risk of producing wildly not-idiomatic code.
Of course, you lose out on all the runtime benefits that various platforms might provide (I don't see anyone who is invested in the JVM giving it up for wasm) but why not have rust and golang binaries compiled to a common format and let the communicate directly?
Personally, I don't do much polyglot programming these days so I don't really have a dog in the fight, so to speak, but my experience with source-to-source translation is that it tends to be pretty awful short of simple translations that keep the same runtime and underlying data primitives.
> isn't server-side wasm basically solving the need for source translation
The hard part about bridging languages isn't solved by WASM, it's about working with complex types across the call boundary. If you want to pass a Python dict to a C++ function that accepts std::map then something will have to do that translation, handle GC concerns, etc. Likewise for function types, object instances, files, strings etc. And then that interop overhead will hurt performance so you may need to optimize that.
That's why if you look at Truffle, which is SOTA for this problem, it works in a very different way to WASM. There's no attempt to do a unified bytecode or a COM-like component system. It works with whatever the language's natural representation is and does interop by optimizing and JIT compiling merged partially evaluated ASTs or unrolled bytecode interpreters.
If you already have to build a wasm target, I don't see why building one for each arch is that much worse, especially if there are probably performance benefits by building platform specific.
There's a lot of very lovely very interesting very enjoyable Function-as-a-Service things & platforms, and server-side wasm is one very promising generic way to be able to potentially simmer down & make generic & very fast these networks of functions.
Today FaaS (ex: aws lambda) are most frequently run by having a process for each function, but with wasm the nice sandbox system/object-capabulities style, everyone can share a runtime & have less barrier crossing & much less overhead. There's a ton of near magic properties here that could help radically change how we write & maintain systems, shifting us from big blocky monoliths to something more like an Erlang universe, where there's lots of small interoperating pieces, sharing a common fabric that stitches them together.
I think the CloudFlare Workers world probably comes the closest to showing off these promises, and it's a pretty slick elegant low fluff take, that also comes with very big advantages about being architected from the start to have many many deployment points, with the ability for individual workers to come & go on demand at very high speed at any given deployment site.
We can also easily spot incipient demand for wasm by just looking at all the places folks embed scripting languages. Every time you see a Lua scripting engine (neovim, nginx, a million other places), you could potentially have any langauge you please. And given the lightweight sandbox, how quick it is to spawn instances, we can make our software pipelines much more user configurable & dynamic.
A good parallel is the kernel, which has become much more capable of programmed behavior via the addition of ebpf to instrument not just networking but all kinds of subsystems. This has lead to huge wins, huge gains in all kinds of very fast very secure networking interlinks, with record low overhead. It's being used to help remap weird input devices behaviors, and dozens of other interesting little things you only get by having some flexibility built in. Rather than hard-coded subsystems everywhere. Wasm is lined up to be the ebpf of user land systems, it is the thing that allows apps to bake in flexibility & programmability, in a fast, safe, interoperable way.
The discussion here is about how the library of wasm modules can arise & reinforce each other. The cynicism against interoperability, suggesting source translation, is blind to the use cases above. It doesn't see the Erlang view of the world, it's rooted in the boring legacy view that computing is and always will be giant monolithic apps doing three million different things within the confines of the colossus-sized process. Having other alternatives to computing where we can have a sea of smaller units that message each other doesn't have a clear & obvious triumph, it's not inherently obviously better than the megaprocess view of the universe, but it should be possible & we should find out what we can do. And even if the femto-service / Nano-function model ends up being bad, we'll still have apps that have a well established means & well supported common tool chains for adding programmability in. That requires a mature interoperable modular system to get anywhere, and thankfully folks aren't scared off from trying, not dissuaded because someone mentions the CORBA boogeyman under the bed & tries to gloom us away.
This is incredibly promising technology & I recommend everyone come evaluate it with a positive hopeful light. I hope I've helped share good outlooks that can help get folk excited (as they should be!)
> Every time you see a Lua scripting engine (neovim, nginx, a million other places), you could potentially have any langauge you please
but people like code reuse, so its almost always better to communicate, collaborate and share at the source code level than at the level of a compiler target
as far as I can tell, the only real target audience here are pathological language snobs ("I insist on writing neovim extensions in Rust!") who will validate their inflexibility by pointing at the lowest common denominator of wasm
What do you propose nginx does? Should they embed rust, go, c, visual basic, turbo pascal, nim, and 37 other scripting runtime? Should they pick 3 to support?
How many of these have embedded scripting systems? How different will they be? How many offer lightweight nano-processes? How many are secure sandboxes? How many support arm, RISC-V, or the next architecture?
Having a common platform here makes so much sense to me. Enabling a variety of things, safely, quickly, & with well defined patterns, seems intuitively advantageous, frees us from having to tangle with a variety of minor subdivisions that fix software onto narrower paths.
> What do you propose nginx does? Should they embed rust, go, c, visual basic, turbo pascal, nim, and 37 other scripting runtime? Should they pick 3 to support?
no, pick one that is a decent compromise and move on
no one is going to dismiss the entirety of a platform like nginx because they can't script it in their favorite language
> but people like code reuse, so its almost always better to communicate, collaborate and share at the source code level than at the level of a compiler target
And now you argue we should all only ever have Lua to collaborate on.
Cause that's the only big player in embedded scripting atm. Js alas didn't take off but it's not that bad to embed. Few other languages have much at all. There are some projects to make ebpf embeddable in apps.
You seem to really have it in for what seems like a perfectly good attempt that seemingly makes a lot of sense & has hugely obvious benefits. I don't get the opposition to trying.
The other day I came across an interesting "alternative" to WASM which gives you OS portability using fully native code, without CPU portability, the latter seeming not that big of a deal these days anyway as cross compilers have got quite good and there are only two CPU archs in wide usage anyway.
The idea is to simply run normal Linux binaries on macOS and Windows. How? You create a virtual machine using the Mac/Windows APIs without any OS inside, in fact without even any virtual hardware. It's literally just a new address space and some trivial min-viable VM configuration. Then you map the ELF binary and a ld.so into the VM with a minimal ELF interpreter, kick off execution and anytime there's a syscall you trap it and translate to the host OS syscalls. It can work quite well on macOS because the syscall interface is so similar.
Note that this sort of VM is not:
• A sandbox
• A hardware abstraction
Apps run this way hold all their data in the filing system of the host OS, they use the network stack of the host OS, etc. The VM is only being used to allow trapping and emulation of the syscall interface. The app isn't aware that it's being run in a special CPU mode on top of an emulated kernel.
Advantages: lightweight, simple, apps can use all CPU features, can run at native speed, the Linux syscall interface is highly stable, based on POSIX specifications and you can easily pick a subset of it to standardize.
Disadvantages: requires the emulator, apps exposed to missing features or quirks of the host OS e.g. Windows file system performance is much lower than Linux.
WSL1 sort of worked that way, albeit without the VM aspect that lets userspace apps do it. They abandoned it partly for performance reasons and users expected all existing Linux apps to just work. But WASM doesn't target existing apps. It expects developers to bend and do things the WASM way, and accepts that not all apps are compatible with it, so that's not necessarily a problem.
> without CPU portability, the latter seeming not that big of a deal these days anyway as cross compilers have got quite good and there are only two CPU archs in wide usage anyway
Do we really want to surrender to the dominance of two proprietary ISAs? To me, the insistence on something like wasm bytecode is a kind of optimism, a hope that the current duopoly won't be permanent.
Well, Apple have proven several times now that you don't need a bytecode abstraction to allow CPU architecture transitions. They went from PowerPC to Intel to ARM whilst preserving the benefits of native code each time, using relatively simple stuff that stayed out of developers way and didn't require buyin to entirely new ways of coding or APIs. Rosetta 2 is just an ordinary AOT transpiler with a few magic flags in their ARM fork that lets them optionally match Intel semantics for code that needs it, other people should be able to match that.
So bytecode is useful for lots of other reasons when paired with good runtimes, but it doesn't seem to be necessary to let you switch CPU ISAs or vendors.
Meanwhile the costs are super high. WASM doesn't and isn't going to expose the real capabilities of the hardware any time soon, so it'll always lag behind in performance. The web has an unfortunate design goal of killing all the incentives OS and hardware makers have to add new features, because browser makers are these days pretty averse to allowing web pages to have special capabilities on specific underlying platforms, so features only get exposed to the web if every (major) OS and CPU combination supports them. But why add new features if apps will only start to use them if all your competitors also ship the same feature - there's no competitive advantage to be had there, which in turn means there's no point in investing in R&D.
In turn that means that there's actually no point in designing or deploying a new ISA if everything is WASM. Why bother if it can't add any unique capabilities or nothing would use them? RISC-V says licensing is a good enough reason, but not many competitive design houses seem to care: making a high performance CPU is so expensive that ARM licensing costs just aren't a big deal in the grand scheme of things and in practice silicon OEMs seem fine with outsourcing ISA development to them.
Leaning in to native code fixes the incentive problem, and software is constantly being updated these days anyway. If CPU portability was all that mattered it'd be cheaper and quicker to invest in better cross-compilation toolchains, like a standard file format for expressing all the URLs to all the different OS/CPU specific binaries for a program and build servers that could incrementally build and upload binaries for less popular ISAs in the background.
I outline a proposal for an alternative web-like thing that works this way here (whilst also supporting wasm and other bytecodes):
> Rosetta 2 is just an ordinary AOT transpiler with a few magic flags in their ARM fork that lets them optionally match Intel semantics for code that needs it,
This really understates how significant it is that Apple customized their CPUs in order to give their emulator good performance. Ordinary emulator developers can't do this.
No, but that's fundamental. You have to pick a memory model the moment you support shared memory multi-threading, and then either you'll be incompatible with existing code or you'll have to implement that stricter memory model on ARM. WASM just punts on shared memory multi-threading, or does it?
Yeah that's an interesting way to put it - lightweight federated overlay OS. Nice! I like it. Perhaps that can be the origin of a name. Lofos - lightweight overlay federated operating system.
Not really pitching it to anyone tbh. I'm doing this as a hobby exercise to try and keep my design skills sharp at a time when I'm mostly maintaining and debugging a mature codebase.
You're right, I didn't know about that. I thought QEMU is always a full OS emulator. Could you give a link for hangover? I'm not sure how to disambiguate in search from actual hangovers.
They were working on ARM and a bit of POWER9 for a while, before it had to be put on hold due to all the PE conversion work and resulting churn in WINE. Now it seems to have been restarted/rewritten.
Deep respect to the CloudABI folks, a project which inspired the WASI ideas:
> CloudABI is no longer being maintained. It was an awesome experiment, but it never got enough traction to be sustainable. If you like the idea behind CloudABI, please consider looking into the WebAssembly System Interface (WASI). WASI's design has been inspired by CloudABI.
I was investigating using wasm/wasi to implement a parser that is currently written in javascript, such that I would still be able to use it in the browser but also open up other avenues for it to be used on other platforms.
I got bogged down in the wasi/wasm debate and terminology and different available runtimes (or whatever they should be called) - wasmtime, wasmer, wasmedge, fermyon, bytecode alliance... I probably would have spent more time and figured it out if I didn't want to learn rust right now. Seems rust is the best supported language for wasm as far as I can tell.
Otherwise admittedly I would have used a large token context ChatGPT to convert the library to rust for me, and then also have it compile it to wasm.
Just wrap the core functionality into a 'pure' WASM module which doesn't need to access 'system APIs', and then if needed write two thin wrappers, one for the 'web personality' and one for the 'WASI personality'.
WASI has the advantage that many compilers support it directly, e.g. in Zig:
This is a timely article for me since I've been trying to learn more about webassembly lately, thank you for it.
I think the system interface is probably extremely important and significant part of webassembly and history. It's history making. It's the definition of a foundational interface that all users of webassembly shall inherit and need to target if it takes off. Which I hope it does. A standardised bytecode format is awesome!
Why do I say this? The binary interface for C programming such as the amd64 SysV calling convention is established and is important for C FFI interoperability. Likewise, the syscall interface for Linux is established and so is Win32 API or POSIX.
If people are trying to compile existing code so it doesn't need to be changed to support webassembly, then that's one constraint. Another constraint is performant Javascript interop. Another constraint which I personally find most interesting is the capability of defining a new API that is fit for purpose for application development. So that incorporates threading, garbage collection and sockets.
The problem is that the wasm-on-the-server-side people don't seem to understand that the browser vendors will never ever let them have the laundry list of features that are necessary to fulfill Solomon's quoted tweet. The sooner the server-side people decide on their own standard (that won't be compatible 1:1 within the browser) for the server workloads the faster they can work on their end goals. WASI has been around for a while and is still missing quite a lot.
raw sockets, dynamic linking, 32bit vs 64, linear memory (gc), memory protection, real threads, TLS - the list is seemingly endless.
If I were building something in the wasm community (I'm not) I'd be less focused in on building one off bindings for fuctions-as-a-service, which seems to be the gtm right now and more focused on building the underlying foundations that are necessary.
I agree with your overall point, but interestingly it does look like Chrome is interested in shipping WASM GC in the browser. Here's their tracker for it: https://chromestatus.com/feature/6062715726462976
The fight for WASI is dramatically more important for the future of tech than AI, bitcoin, and all the other silly fads.
If WASM gets sucked into the vortex of pragmatism and fails at its original mission of discarding all of the technical debt and obsolete paradigms, then we'll miss out on a golden opportunity to reinvent the modern technology stack.
Those who want to deliver something that works right now can fork the project and call it WARN (WebAssemblyish Right Now).
To me it sounds like you could implement some parts of the WASI in JavaScript, so a WASM module depending on WASI doesn’t preclude it from running in the browser or a browser-like runtime. Instead of a bespoke shim generated by the compiler, you could use a standardized shim that provides a known subset of WASI.
On the other hand relying on JS shims means a lot of guaranteed copying to/from the JS heap, which is a very annoying speed tax compared to… not.
I wonder why browsers don’t allow direct WebIDL-based APIs direct from WASM to browser APIs
Wasmer's one in theory works in the browser but I've yet to have it work. You have to polyfill some stuff like Buffer and even then you run into some annoying runtime Wasm errors.
WASI is also used as an ABI for ProxyWASM so you can load whatever custom code you want into a proxy to handle request transformations and the likes that are too large or bespoke to fit into plain configuration files. Envoy does this using a C++ host that starts V8, but there are legacy implementations without V8, and alpha implementations that did it with newer WASM runtimes.
Very timely post. I was recently looking at the ways one can implement a plugin system using WASM (outside the browser). Communicating between the host application and the plugin is sure tricky... I've seen places that use WASI to map stdin/stdout to files and use that. It feels... wrong.
Shouldn't the allocation approach of WebAssembly change for this to be a real thing? Right now (at least in the browser) my main issue with WASM is that it just allocates a linear piece of memory that quickly fragments over time. How will this work for a server app?
There really isn't an allocation approach in WASM. It just gives you a buffer and can grow, and the rest is up to you. Which means you have to bring your own allocator. Your fragmentation entirely depends on how good your allocator.
Since we optimize for size on the web, it's hard to justify bringing the entire jemalloc into every wasm module.
… the allocator can only do so much. If there's a small allocation after a large one, and the large on is freed and the small one held forever, you may never be able to de-fragment that.
I think it's telling that Unix started with sbrk and ended up with mmap, here.
The OS just gives you new chunks of memory, but it's the job of the application to manage the memory it has been given by the OS.
Modern general purpose allocators are a bit more clever then just grabbing the next available address from a single free-list, usually they group allocations by size into buckets.
Then there's specialized allocators (e.g. arena, bump, stack ...) which not only prevent fragmentation but also increase memory management performance.
In general, memory fragmentation in a limited address space is only a problem for programs that don't have a proper memory management strategy.
I'm aware of all of the things that you mention, and you can assume that the allocator I am discussing here is reasonably intelligent and does all of those things.
Even with everything you mentioned, the point is that the allocator (doing all those things) cannot return free memory to the OS, because the APIs simply don't exist for it: that's what mmap() provides over sbrk().
In the worst case, even for an allocator doing all those things, this can result in an allocator being unable to return any memory: if exactly the last allocation remains alive, sbrk() cannot be called (as you'd free a still-live allocation); but in the case of mmap(), we can free most, or in the best case, all of the memory not in actual use.
Giving you a buffer that grows is the allocation approach I am talking about. This is not how your OS works. Your OS itself works with an allocator that does a pretty good job making sure that your memory ends up not fragmented. Because WASM is in between, the OS is not in control of the memory, and instead the browser is. The browser implementation of "bring your own allocator" is cute but realistically just a waste of time for everybody who wants to deploy a wasm app because whatever allocator you bring is crippled by the overarching allocator of the browser messing everything up.
It seems like the vendors are recognizing this though, with firefox now having a discard function aparently!
> This is not how your OS works. Your OS itself works with an allocator
This is exactly how your OS works. There is no malloc syscall. You call sbrk [1] (the modern mechanism to do that on Linux eludes me, maybe it's through mmap), get a chunk of memory and do your thing. malloc is implemented in glibc, and you can swap it out with any other allocator, but your kernel just gives you a chunk of memory and it is your job (as userland process) to suballocate it, deal with fragmentation, etc.
I am not sure you read the link I posted. Picking a single point out of it, like "No way to shrink the allocated Wasm Memory." makes it clear that this allocation strategy is really very different to how your OS works when you call malloc/free outside of wasm.
Also take a look at "Some applications need address space, not memory" in that link, same stuff.
Long story short, running inside WASM is like running without virtual memory. Virtual memory was invented to solve a bunch of issues, so now WASM has all those issues unsolved.
Allocation strategies are the job of the program that's run, not of the VM the program runs inside. "Just" use an allocation strategy that doesn't fragment your heap, usually the best solution is to use a handful specialized allocators instead of a single 'general purpose' allocator.
There’s a third path here, which is to target WebAssembly directly, with no wrappers. Then you can import just the functionality the module needs to work.
The benefit is that you can actually understand what your code is capable of doing to its environment (instead of working with the “one size fits all” type of interface that has produced so many security issues on existing platforms).
Java bytecode was (and still is) fairly strongly tailored to Java. Which means that, on one hand, it deals with abstractions like classes and object references, and on the other hand, there's no way to, say, just read or write some memory address.
wasm is much lower level bytecode that e.g. C can be compiled to.
Man people have to get over this joke. Its been like 10 years of every time WebAssembly comes up somebody has to make this 'joke'. Like it was never funny, and it was always fucking dumb.
Java isn't the first language base on bytecode, and WebAssembly will not be the last.
Wasm in the browser is pretty cool, but I still don’t understand why you would want to give up the advantages of native code when running server side. Native code is faster and can actually use features of your hardware like the MMU, giving you things like virtual address space and memory protections. If you want to glue modules together, we can already do that - just expose symbols in your libraries and link them together.
That's looking from the outside in but consider looking at it from the end-user (read: attacker's) viewpoint.
The lack of memory protections that wasm currently has such as the lack of read-only memory or memory randomizations or anything else you really can't run any code with any level of assurance that it won't be taken advantage of. Typically a software engineer doesn't have to think about someone being able to rewrite a function during run-time or a statically defined variable because those memory protections are in place. It simply isn't allowed. However in wasm land, if you have a function that is say 'func isAdmin()' for instance you'd expect to return true if the logic is correct but without memory protections an attacker can force it to return true every time. This is one of the pretty serious features missing in wasm currently.
wasm has separate address spaces for code and data, and wasm code does not even have read access to the code space, much less writing to it. The call stack is also completely isolated and not addressable. So how exactly would malicious code running in a wasm sandbox "rewrite a function"?
It is only happening, because now we have startups with VC, trying to redo Java and .NET application servers, but now with Kubernetes and WASM containers they are supposed to be hip and trendy with plenty of YAML, not like the granddaddy's JVM and CLR based boring servers with XML all over place.
This plays out precisely as the blog post details with the split between WASI and Web Platform. Say you want to compile a Rust + C codebase to Wasm and run in the browser. You have three targets: wasm32-unknown-emscripten, wasm32-unknown-unknown, and wasm32-wasi. Emscripten is relatively old and not maintained. It'll work but you get some old JS that doesn't play well with newer stuff. wasm32-unknown-unknown has an ABI incompatibility which means you cannot interoperate between C compiled to Wasm and Rust compiled to Wasm. wasm32-wasi works, but now you have to have a WASI implementation in the browser and that's still very immature. Tools like wasm-bindgen or wasm-pack don't work with wasm32-wasi either.
Basically you have the Web Platform backend (wasm32-unknown-unknown) that plays well with the browser part, but does not play well with the interoperability across languages part. And you have the WASI backend (wasm32-wasi) that plays well with the interoperability across languages part but does not play well with browser part.