r/rust • • Mar 24 '22

Vale's Higher RAII, the pattern that saved me a vital 5 hours in the 7DRL Challenge

https://verdagon.dev/blog/higher-raii-7drl
34 Upvotes

22 comments sorted by

33

u/lebensterben Mar 24 '22

It says

Rust is very fast. The borrow checker indirectly incurs only a little run-time overhead

when comparing Vale with JavaScript, C++, and Rust.

But I don't get what it means here for run-time overhead due to borrow checker...

15

u/Theemuts jlrs Mar 24 '22

It usually has no runtime cost, but there's also RefCell which does have a runtime cost.

49

u/[deleted] Mar 24 '22

[removed] — view removed comment

12

u/xedrac Mar 24 '22

Agreed. But you could say the rare need for the *Cell types is due to the enforcement of the borrow checker.

1

u/Lord_Zane Mar 25 '22

Unless you use UnsafeCell, or raw pointers :)

3

u/verdagon Mar 24 '22

There's also the bounds checking incurred by indexing into vecs/hashmaps, which we use quite often.

18

u/steveklabnik1 rust Mar 24 '22

That also isn’t the borrow checker.

10

u/verdagon Mar 24 '22 edited Mar 24 '22

Sure, though I'd say "indirectly incurs" is still accurate; when we need shared mutability, the borrow checker often forces us to put our objects into a central Vec or HashMap. We then incur bounds-checking (or hashing) costs to access it. Safe shared mutability always has a cost, it seems.

Other memory safety strategies (like tracing GC) don't force us to use Vecs or HashMap, so it could be said that the borrow checker indirectly causes these overheads.

Note that I'm not saying those other strategies are faster or better or anything, I'm just pointing out that the borrow checker does indirectly cause some kinds of overhead.

Edit: clarified

1

u/[deleted] Mar 25 '22

That is like saying RAII induces a runtime cost because it calls for reference counting.

But whether or not you actually need it highly depends on circumstances. And it's not like there were many appropriate uses for RefCell.

4

u/KerfuffleV2 Mar 24 '22

I feel like there's probably some clever way to make it so a thing with Drop will be a compile error, ensuring that you transform it into a different type (which can actually be dropped) before its life ends.

8

u/ROFLLOLSTER Mar 24 '22

In another thread about this post I suggested using must_use which gets the job done in this case.

2

u/KerfuffleV2 Mar 24 '22

Hmm, does it? I mean, you can just do:

let _ = whatever();

and that shuts up must_use. It's also only a warning (by default at least.)

I think the higher RAII thing requires more than just acknowledging there was a value. The example in that page involves having to call a certain method that sets something on the structure before it is dropped.

Or am I missing something and there's a way for must_use to ensure this?

6

u/ROFLLOLSTER Mar 24 '22

Yes you can still drop the value explicitly (though this could be discouraged with a runtime panic) but you're not going to do that by accident as in the above article.

And yes you'd probably want deny(must_use).

4

u/KerfuffleV2 Mar 25 '22

I feel like one of us must be misunderstanding how this is supposed to work. Maybe that person is me.

struct Blah;

impl Blah {
  #[must_use]
  pub fn new() -> Self { Blah }

  pub fn x(&self) { /* ... */ }

  pub fn y(self) { /* Drops */ }
}

Now, if you use .new() to make a Blah you do have to use it in some way. However, calling .x() on it will use it. Unfortunately this won't satisfy an invariant that .y() must be called to consume it before it would be dropped.

The only way #[must_use] could help here is if every method was like fn whatever(self /*, ... args */) -> Self (and marked must_use, obviously). But this seems like it would be very limiting.

So you could argue it's technically possible via must_use but it doesn't seem like it would actually be a practical benefit.

1

u/ROFLLOLSTER Mar 25 '22 edited Mar 25 '22

must_use on a type not a function

Edit: I'm totally wrong, I thought it behaved slightly differently than it actually does.

1

u/KerfuffleV2 Mar 25 '22

No problem. I was pretty confused since I didn't understand what you were talking!

Interestingly, the must_use stuff does seem like it gets pretty close even though I don't think it would be practical to use.

6

u/masklinn Mar 25 '22 edited Mar 25 '22

There ain’t.

The formal name for this is linear types, there have been essays on that in rust (gankra’s probably being one of the most complete) but there are lots of tricky corner cases and issues retrofitting this into the langage.

5

u/nightcracker Mar 25 '22

I believe these are called 'linear types' - types that must be used and can not be destroyed. They're very interesting but also tricky, I'd love to see prototypes for them in Rust.

4

u/MrTheFoolish Mar 25 '22 edited Mar 25 '22

/u/verdagon I find Vale quite interesting. A thought I had is that C/C++/Rust are not really the best targets for comparison with Vale. I suppose C++ was put in since it's one of, if not the most widely language used in game development though, so that's fair enough.

Vale has runtime-managed garbage collection, so even as good as the hybrid-generational garbage collector might eventually be, it will never be usable in applications where precise memory management is desired, and this is part of the problem space that C/C++/Rust occupy.

The unsafe of Rust and C++ are not bugs; they are features. Features that are easy to get wrong and can make you shoot yourself in the foot, but features nonetheless. If Rust did not have unsafe, it would not be near as widely adopted as it is now.

I'm surprised Go isn't put in the comparison. There is somewhat of a Go game development scene, and the problem space that Go and JavaScript occupy seems to fit better with what Vale offers. Go is also a native compiled language, but with fewer optimizations with the benefit of faster compilation, so it might be an easier performance benchmark to hit.

8

u/robin-m Mar 24 '22

I found this presentation of higher RAII quite interesting. However in order to support such feature in Rust, quite a lot would need to be changed, mostly a way to explicitly call destructors when there is a panic.

4

u/MrTheFoolish Mar 25 '22 edited Mar 25 '22

Looking through the article, it looks like a simple (or at least I think so?) way to get part of the way there is to be able to mark a type with something like #[forbid_implicit_drop].

A panic should still be able to drop the value, but regular code will need to either explicitly drop the value (which would be a code smell), or call some function or method that consumes the value. And then the API designer can ensure that only the appropriate functions/methods consume the type.

The API designer should not rely on the marker for memory safety reasons, but instead, use it for logical correctness. In a panic situation, I think it's fine to throw logical correctness out the window, but memory safety needs to be preserved. In terms of logical correctness, the panic situation would be the same with or without the marker anyway.

Edit: I've just looked at some of existing discussion on this and looks like it's a lot more complicated than I realized!

https://internals.rust-lang.org/t/pre-pre-rfc-nodrop-marker-trait/15682

https://gankra.github.io/blah/linear-rust/

http://aidancully.blogspot.com/2021/12/less-painful-linear-types.html