Hacker News new | ask | show | jobs
by derefr 2613 days ago
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.

1 comments

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);
      }
  }
Yes, that is different, because in C++ you can move j forward and backwards.
> 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.