Rust 2020: Rust at Wasmer

Mark McCaskey
Wasmer
Published in
9 min readNov 29, 2019

--

This article is a response to the call for blog posts about Rust in 2020 and beyond. We’ll overview how Rust development in 2019 has impacted us as a small company using a lot of advanced Rust features for our WebAssembly runtime and tools, what we’re thankful for in Rust this year, the problems we face, and what we would like to see in the future.

👍 The good

We love Rust at Wasmer! In fact, we were a diamond sponsor at this year’s RustConf because Rust is such a key part of what we do and we think it’s extremely important for companies to give back to the Free and Open Source software projects that they rely on.

The Rust language over the past year has made some incredible improvements that have had a very real impact on the quality of what we’ve been able to create. Rust 2018 has been a significant improvement in language simplicity and intuitiveness.

Here are some of the new features we appreciate the most.

Non-lexical-lifetimes

This significantly improved ergonomics and allowed for better error messages. The inline reborrowing removes the need to use a lot of extra levels of nesting.

For example, if we wanted to feed a dog we used to have to explicitly tell Rust (through lexical scope) that we were done with the dog’s mouth after feeding and that we were done with the dog’s biographical information after looking at it before we could compliment the dog:

{
let mouth = dog.get_mouth_mut();
if mouth.is_hungry() {
mouth.give_food();
}
} // drop mutable borrow to mouth
let message = {
let info = dog.get_info();
format!("{}, you're a good {}!", info.name, info.gender)
}; // drop immutable borrow to info
tell_dog(&mut dog, message);

But with NLL, we can now more directly interact with our furry friends:

let mouth = dog.get_mouth_mut();
if mouth.is_hungry() {
mouth.give_food();
}
let info = dog.get_info(); // automatically drops mutable borrow
let message = format!("{}, you're a good {}!", info.name, info.gender);
tell_dog(&mut dog, message); // automatically drops immutable borrow

The end result that is that it’s much quicker to write Rust and have things “just work”.

The dbg! macro

The dbg!macro allows for inline inspection of intermediate values. It works on any expression that has a result which is Debug. It’s a very small detail, but a real quality of life improvement.

For example if we have an expression that’s long but makes sense as a single expression, like:

let x_plus = -1 * b + sqrt(b * b + 4 * a * c) / (2 * a);

But our intermediate values aren’t as we expect and we want to quickly inspect them then we used to have to make significant structural edits that we’d have to undo before we could ship the code.

let b_square = b * b;
let four_ac = 4 * a * c;
let sqrt_result = sqrt(b_squared + four_ac);
let rhs = sqrt_result / 2 * a;
let final_result = -1 * b + sqrt_result;
println!("b * b = {}; 4 * a * c = {}; sqrt(b * b + 4 * a * ) = {}; sqrt(b * b + 4 * a * c) / (2 * a) = {}; -1 * b + sqrt(b * b + 4 * a * c) / (2 * a) = {}", b_square, four_ac, sqrt_result, rhs, final_result);

And this is strictly worse because it doesn’t include file and line information and the text describing what the values are doesn’t stay in sync with the actual code that’s computing them.

However now we can do:

let x_plus = dbg!(-1 * b + dbg!(sqrt(dbg!(b * b) + dbg!(4 * a * c)) / (2 * a)));

Which is way cleaner, easier to use, easier to delete, and easier to detect as development-only debug code. It was always possible to implement something like dbg! and then use it from an external crate, but having something like this always available — even in throw-away example code — is a game changer for quickly iterating on Rust code and I miss it every time I use a language that’s not Rust.

Various stdlib improvements

Some of the biggest for us this year are the stabilization of to/from_*e_bytes, Pin, TryFrom, and atomic integers as well as the addition of new features like MaybeUninit. We won’t go into the details here, but they let us be more precise about what we want and make getting things done easier.

Cargo quality of life features

Cargo is extremely nice. When it works, it does exactly what you’d expect and gets out of your way — and it usually works great! We’ll highlight two new features of Cargo that we’re especially thankful for this year:

The --offline flag allows cargo to operate well with dependencies even without an internet connection. This doesn’t come up often for us, but we’re extremely glad it exists when we do need it.

The default-run field. This allows multiple binary targets in a Cargo workspace without forcing the user to specify which one should run when using cargo run. This is a small detail, but it’s had a big impact for us. It’s easy to forget that your project has other binary targets, especially if they’re small utilities that we don’t normally interact with but are important to have. When working on projects like that, I often forget to specify the target which results in a consistent loss of at least 1–10 seconds every time it happens due to having to reorient to the error if I’ve changed tabs to do something else and having to edit the previously run command. Small paper-cuts like these can multiply together and ruin my focus causing my productivity to plummet.

The Rust language and ecosystem’s attention to detail for things like this really make Rust a pleasure to use.

👎 The “bad” — What can be improved?

There weren’t any changes in 2019 or in the 2018 edition that were bad for us, there are only things that haven’t been improved as much as we would have liked yet:

Some error messages with lifetimes

We ran into some lifetime issues recently when upgrading a dependency. In an extremely long function, stable Rust was not pointing to the cause of issue making it nearly unresolvable. Fortunately, by using the nightly compiler and #![feature(nll)] we were able to get the specific and detailed error messages that we’re used to from Rust.

Lack of mutually exclusive features in Cargo

This is a surprisingly pervasive issue — it affects us both directly and indirectly. It would really make things easier if there were two types of features: standard features and mutually exclusive features/sets of features.

The most common use case we have is something that an enum would solve very cleanly: there are multiple options but it only makes sense to use one and exactly one of them.

However looking at it generally, it would be nice to encode more of the information about how features can and cannot be used in the Cargo manifest directly, instead of in cfg logic that enables and disables calls to compiler_error!.

What we want to see going forward

Our team is made up of people with very different amounts of Rust experience so we’re able to offer the perspectives of a new user from a web-development background, a new user from a C++ background, and a user since the beta with a functional programming background:

Rust from the perspective of a web developer

Cargo and crates.io are an amazing resource. I find it’s extremely helpful for new developers to have a way to hit the ground running building cool things, and a great package ecosystem can definitely encourage that. I also feel like crates on crates.io will be an amazing resource for Rust and WebAssembly, for building complex Wasm modules by utilizing or porting existing crates.

However, Rust source code is really hard to read. The amount of syntax and special characters can make lines feel cluttered and the language can seem overwhelming. Additionally, because Rust is positioned as a language to use for WebAssembly, many web developers may be trying it out without a background in systems programming. The distinction between things like arrays, vectors, and boxes is confusing. Therefore, Rust could focus on making the language easier to use for developers from these backgrounds.

Rust from the perspective of a C++ developer

People often say that Rust is like C++, but it’s more like OCaml than it is like C++. I really like the Rust ecosystem: cargo and crates.io are fantastic. And Rust has some great features like pattern matching.

Due to the nature of the work we do (generating machine code and running it) we have to use a lot of unsafe. Rust doesn’t just need a better understanding of the possible effects of unsafe, it needs better runtime tools for debugging it. Aside from the obvious cases where we need unsafe, we run into more issues than it seems like we should where we need unsafe just to do what we want.

The Send + Sync traits work for guaranteeing thread safety, but in the context of executing WebAssembly it can be restrictive. For example, we don’t know if the module we’re compiling will have shared memory statically or not and the solutions are either pay runtime costs or solve the problem with unsafe.

Another issue is that Rust can’t easily map well to patterns used in C++ and specifically used in LLVM. When attempting to implement mixins in Inkwell, due to some of the overlap in functionality between the traits, we weren’t able to implement this in Rust in a sensible way. The way that this works in C++ is by using multiple inheritance. Another feature that would be nice to have is an unsafe way to catch or at least throw an exception from Rust for FFI purposes.

Rust from the perspective of a Rust and FP developer

Rust has improved so much during my time using it. The way the language has been developing is extremely exciting and it’s impossible for me to pick just one thing I’d like Rust to focus on, so I’ll limit myself to talking about a few things here and talk about how they would help improve Rust:

  • Standard map!, and set! macros that act like vec!. This would be like a generic version of the macros in the maplit crate, but in the stdlib. I’m a big fan of the small stdlib that Rust has, but for day to day programming these core data structures are so important (a fact proven by their inclusion in the stdlib) that being able to concisely and ergonomically construct them will encourage a more obviously correct, readable, and composable style.
  • Deeper abstractions. The biggest feature I miss from Haskell are the core traits that define so little, yet by which extremely powerful and abstract things are built. For example, I wish I could write functions that return impl MonadFail and have the caller decide how they want to receive their errors: as an Option, as a Result, as an empty data structure, or as anything else a user may want for their own types. These types of abstractions improve code reuse, ergonomics, and productivity.
  • Safer unsafe. For almost the entire time I’ve used Rust, I essentially never needed to use unsafe. However since joining Wasmer, I’ve written a lot of unsafe code. When dealing with the complexities of FFI, existing libraries, and when trying to expose a safe, sound API over a large system that does some fundamentally unsafe things, it’s very hard to be confident that everything is correct. I don’t know exactly what the solution to this is, but it would be nice if there were levels of unsafe or tools for auditing the safety of unsafe code.

Rust strikes a good balance between being easy to use and safe/expressive and it’s important that we preserve that. In general, I think Rust is doing a fantastic job overall but there’s always more to improve. One last thing I hope to see in 2020 is… myself contributing to Rust more seriously!

Wrapping it up

Rust 2018 was a success for us and we’re looking forward to seeing how Rust develops going forward. Knowing that the language and ecosystem is constantly improving gives us confidence that no matter how hard the technical challenges we face are, Rust is growing with us and will help us get things done.

Thank you to the Rust teams and all the contributors for your hard work this year!

--

--