A few days ago, I decided I needed to have an idea of what was the state and promises of Rust/Wasm development.
And in order to have a clear idea of the soundness and strength of the foundations, I didn't want to deal with the advantages and problems of the various frameworks, I wanted to look at the real deal, Rust/Wasm without superfluous frameworks and libs.
So I decided to write the tic-tac-toe game in pure Rust/Wasm.
Before I go on with the journey, Here's the result.
Nothing fancy but it made me test what I wanted to test. It works. And if you open the network tab of your browser's developer tools, you can notice the files are rather small, and there's no polling or useless redraw.
Here's a simple example, though: wasm-bindgen without a bundler. It's only a "Hello World" but it sets us on track.
The basis is to use the wasm-pack cargo tool to build the wasm file. You'll use only three libraries:
js_sys wich gives access to standard build-in objects like the JSON parser, the binary arrays or the Math function (when in wasm, you don't have access to the standard Rust libraries, even the ones which can be sandboxed, because they would have to be embedded in the wasm file), and
web_sys which gives you access to the browser's world (including the DOM).
So, thanks to the example, I just wrote by hand the few files (no, you don't need a generator), that is the
Cargo.toml file and
This is enough. You just have to execute
wasm-pack build --target web
and voilà, you have a pkg directory containing your wasm file and a standard JS bootstraper.
Of course, just like a JS file doesn't make an application, a Wasm one doesn't. So I added a HTML file, and a CSS one (you should have a look at the HTML file.
From there, you just have to open your file through HTTP (after having configured the server to serve
.wasm files with the
application/wasm mime type. There's no need to publish to the npm repository, there's no need to use another packager, you can keep those kind of tasks for when your application really needs it and you know why, with the tool chain of your choice.
At this point I had just copied the code from the example, which was adding an element with "Hello World" as inner HTML.
The next step was to add the elements of a Tic-Tac-Toe game, the matrix.
Knowing the DOM well enough I had no difficulty finding the right methods and structs in web_sys (Exhibit 1: Element.
I could directly write the elements in rust. It was clean enough but you fast notice this interface gives you a lesser type system, as some methods return you some value which may, or not, be of the desired type (for example if you create a node with
Node you get may be, or not, a
HtmlElement depending on the tag.
This could easily be isolated in a tiny helper library, though, so this isn't a big deal.
Glancing at web_sys, you may notice the methods to add event listeners are also here.
Now it looks good, we just have to design how we will cleanly keep our application state around and have a normal event based web application continuously adding and removing elements and event listeners, deal with events, and so on.
Just the normal life of a web app, with the sanity of Rust, the perfect dream.
But it's not that simple.
- There's only one UI thread, and your event handlers are called on this thread.
- A Rust application isn't supposed to have a mutable static state anyway.
- You can't store your handlers behind an Arc, because the pointer to the JS isn't transferable between threads
- You don't have mpsc channels (in fact you don't have much of the std utilities)
So... There's no really clean solution.
I built something which is probably safe, and quite clear, but it involves ugly calls to
unsafe blocks to access the global state. I've put the active event handlers in this global state (which could be modularized).
Closure creation and boxing is ugly too (exhibit 2: BoardView).
Part of this problem could easily be isolated by a small framework, but the core isn't pretty.
And a big framework means bigger wasm files.
I hope some micro framework just for this problem will appear and be good enough without succumbing to the tentation of building a whole bag of template/binding/virtual-dom/etc fatness (some will need this one, of course, but we need a small strong core too).
From there I just had to add the necessary logic (which isn't long or complicated for such a game) and the "You win/lose" panel, then to add a "New Game" button, mostly to check I can add and remove buttons and their handlers without leaking memory.
And the game is done.
What do we gain
- We write in Rust, which has a nicer syntax. Union types, traits, patterns, sane generics, an ownership which avoids the overhead of a garbage collector (and you really don't want your wasm file to include a GC), and no null pointer
- We have a little better type safety
- A lot more is tested at compile time than if we were to use JS, TS or another wasm language
- Rust modularisation is much cleaner than anything standard in the JS world, both at the internal application level and when it comes to use external packages
What do we not gain
- Type checking isn't totally perfect, due to the interface with the browser. It's probably not much better than TypeScript at this point
- There are more runtime errors that what would be expected from a rust program (because it's not a rust program, it's a browser+rust program)
- There's no multithreading. Concurrency tools like the mpsc channels aren't available. Even mutexes are only partly usable. Yes I'm sure there will be tools "supporting" multithreading through webworkers but serialization-based message passing between agents is reserved to a smaller set of use cases. Rust will be the most apt to use safe multithreading when SharedArrayBuffers are back, but there's no clear direction or dates for that.
What do we not lose
- There's not much from the client-side JS ecosystem which will be missed. Tools will be easily made to replace them.
- building applications totally or partly based in Rust doesn't seem to me to really complexify the tool chain. And I'm not sure it's slower to compile Rust than to lint/transpile JS nowadays.
- files aren't big. Rust needs no runtime, needs no gargbage collector, so there's not much to add to the wasm files.
What do we lose
- Managing the state of your application and the handlers makes for ugly code.
- There's less liberty in the architecture of your application.
As I love Rust I'll probably write some of my next webapp frontends in Rust/Wasm but this will need a small framework (of my own if I don't find anything good enough) and it's not something I would recommend at this point to developers with no knowledge of Rust.
As you may have noticed, I wrote this with not much experience in Wasm (which was the point). I'd be really interested by your analysis and maybe corrections.
Top comments (0)