Smart pointers for the kernel
Rust has a plethora of smart-pointer types, including reference-counted pointers, which have special support in the compiler to make them easier to use. The Rust-for-Linux project would like to reap those same benefits for its smart pointers, which need to be written by hand to conform to the Linux kernel memory model. Xiangfei Ding presented at Kangrejos about the work to enable custom smart pointers to function the same as built-in smart pointers.
Ding showed the specific "superpowers" that built-in smart pointers have in his slides: unsizing and dynamic dispatch. Unsizing allows the programmer to remove the length of an array behind a pointer from its type, turning a Ptr<[T; N]> (bounds-checked at compile time) into a Ptr<[T]> (bounds-checked at run time). This needs special support because slices (values of type [T]) do not have a known size at compile time; therefore the compiler needs to store the size somewhere at run time. The compiler could store the size in the pointed-to allocation, but that would require reallocating the array's memory, which would be expensive. Instead, the compiler stores the size alongside the pointer itself, as a fat pointer. On nightly Rust compilers, users can enable an experimental feature and then have their pointer type implement CoerceUnsized to indicate that it supports that.
![Xiangfei Ding [Xiangfei Ding]](https://static.lwn.net/images/2024/xiangfei-ding-small.png)
The second superpower is called DispatchFromDyn and allows converting a Ptr<T> into a Ptr<dyn Trait> when T implements Trait. This has to do with the way that Rust implements dynamic dispatch — a value of type Ptr<dyn Trait> uses a dispatch table to find the implementation of the method being invoked at run time. That method expects to receive a self pointer. So converting a smart pointer to use dynamic dispatch only works when the smart pointer can be used as a self pointer.
These features are both experimental, because the Rust project is still working on their design. Ding explained that there is an RFC aimed at stabilizing just enough for the Linux kernel to use, without impeding the development of the features. The RFC would add a new macro that makes it trivial for a smart pointer satisfying certain requirements to implement the necessary traits, no matter what the final forms of the traits end up looking like. That would let the kernel start using its custom smart pointers on stable Rust sooner rather than later.
There is one catch — implementing these features for a smart-pointer type with a malicious or broken Deref (the trait that lets a programmer dereference a value) implementation could break the guarantees Rust relies on to determine when objects can be moved in memory. This is of particular importance to Pin, which is a wrapper type used to mark an allocation that cannot be moved. It's not hard to write smart-pointer types that don't cause problems, but in keeping with Rust's commitment to ensuring safe code cannot cause memory-safety problems, the RFC also requires programmers to use unsafe (specifically, implementing an unsafe marker trait) as a promise that they've read the relevant documentation and are not going to break Pin. With that addition, the code for a smart-pointer type would look like this:
// Use Ding's macro ... #[derive(SmartPointer)] // On a struct that is just a wrapper around a pointer #[repr(transparent)] struct MySmartPointer<T: ?Sized>(Box<T>); // Implement Deref, with whatever custom logic is needed impl<T: ?Sized> Deref for MySmartPointer<T> { type Target = T; fn deref(&self) -> &T { ... } } // And then promise the compiler that the Deref implementation is okay to // use in conjunction with Pin: unsafe impl<T: ?Sized> PinCoerceUnsized for MySmartPointer<T> {}
Andreas Hindborg asked for some clarification about why the marker trait is needed. Deref is supposed to be simple, Ding explained. Usually, someone writing a smart-pointer type would have a normal pointer stored in their type; when implementing Deref, they can just use the normal pointer. But it's technically possible to implement something more complicated than that. In this case, you could have a Deref implementation that actually moves data out of the object pointed to and stores something else there. This would not normally be a problem, except when the smart pointer is contained in a Pin, which is supposed to prevent the value from being moved. If the Deref implementation moves the value anyway, then that would be undefined behavior. The unsafe marker trait is a promise to the compiler that the programmer has not done that.
The new macro is available on nightly Rust, although Ding says that it needs a bit more testing in order to stabilize, as well as some additional documentation which he is working on. Miguel Ojeda asked how soon the macro might be stabilized; Ding answered that it should be quite soon. He will make a stabilization report shortly, and then it is just a matter of checking off the requirements.
Index entries for this article | |
---|---|
Kernel | Development tools/Rust |
Conference | Kangrejos/2024 |
Posted Oct 5, 2024 16:11 UTC (Sat)
by tialaramex (subscriber, #21167)
[Link] (2 responses)
I implement Deref and DerefMut for misfortunate::Double which (the whole point of the misfortunate crate) does something that's legal in safe Rust but is probably not what you wanted. in this case although dereferencing a Double<T> gets you access to a T, *mutably* dereferencing it gets you a different T! There are two inside it (hence the name) but they appear to be singular from outside.
I don't know whether it would be safe for this to have the marker or not, but because it's unsafe I know I can just not implement it and never need to know.
Posted Oct 6, 2024 15:13 UTC (Sun)
by daroc (editor, #160859)
[Link] (1 responses)
Posted Oct 8, 2024 0:25 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link]
But if neither of those issues were present, this would be entirely fine since Double::swap() takes a &mut self parameter, and you only violate this invariant if you re-seat the pointer without calling such a method. Pin does not allow borrowing either the pointer or the pointee mutably (unless the pointee is Unpin), so it would prevent you from calling swap(), and everything would be entirely sound.
Posted Oct 6, 2024 7:54 UTC (Sun)
by smurf (subscriber, #17840)
[Link] (20 responses)
With Rust you add funky semi-comprehensible macros to your classes to tell the compiler which invariants your class and/or pointers to its members requires (and obeys). You don't do that? people will have problems using your class.
With C you write simple and legible code (or what looks like such) and document the invariants in comments or documentation (or not). Your users can walk right over them and nobody cares — until the kernel BUGs on you, that is. Or worse.
Posted Oct 6, 2024 13:38 UTC (Sun)
by tux3 (subscriber, #101245)
[Link] (18 responses)
Posted Oct 6, 2024 21:11 UTC (Sun)
by kleptog (subscriber, #1183)
[Link] (13 responses)
Posted Oct 7, 2024 16:42 UTC (Mon)
by carlosrodfern (subscriber, #166486)
[Link] (12 responses)
Posted Oct 7, 2024 17:59 UTC (Mon)
by farnz (subscriber, #17727)
[Link] (3 responses)
The bigger problem, IME, is not time, but actually recognising that you've changed something relevant. In that regard, documentation and comments (including Rust safety comments) are the worst possible way to deal with an API invariant, since there's not even a guarantee that the wording of the comment will be consistent enough to be usefully greppable.
The ideal is always type-level checking of your invariants, because that check stops you making mistakes. The next best thing is what this proposal does with unsafe impl, where users get the invariant type-checked (so I can remove the unsafe impl if it's wrong, and find all the places that need fixing), and where it's not hard to write a tool that will definitely find all the places that need manual checks.
The worst case is a situation where code at the point of use says something like "relies on the fact that foo does not move its contents, ever" in a comment, so that when someone refactors FooPtr, they've got to review all users of FooPtr to discover that the "foo" in the comment refers to something of type FooPtr.
Posted Oct 7, 2024 23:51 UTC (Mon)
by NYKevin (subscriber, #129325)
[Link] (2 responses)
Even misfortunate does not do it (at time of writing): Double does not specify T: ?Sized so you can't put a fat pointer into it (although I suspect that's merely an oversight rather than an intentional decision), and Double::swap() is a &mut self method, so even if T could be unsized, swap() counts as a modification of the pointer anyway.
In order to violate this invariant, you'd need to be doing something really ridiculous, like modifying the raw pointer value through a Cell<T> (not a Mutex<T>, since the whole smart pointer has to have the same layout as a raw pointer for this unsizing coercion to be possible in the first place), or using mem::transmute() to replace the raw pointer's vtable with a different one (not UB if the concrete types are layout-compatible).
Posted Oct 9, 2024 20:22 UTC (Wed)
by NYKevin (subscriber, #129325)
[Link]
Self-nitpicking: This should read "any &mut self method other than DerefMut::deref_mut()." But that's still a ridiculous thing to do, because deref_mut() is the method that overrides the * (dereference) operator for write operations, so if you have it re-seat the pointer, you'd be causing code like *foo = bar to change what address ("place" in Rust's formal terminology) foo points at. Nobody who writes *foo = bar expects something like that to happen.
At this point, I imagine that C++ programmers will object that, even though this operation should not change the address of the pointee, it could change the object's dynamic type. This is sort of true in C++ (objects may change dynamic type during construction, so when the object is copy-constructed, it could temporarily have a different dynamic type from bar, and you can interact with it through foo during this time), but not true at all in Rust (vtables are not even part of the object representation in the first place, they are statically allocated and tracked by fat pointers, so there is no plausible mechanism for modifying an object's dynamic type, nor is it possible to interact with partially-constructed objects through safe Rust).
Posted Oct 13, 2024 20:06 UTC (Sun)
by tialaramex (subscriber, #21167)
[Link]
In the event you find yourself writing such a type please let me know, I'm named tialaramex most places - including Google's "Gmail" of course.
Posted Oct 8, 2024 18:04 UTC (Tue)
by jezuch (subscriber, #52988)
[Link] (7 responses)
That said, the claim was that comments documenting invariants have value. They do, but only because you cannot do it in the programming language itself. How can you put faith in proofs in an unspecified language that the compiler doesn't even look at?
Use a language in which you don't have to do it most of the time.
Posted Oct 9, 2024 8:37 UTC (Wed)
by taladar (subscriber, #68407)
[Link]
That is a good point, essentially you can never rely on them when trying to understand the code because they might be outdated but you would always have to read them when modifying the code to see if they need to be updated. That is a very unlikely combination of behaviors to develop naturally.
Posted Oct 13, 2024 13:41 UTC (Sun)
by mathstuf (subscriber, #69389)
[Link] (5 responses)
Sounds like a bad habit to me. I find comments to be very helpful in both development and review. A recent example: https://gitlab.kitware.com/utils/ghostflow-director/-/mer...
Posted Oct 13, 2024 13:57 UTC (Sun)
by pizza (subscriber, #46)
[Link] (1 responses)
So I take it you've never had to review (or otherwise work with/consume) external/third party code?
In my experience, comments are rarely helpful, and more often than not, actively harmful to your understanding of what the code _actually_ does. (As opposed to what was intended at the time the comments were written).
Posted Oct 13, 2024 20:08 UTC (Sun)
by mathstuf (subscriber, #69389)
[Link]
Posted Oct 21, 2024 9:09 UTC (Mon)
by jezuch (subscriber, #52988)
[Link] (2 responses)
I'm not saying this is a *good* thing :) It just happens, at least in my brain, when tracing the code itself. I remember to look at the comments only when I'm confused, baffled, or otherwise can't figure out why it does what it does.
On the other hand, I only *add* comments when I know the code may be confusing for the reader. (This includes me, a month from now.) For example, there was a bug and the fix required some unexpected or counter-intuitive condition(s). Other than that the code should be "obvious" and in code like that comments are unnecessary at best, harmful at worst.
Posted Oct 21, 2024 12:29 UTC (Mon)
by mathstuf (subscriber, #69389)
[Link] (1 responses)
FWIW, I add comments as I code if I'm building up the algorithm from scratch or (as in this case) when I'm self-reviewing the code before merging (I think I added these even before I opened the initial MR).
Posted Oct 21, 2024 13:59 UTC (Mon)
by Wol (subscriber, #4433)
[Link]
The individual lines of code should be self explanatory, but especially when they are "elegant" (my word for "obvious once you know what it's doing") or convoluted, they may warrant a short one-liner.
Two different types of comment, for two different scenarios, shame many languages don't allow you to differentiate. This is where C(++)s' "/* */" and "//" are actually very good, as you can differentiate between the two.
Cheers,
Posted Oct 6, 2024 21:17 UTC (Sun)
by iabervon (subscriber, #722)
[Link] (3 responses)
C assumes that nobody will ever make that mistake (and it's probably right), whereas Rust wants to be sure that they can't make it.
Posted Oct 18, 2024 6:00 UTC (Fri)
by blackfire (guest, #92738)
[Link] (2 responses)
And it's actually trivial enough in this case. See a sample StrBuf in the Rust playground: https://play.rust-lang.org/?version=stable&mode=debug...
Uncommenting the one commented line (which would lead to the bug you mention) will cause a compile error. The `append` method takes a mutable reference to `self` (the object it's modifying) and an immutable reference to the bytes it's about to append.
The catch is you cannot have both a mutable and immutable reference to the same object, so there's no way trying to append a buf to itself would work (except `unsafe`, but if you use it to violate the "aliased xor mutable" rule, it's insta-UB anyway).
Posted Oct 18, 2024 6:13 UTC (Fri)
by mb (subscriber, #50428)
[Link] (1 responses)
Hm, are you sure that just having two aliasing nonconst raw pointers is insta-UB?
Posted Oct 18, 2024 7:56 UTC (Fri)
by blackfire (guest, #92738)
[Link]
Having multiple raw pointers (const or not) is fine, having multiple mut references (or one mut and 1+ const) is UB, even without dereferencing any of them.
(this is made mildly more complex by stacked borrows but that's the gist and honestly I don't claim to fully understand the intricacies of the model, but you do have to watch out for UB any time you make a reference from a raw pointer)
Posted Oct 8, 2024 0:26 UTC (Tue)
by gerdesj (subscriber, #5446)
[Link]
My takeaway from this article is that both camps are (begrudgingly) listening to each other.
As a civilian, I don't really give a shit about the rights and wrongs of a programming paradigm or whatever wankery is in the ascendant today. I am persuaded that C and Rust are both serious ways of generating machine code to run on my CPUs 'n that.
What I do like to see is gangs of serious engineers getting to grips with novel ideas and gradually thrashing out the best (for a given value of best) way forwards.
In the end its all about the engineering. Unless you are writing raw machine code, you need Assembly, C or Rust or whatevs to get stuff done ... err make stuff happen. Do remember that in the end you are generating machine code that does something - that's the goal. How you get there is a "journey" and that is up to you.
My laptop does not "run" C or Rust. It runs machine code and that's all. Please ensure it runs the best machine code available, however you get it there! I will be forever grateful for that.
Posted Oct 11, 2024 18:41 UTC (Fri)
by geofft (subscriber, #59789)
[Link] (4 responses)
I think the part I don't understand is the relevance of interop between Rust and C. Is there any case where Rust and C are modifying the same atomic variable? Is the kernel's implementation of
Maybe put another way, if the standard library gained
For that matter, why are Rust atomics and LKMM atomics different? I'm vaguely familiar with the fact that (e.g.) Alpha does things with memory ordering that people don't expect and so you need more barriers than your same code would need on other architectures, and so the kernel does emit those barriers on Alpha. But wouldn't this apply to all code on Alpha, and thus wouldn't Rust want to handle atomics in the same way? Why is there not one obvious correct way for anyone to implement an atomic reference count on a given architecture?
Posted Oct 11, 2024 19:01 UTC (Fri)
by daroc (editor, #160859)
[Link] (1 responses)
That is an excellent summary of the talk. As to why the LKMM is relevant — the LKMM doesn't just say things about how to write to atomic variables, it also has guarantees about how those writes interact with other constructs like threads. Boqun Feng actually had a neat talk that gave more detail about this later in the day that I'm still in the process of writing up. But one example is that if one thread writes to an atomic variable and then wakes another thread, the LKMM says that second thread is guaranteed to see the write. Rust atomics don't make that guarantee. Does that discrepancy cause problems? Maybe. At the very least, it's an extra complication to think about. Writing correct multithreaded code is hard enough; there's no reason to make it harder by having two conflicting models.
Posted Oct 11, 2024 20:44 UTC (Fri)
by geofft (subscriber, #59789)
[Link]
Posted Oct 11, 2024 19:12 UTC (Fri)
by farnz (subscriber, #17727)
[Link] (1 responses)
Rust atomics exactly match the memory model defined for C11, but with memory_order_consume (which no compiler benefited from, last I checked - all implementations I've seen treat it as a weird spelling of memory_order_acquire) removed completely.
The LKMM predates C11 by quite some time, and has different rules about what happens-before and synchronizes-with relationships are established by the various atomic operations you can do. As a result, something needs to make sure that when Rust code accesses atomics that are shared with C, the LKMM rules are followed, and not the C11 rules; if everything just followed the C11 rules, then the kernel code that assumes LKMM would be broken (although I believe that if everything follows the LKMM rules, code that assumes C11 rules will still work).
And Rust code will eventually need to modify atomics shared with C, because some of the existing kernel data structures depend on atomic modifications.
Posted Oct 18, 2024 6:28 UTC (Fri)
by westurner (guest, #145208)
[Link]
[Memory and IO Interfaces — Apache Arrow v17.0.0](https://arrow.apache.org/docs/python/memory.html#memory-p...)
> External memory, under the form of a raw pointer and size, can also be referenced using the foreign_buffer() function.
pyarrow.foreign_buffer(address, size, base=None)
> base: Object that owns the referenced memory.
> The buffer will be optionally backed by the Python base object, if given. The base object will be kept alive as long as this buffer is alive, including across language boundaries (for example if the buffer is referenced by C++ code)
The Arrow Parquet docs mention that parquet must be reshaped when reading and writing from disk.
Feather (Arrow IPC) format is the same shape on disk as in RAM, with ZSTD or LZ4 compression by default.
serde.rs supports very many serialization formats for rust, including Python pickles and CSV and JSON and so on.
lancedb also does zero-copy with Rust and Arrow: https://lancedb.github.io/lancedb/#why-use-lancedb :
> Tight integration with the Arrow ecosystem, allowing true zero-copy access in shared memory with SIMD and GPU acceleration
arrow-ipc-bench compares Flight, Plasma (*), sharedmemory with MacOS, IIRC:
Rust arrow_ipc::reader > Struct StreamReader:
Apache Arrow > Serde.rs compatibility:
Posted Oct 19, 2024 0:29 UTC (Sat)
by pabs (subscriber, #43278)
[Link]
https://github.com/acbits/reftrack-plugin
Unsafe
Unsafe
Unsafe
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Documented invariants versus invariants in code
Documented invariants versus invariants in code
Documented invariants versus invariants in code
Documented invariants versus invariants in code
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Rust vs. C
Wol
Rust vs. C
Rust vs. C
Rust vs. C
That doesn't look right to me.
Rust vs. C
> That doesn't look right to me.
Rust vs. C
I'm trying to understand the reference to the Linux kernel memory model here. Here's my very limited understanding so far and I'd appreciate corrections:
Smart pointers and memory models
Arc<T>
(atomically reference-counted pointer to T
uses the atomic support in the Rust standard library for the refcount, and there's some particular implementation of those.Arc
(and Rc
and a few other standard library pointer-wrapping types) have the ability that, if they hold a two-word "fat pointer", you can use them in contexts where you need a normal pointer and you know you don't need the metadata (e.g., because you're calling a method from the vtable, and that method was compiled knowing what type it's being called on) by just casting (transmuting) the type of the smart pointer and ignoring the second word.Rc<[u8; 10]>
is the first few bytes of Rc<[u8]>
.#[repr(transparent)]
wrapper around something in the standard library that is marked as implementing those traits (a raw pointer is one of these), which guarantees the layout trick.Arc
(or ListArc
) intended to be layout-compatible with any existing C code in the kernel? It seems like "We want to leak on overflow instead of panicking" and "We need the intrusive linked list support because that is a useful design in kernelspace" by itself justify this, and the different memory model doesn't come into play. What goes wrong if Rust types use Rust atomics and C types use kernel atomics?
SaturatingArc<T>
and an appropriate intrusive linked list type, which seem like not unreasonable things for everyone to have, would those suffice? (To be clear, I'm not actually suggesting this instead of the current approach, just asking it as a hypothetical for my understanding.)
Smart pointers and memory models
Smart pointers and memory models
Smart pointers and memory models
Smart pointers and memory models
https://arrow.apache.org/docs/python/generated/pyarrow.fo... :
https://github.com/wjones127/arrow-ipc-bench/tree/main
https://docs.rs/arrow-ipc/53.1.0/arrow_ipc/reader/struct....
https://docs.rs/arrow/latest/arrow/#serde-compatibility
reftrack-plugin
https://news.ycombinator.com/item?id=41875792