Table of Contents
- @fanf March 2022
In the BIND meeting yesterday, Vicky asked me what I thought about using Rust in BIND. I ummed and ahhed a bit negatively, so I thought it might be worth writing some more coherent notes.
The tl;dr is it would require a great deal of enthusiasm for Rust amongst the BIND developers, and careful consideration of where the benefits might be worth the effort and the added build system complexity.
In the sections below I discuss each of Rust's claimed advantages, and where to draw a boundary between C and Rust so that we don't lose more than we gain. I'm mostly writing from the point of view of replacing rbtdb or other core data structures in BIND, which I think is not a good fit for Rust; I have not made much effort to look for areas that might fit more comfortably.
It might be best to keep this discussion low key, lest the Rust Evangelism Strike Force get the wrong end of the stick and cause a massive distraction smile
concurrency
One of Rust's selling points is "fearless concurrency", which largely means that safe Rust code is free from data races. (An attractive offer for a higly concurrent and threaded program like BIND!) This guarantee is enforced by Rust's borrow checker, which imposes rules on which data in the program is mutable or shareable.
Rust's core data structures provide APIs that use features of Rust's type system (lifetime and mutability annotations) to tell the borrow checker how the data structure can be used safely. The internals of these data structures are often implemented in unsafe Rust, but the unsafe parts are carefully written to implement the guarantees that their APIs advertise.
(Unsafe Rust is necessary for things like calling foreign functions such as malloc(), and sometimes for performance reasons such as control over when memory is initialized.)
Because Rust's fearless concurrency depends on its type system, it does not extend across a foreign function interface where C calls Rust. But it is possible for a C program to benefit from Rust's support for parallel programming, if the parallelism is all on the Rust side of the boundary and exposed to C in a simple way. As a consequence it probably only makes sense for relatively large subsystems: a real world example is the parallel CSS layout engine in Firefox.
perfomance
Rust, like C++, likes to talk about "zero cost abstractions": a library author can provide a generic interface that is specialized for each use. A basic example is something like qsort(), which in C relies on slow indirect function calls to support generic array element types; whereas in C++ and Rust the comparison function can be inlined and optimized for sorting. C qsort() also relies on untyped pointers, where as C++ and Rust are fully typed.
Rust has some advantages over C++: its "traits" provide more direct type checking for generics, allowing Rust to provide better error messages than C++ can. (C++ "concepts" are still some way off.) Rust makes fewer promises about data layout than C and C++, so the compiler is able to tune a data structure for each specific use. A nice example is that Rust doesn't need a specific HashSet type: you can just use HashMap<T,()> because () is a zero-sized type (something C and C++ don't have) so Rust can automatically optimize the HashMap to have the same compact layout that a HashSet would. And Rust uses its knowledge of mutability and sharing to move data around when that makes sense, with more freedom than is sensible in C and C++.
To benefit from zero cost abstractions, a data structure and its key type need to be implemented in the same programming language so that they can be optimized together. (This is perhaps less important for the value type, if it is OK to store a pointer to the value instead of placing the value inline.) In a DNS server domain names are one of the most important types, being heavily used in the hot paths and as a lookup key in many places. So for a DNS server to benefit from a data structure written in Rust, names must be able to cross the language boundary without friction, so that the potential performance gains aren't eaten by an impedance mismatch.
safety
Safety is a combination of aiming for correctness, and failing gracefully when the code is inevitably not 100% perfect.
A nice property of data structures is that they are easy to test very thoroughly. John Regehr has a good tutorial on fuzzing data structures, and I have used similar techniques with great success. Another technique that he and I like (but not mentioned in the tutorial) is differential testing against another implementation, ideally one that is known to be good. (If not, one that is likely to have different failure modes!)
When I was testing my copy-on-write DNS-trie, I used mprotect() to ensure that read-only pages were not written: too slow for production code, but useful reassurance for testing. That would be unnecessary in Rust because of its borrow checker, but the other tests are necessary whatever language the code is written in.
This is enough for me to be reasonably confident that a data structure written in C is correct, though it probably needs souping up to be good enough for parallel code.
In contrast, protocol parsing code is much more safety-critical, since it is exposed to untrusted inputs, and is responsible for establishing the invariants that data structure code relies on. BIND9's history in this area is fairly typical for a C program (i.e. not great) which suggests there would be some benefit from using a safer programming language instead. But I think there are bigger payoffs to be found here than just safety: I would like a simple DNS record specification language that can be used to automatically generate parsing, serialization, and user interface code. But that's a different discussion!
conclusion
I think at the moment, with my level of knowledge of BIND and Rust, it's hard for me to see that there will be benefits compelling enough to justify the effort. Plus it's a non-starter without lots of enthusiasm for Rust amongst other BIND dev team members.
And I have not dicsussed the implications for building BIND, and platform support, and other things that have caused consternation in the Python world (https://lwn.net/Articles/845535/) and in the Gnome world (https://lwn.net/Articles/771355/).