📜 ⬆️ ⬇️

Why I refused to Rust


When I learned that a new system-level programming language had appeared, with a performance like C ++ and without a garbage collector, I immediately became interested. I like to solve problems using languages ​​with garbage collectors, like C # or JavaScript, but I was constantly tormented by the thought of the raw and crude power of C ++. But in C ++ there are so many ways to shoot myself in the foot and other well-known problems that I usually did not dare.


So I got into Rust. And, damn it, got deep.


Rust is still quite young, so its ecosystem is still in its initial development stage. In some cases, for example, in the case of web sockets or serialization, there are good and popular solutions. In other areas, Rust is not so good. One such area is the OpenGL GUI, such as CEGUI or nanogui . I wanted to help the community and the language, so I started to port nanogui to Rust, with the code in pure Rust, without bundles with C / C ++. The project can be found here .


Usually, acquaintance with Rust begins with the struggle with the idea of ​​borrow-checker. Like other programmers, I also had a period when I could not figure out how to solve this or that problem. Fortunately, there is a cool community in # rust-beginners . Its inhabitants helped me and answered my stupid questions. It took me a few weeks to feel more or less comfortable in Rust.


But I did not realize that when faced with a problem, the search for a solution is similar to the orientation in the jungle. Often there are several answers that are similar to solving your problem, but not suitable because of the tiny details.


Here is an example: Imagine that you have a base Widget class and you want the widgets themselves (Label, Button, Checkbox) to have some common, easily accessible functions. In languages ​​like C ++ or C #, this is easy. You need to make an abstract class or base class, depending on the language, and inherit your classes from it.


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, you need to use traits for this. However, the type knows nothing about the internal implementation. The description may define an abstract function, but it does not have access to internal 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 } } } 

» Run in an interactive sandbox


Think about it. My first reaction was "Um, what ?!" Of course, there is a fair criticism of the PLO, but such a decision is just ridiculous.


Fortunately, it turned out that the language is changing and improving with the help of Requests For Change, and this process is well established. I’m not the only one who believes that such an implementation severely limits the language, and now there is an open RFC designed to improve this nonsense. But the process has been going on since March 2016. The concept of types has existed in many languages ​​for many years. Now - September 2016. Why is such an important and necessary part of the language still in a deplorable state?


In some cases, you can bypass this restriction by adding a function to the type, which is implemented not in the type, but in the object itself, and then use it to refer to the real function.


 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(); } } } 

» Run in an interactive sandbox


But now you have a public function (the type functions behave as an interface, and now there is no way to mark the type function as mod-only), which still needs to be implemented in all specific types. So you either do not use abstract functions and duplicate a bunch of code, or use the approach above and duplicate a little less, but still too much code. And you get a leaky API. Both outcomes are unacceptable. And there is no such thing in any of the established languages ​​like C ++, C # and, damn, even in Go there is a normal solution .


Another example. In nanogui (in CEGUI, this concept is also used) each widget has a pointer to a parent and a vector of pointers to its descendants. How is this implemented in Rust? There are several answers:


  1. Use Vec<T> implementation
  2. Use Vec<*mut T>
  3. Use Vec<Rc<RefCell<T>>>
  4. Use C bindings

I tried methods 1, 2, and 3, each had disadvantages that made their use unacceptable. Now I am considering option 4, this is my last chance. Let's take a look at all the options:


Option 1


This option will choose any newcomer Rust. I did so, and immediately ran into problems with the borrow checker. In this embodiment, the Widget must be the owner of its descendants AND the parent. This is not possible because the parent and the child will have circular references to each other’s possession.


Option 2


This was my second choice. Its plus is that it is a march on the C ++ style used in nanogui. There are several drawbacks, for example, the use of unsafe blocks everywhere, inside and outside the library. In addition, the borrow checker does not check pointers for validity. But the main disadvantage is that it is impossible to create a counter object. I do not mean the equivalent of a smart pointer from C ++, or the type Rc from Rust. I mean an object that counts how many times it has been pointed at and deletes itself when the counter reaches zero. Here 's a C ++ example from the nanogui implementation.


For this thing to work, you need to tell the compiler that you can only delete yourself from inside the object. Take a look at an 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) }; } 

» Run in an interactive sandbox


The output will be:


 widget font_size 2 dropped parent font_size: 2 widget font_size 1 dropped 

This is necessary so that the use after free error does not appear, because the memory is not reset to zero after deletion.


So for the correct implementation of such a counter, you need to reserve memory globally. There is simply no easy way for the compiler to not automatically remove a variable when it goes out of scope.


Oh well. Do as you know, Rust. What is the way to implement a cyclic directed graph is idiomatic in Rust?


Option 3


In the end, I found a good library to create trees, which is called rust-forest . It allows you to create nodes, point to nodes with smart pointers, and insert and delete nodes. However, the implementation does not allow adding nodes of different types T to a single graph, and this is an important requirement of a library like nanogui.


Take a look at this interactive example . It is a bit long, so I did not add a full listing directly to the article. The problem with this feature is:


 // 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. } 

» Run in an interactive sandbox


By the way, this strange thing can be circumvented, but I still do not understand why this is a problem at all.


 let refObj = Rc::new(RefCell::new(WidgetObj::new(1))); &refObj as &Rc<RefCell<Widget>>; // non-scalar cast 

» Run in an interactive sandbox


Conclusion


The problems that I encountered in the implementation of methods 1, 2 and 3, suggest me that the fourth option with a bunch of C is the only way that is suitable for my task. And now I think - why make a bundle with C, when you can just write everything in C? Or C ++?


The programming language Rust has positive features. I love how Match works. I like the general idea of ​​types, like the interfaces in Go. I like the cargo package manager. But when it comes to implementing details of types, reference counting and the inability to redefine the behavior of the compiler, I have to say no. It does not suit me.


I sincerely hope that people will continue to improve Rust. But I want to write games. Instead of trying to defeat the compiler or write RFCs to make the language more suitable for my tasks.


Translator's Note


I did not understand what the author means when he says “ to correctly implement such a counter, you need to reserve memory globally, ” as if this behavior were atypical for other languages, in particular C and C ++. They also need to put a variable in dynamic memory if you want to save it after the function is completed, right?


In addition, “ there is no simple way for the compiler to not automatically delete a variable when it goes out of scope ” —it seems to be just a false statement, because the std :: mem :: forget function was created specifically for this (from the discussion on the editor).


Good article discussions:



')

Source: https://habr.com/ru/post/309968/


All Articles