Why I’m dropping Rust
When I saw that there was a new systems level programming language, providing similar performance to C++ and no garbage collection, I was immediately interested. While I enjoy solving problems in GC languages such as C# or javascript, I had the nagging feeling that I was missing the raw power of C++. But C++ has so many footguns and other well-known problems, that I'd usually forgo it as an option.
So I dived in. And boy, did I dive deep.
Rust's ecosystem, since Rust itself is reasonably young, is still in a developing stage. For some use cases, websockets or serialization for instance, have quite well developed solutions that are popular. For other use cases, Rust is still lacking. One such use case would be an OpenGL GUI, like CEGUI or nanogui. In wanting to help the community and the language, I opted to port nanogui to Rust, purely in Rust, without using bindings to C/C++. The project can be found here.
The usual start in Rust is known as fighting the borrow-checker period. As any other, I also had a period where I was stumped at how to solve certain issues. Luckily, there's a great community in #rust-beginners who where willing to help me with my silly questions. It took me a couple of weeks, but I was starting to get the hang of the language.
What I didn't know though, was that when you hit a road-block, finding a solution feels like navigating a jungle. Oftentimes there are multiple answers that seem like the solution to your problem, but due to a minor detail, can't be used.
An example: suppose you have a base class Widget and want all your actual Widgets such as a Label or a Button or a CheckBox to have certain easily-shared functions. In languages such as C++ or C#, this is an easily solved problem. You create an abstract class or a base class depending on language and inherit that in your actual class.
public abstract class Widget {
private Theme _theme { get; set; }
private int _fontSize { get; set; }
public int GetFontSize() {
return (_fontSize < 0) ? _theme.GetStandardFontSize() : _fontSize;
}
}
In Rust, to do something similar to this, you have Traits. However, a trait has no knowledge of the underlying implementation. So a trait can define abstract functions, but can't access any underlying fields.
trait Widget {
fn font_size(&self) -> i32 {
if self.font_size < 0 { //compiler error
return self.theme.get_standard_font_size(); //compiler error
} else {
return self.font_size; //compiler error
}
}
}// playground
Let that sink into you. My first reaction to this was "Sorry, what?!". While there are valid criticisms against OOP, to offer this as a solution is just silly.
Luckily, I found out, there's a really good process to change the language through Requests For Change. I'm not the only one who thinks this is a severely limited part of the language and there's currently an RFC going to make it less silly. However, this has been going on at least since March 2016. Traits as a concept have been part of the language for numerous years. It's currently September 2016. Why is such an important and vital part of the language missing, still?
In some cases you can work around this by adding a function to the trait that is not implemented in the trait itself but on an actual object, then use that function to do your function with.
trait Widget {
fn get_theme(&self) -> Theme;
fn get_internal_font_size(&self) -> i32;
fn get_actual_font_size(&self) -> i32 {
if self.get_internal_font_size() < 0 {
return self.get_theme().get_standard_font_size();
} else {
return self.get_internal_font_size();
}
}
}// playground
But now you have a public function (trait functions act like an interface, it is currently impossible to mark a trait function as mod-only) that you still have to implement in all your concrete types. So you either don't use abstract functions and have a lot of code duplication or use the setup of the example and have slightly-less-but-still-too-much code duplication AND a leaky API. Neither is acceptable. Neither is an issue in established languages such as C++, C# and heck, even Go has a suitable answer.
Another example. For nanogui, and CEGUI uses this concept as well, each widget has a pointer to a parent and a vector of pointers to its children. How does this concept map to Rust? There are several answers:
- Use a naive Vec<T> implementation
- Use Vec<*mut T>
- Use Vec<Rc<RefCell<T>>>
- Use C bindings
Since the start of my journey, I have tried options 1 through 3 with several drawbacks, each making them not fit for use. I'm currently looking at point 4 as my only remaining option to use. Let me go over each of them:
Option 1
This is the option someone new to the language would choose. I chose this as the first option as well and immediately had problems with the borrow checker. Of course, using this implementation means that the Widget becomes the owner of the children AND the parent. This is impossible, since then a parent and a child would have a cyclic reference to owning each other.
Option 2
This is the option that I chose next. It offers the advantage that it's reasonably similar to the C++ style of nanogui. There are a couple of downsides, such as having to use unsafe blocks everywhere inside and outside of the library and not having the rust borrow checker check your pointers for validity. But the major breaking point of this option is that it is currently impossible to create a self-reference counting object. Note that I don't mean the equivalent of C++'s smart pointers or Rust's very own Rc type. I mean an object that counts how many times it's been referenced and deletes itself when this counter reaches 0. You can find an example in the C++ version of nanogui.
For this to work, you need to be able to tell the compiler to only allow deleting/dropping oneself from inside the object. Take the following Rust example:
struct WidgetObj {
pub parent: Option<*mut WidgetObj>,
pub font_size: i32
}impl WidgetObj {
fn new(font_size: i32) -> WidgetObj {
WidgetObj {
parent: None,
font_size: font_size
}
}
}impl Drop for WidgetObj {
fn drop(&mut self) {
println!("widget font_size {} dropped", self.font_size);
}
}fn main() {
let mut w1 = WidgetObj::new(1);
{
let mut w2 = WidgetObj::new(2);
w1.parent = Some(&mut w2);
} unsafe { println!("parent font_size: {}", (*w1.parent.unwrap()).font_size) };
}// playground
This gives the following output:
widget font_size 2 dropped
parent font_size: 2
widget font_size 1 dropped
This happens to not give a use after free error since presumably the memory is not set to 0 after delete, but the behaviour is of course undefined.
So to have a correctly working self-reference counting object, you need to allocate it somewhere globally. There's simply no way to tell the compiler to not drop a variable automatically when it goes out of scope.
Alright, I said. Have it your way, Rust. What IS the idiomatic Rust way to do a cyclical directed graph?
Option 3
After some searching I found a nice Rust library for creating trees called rust-forest. It neatly creates a way to have nodes, references to nodes using smart pointers and ways to insert/remove nodes. However, the implementation given in rust-forest doesn't allow nodes of different T types to be in the same graph, a requirement for a library such as nanogui.
To illustrate see this playground. It was a bit too long to include here fully, but the problem lies in this function:
// Widget is a trait
// focused_widgets is a Vec<Rc<RefCell<Widget>>>
fn update_focus(&self, w: &Widget) {
self.focused_widgets.clear();
self.focused_widgets.push_child(w); // This will never work, we don't have the reference counted version of the widget here.
}// playground
As an aside, the following is an odd thing that can be worked around, but I just don't see why this is a problem:
let refObj = Rc::new(RefCell::new(WidgetObj::new(1)));
&refObj as &Rc<RefCell<Widget>>; // non-scalar cast// playground
Conclusion
The problems I've encountered in option 1 through 3 leave me to believe that option 4, making bindings to C, would be the only working alternative for the library I'm trying to write. And now that I'm at this stage, I'm thinking, why write bindings to C when I could just as well be writing in C? Or C++?
There are some good things about Rust as a programming language. I quite like the way Match works. I like the idea behind traits much like the interfaces in Go, I like cargo as a packaging tool. But when it comes to the implementation details of traits, reference counting and impossible to overwrite behaviour of the compiler, I'm just forced to say: no. This will not work for me.
I sincerely hope that people continue to use and improve Rust. But I want to write games. Not wrestle with the compiler or having to write RFCs to make the language more favourable towards my goals.
If you’ve read the whole article so far and stuck with it, thanks for reading my rant. If not, there's no TL;DR here, sorry. There's too many concepts and solutions going on here that I can't condense it much further.
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!