Hacker News new | ask | show | jobs
by quietbritishjim 2612 days ago
I know very little about Rust or Wayland (I suppose I'm not the target audience) but I got lost very quickly here:

> A Wayland “output” is the resource that represents a display device. Commonly this means it handles a computer monitor. This resource could disappear at any time in the life cycle of the application. This is easy enough to imagine: all it takes is a yank of the display’s power cord and the monitor goes away.

Surely even if a physical monitor is connected from a computer, the object representing it doesn't instantly go away? If it literally got freed as soon as the user disconnected the monitor then any access to such an object would be dangerous as you could be accessing an object after it's freed, or even another valid display object that got allocated into that space in the mean time.

Instead, I would expect an object in that situation to go into some error state, and even that might only be picked up when you perform certain operations. If that were the case, I don't really see how Rust lifetimes are a problem.

Since this was the main summary of the problem for laypeople like me, it made the rest of the article quite hard to follow.

3 comments

I think you missed this part:

> [the resource handle] can only be dropped between event callbacks, wlroots/Wayland is callback based

So "at any time" means between any two user callbacks, but not during them.

Error states really aren't a solution IMO, as they tend to pollute the entire interface in that methods that would be expected to work unconditionally can now fail or panic. Either you have to check each method invocation (entirely unnecessarily, since if a resource isn't in an error state at the start of your callback it won't go into one during it) or you make the absence of error state a precondition, in which case you're guaranteed to miss one or two cases and panic when the user yanks out the cord.

> > [the resource handle] can only be dropped between event callbacks, wlroots/Wayland is callback based

Thanks, that certainly makes a hell of a lot more sense.

Picture a pointer to video memory. Or, simpler, picture a pointer to an SHM section that either side of the SHM IPC conversation can deallocate. From both sides’ perspective, that pointer is probably implemented as both an SHM section, but also an SHM pointer to the section, such that either side can set the SHM pointer to NULL, and then (if they managed to do that) proceed to tell the SHM infrastructure to unmap all mappings of the SHM section.

When this sort of structure is used, there are certainly going to be guarantees in place (probably by using some sort of session/transaction functions that compare-and-swap an atomic SHM spinlock controlling the SHM pointer) such that the SHM pointer won’t go NULL [and the thing it points to won’t be deallocated] in the middle of either side writing to it. But those functions aren’t actually consuming the resource and spitting out a new temporary one (i.e. a “handle” to the resource, as the article’s author implemented in their Rust wrapper library originally.) Instead, they’re just functions that block either side from writing to the pointer as long as you’re in them—sort of like disabling interrupts in a critical section.

How do you model the ownership of such a C-FFI-runtime-guarded IPC SHM volatile pointer-to-pointer, in Rust? Is there an idiomatic translation for it?

Because this sort of thing comes up all the time in the context of kernel handles to buffers, and I would be surprised if the folks writing OSes in Rust haven’t hit on it before.

IIRC, there’s an IPC abstraction called a ‘blackboard’ (sort of related to a tuple space) that is the generalization of this SHM model, so it might also help to ask how you’d model an IPC ‘blackboard’ in Rust.

Quick answer, without understanding all the details: a weak-pointer-like structure that performed all the necessary locking and checking before giving out access to the underlying SHM.

Rust's borrow checking is more or less a formalization of C++'s RAII style; in my experience, solutions for that translate relatively simply.

Rust's borrow checking does not correspond to RAII. A simplified explanation is that borrow checking gives you the guarantee that mutable references are unique and immutable references do not change.

Rust's lifetime system is vaguely like RAII, but the C++ type system gives you no way to create an object with lifetimes that don't correspond to some scope. In Rust this is done by moving values, but in C++ this is not possible, you have to fake it by using std::move(), which really just creates an rvalue reference.

One of the big things that causes problems in C++ is iterator invalidation. This is not solved with RAII, but it is solved in Rust with the borrow checker. The price you pay is that iterators in Rust are strictly less powerful than iterators in C++, because the way C++ iterators work cannot really be expressed in the Rust type system. (In short, C++ lets you have as many iterators as you like into the same container, and defines ranges as pairs of iterators. Some algorithms are more naturally expressed this way.)

> C++ lets you have as many iterators as you like into the same container

While it's true that Rust doesn't let you have multiple mutable iterators, it's worth pointing out that you can certainly have multiple immutable iterators.

    let items = &[42, 101];

    // All products (a*a, a*b, b*a, b*b)
    for x in items {
      for y in items {
        println!("{}", x * y);
      }
    }

    println!("");

    // Pairwise products (a*a, b*b)
    let pairs = Iterator::zip(items.iter(), items.iter());
    let pointwise_products = pairs.map(|(x, y)| x * y);
    for product in pointwise_products {
      println!("{}", product);
    }
That's really not what I'm talking about. In your example you are using multiple iterators to iterate over the same structure multiple times, but in C++ you can use pairs of iterators to represent ranges. For example, I can have three iterators i, j, and k, which represent two ranges: i..j and j..k.

Even if i, j, k are const iterators, it's not pleasant to translate this to Rust. So it's not an issue of whether Rust lets you have multiple iterators or not, the issue is that Rust iterators are strictly less expressive than C++ iterators.

And that's okay. It's a tradeoff.

For more information about how iterators are used in C++, I would refer to the <algorithms> portion of the standard library. https://en.cppreference.com/w/cpp/algorithm

Ah, I understand. You're referring to how iterators in C++ are generalized indices into their collections. Certainly, Rust iterators have a more constrained purpose.

I personally prefer having separate constructs for iteration and indexing, so I think it's a matter of taste.

Is that different from something like this?

  fn main() {
      let list = &[0, 1, 2, 3, 4, 5, 6, 7, 8];
      let i = 0;
      let j = 4;
      let k = 8;
      let iter_ij = list[i ..= j].iter();
      let iter_jk = list[j ..= k].iter();
      let pairs = Iterator::zip(iter_ij, iter_jk);
      for (a, b) in pairs {
          println!("{}, {}", a, b);
      }
  }
> but the C++ type system gives you no way to create an object with lifetimes that don't correspond to some scope.

Counterexample: a data member of a class has a lifetime tied to the lifetime of the containing object instance, rather than to a lexical scope.

You are just using the same storage duration as the containing object instance, and the C++ type system only gives you any guarantees for automatic, static, and thread storage duration. These all correspond directly to lexical scopes. The type system does not have a way to express dynamic storage duration, but Rust does because Rust has move semantics in the type system. C++ has move semantics, but they are runtime semantics built into the implementation of the library and not part of the type system, so you get no compile-time checking that you used move semantics correctly.

Phrased another way, I'm just saying that the C++ type system only expresses two types of storage durations: those tied to lexical scopes and dynamic storage durations, and provides no checks for dynamic storage durations.

Yes that's the real solution to the problem.

If you have to access something in lots of code, don't make it go away asynchronously. There are many techniques to archive that. Keeping objects around in an error state is one easy way to do that.

You never want memory management to be too fine grained. Even if he could implement the fine grained memory management it would likely be impossible to test all the corner cases.