-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can we have VolatileCell #411
Comments
I think the question whether we "can" have is fairly straight-forward -- yes, we can totally do that. I think it wouldn't be too hard either, operationally speaking. For instance we could say that For |
Is this something that is actually needed? The main use case is making "register layout structs" in Peripheral Access Crates (PACs): struct MyRegisterBlock {
foo: VolatileCell<u32>,
bar: VolatileCell<u32>,
baz: VolatileCell<u32>,
}
struct VolatileCell<T: Copy>(UnsafeCell<T>);
impl<T> VolatileCell<T> {
fn read(&self) -> T { self.0.get().read_volatile() }
fn write(&self, val: T) { self.0.get().write_volatile(val) }
}
let regs: &MyRegisterBlock = &*(0x4000_0000 as *const MyRegisterBlock);
regs.foo.write(0x42); However, you can accomplish the same thing by staying entirely within raw pointers: struct MyRegisterBlock(*mut u32);
impl MyRegisterBlock {
fn foo(&self) -> Reg<u32> { Reg(self.0.add(0)) }
fn bar(&self) -> Reg<u32> { Reg(self.0.add(1)) }
fn baz(&self) -> Reg<u32> { Reg(self.0.add(2)) }
}
struct Reg<T: Copy>(*mut T);
impl<T> Reg<T> {
fn read(&self) -> T { self.0.read_volatile() }
fn write(&self, val: T) { self.0.write_volatile(val) }
}
let regs: MyRegisterBlock = MyRegisterBlock(0x4000_0000 as _);
regs.foo().write(0x42); Pros and cons:
So overall, I don't think there's any reason to prefer the struct approach. in Embassy we've been using PACs generated by chiptool, which generates code using raw pointers, out of concerns for the So IMO it's not worth it to add exceptions to the memory model to support usages like |
I'm an outsider here and totally happy with not having VolatileCell, if the domain experts say that the status quo is totally sufficient. @Lokathor has opinions on this topic, IIRC. |
I specifically would like |
My opinions are:
@Dirbaio I think what people "really want" is something that also has magical field projection so that you can do code like this: #[repr(C)]
pub struct Controls {
display_control: u16,
display_status: u16,
vcount: u16,
}
pub const MMIO: &VolatileCell<Controls> = whatever_expression_here!(0x0400_0000);
fn main() {
// magical field projection from `&VolatileCell<Controls>` into `&VolatileCell<u16>`
let display: &VolatileCell<u16> = &MMIO.display_control;
// call some method to assign some value
display.write(1);
// or we can chain the expression
let y: u16 = MMIO.vcount.read();
} I think without some sort of field projection thing added to the language it's less compelling, though possibly still useful even then. Also note that the above example doesn't bother with when things are unsafe or not, which things are readable or writable, etc. There's a lot of design that can go into an mmio abstraction type. @chorman0773 can you say more about that linker stuff? I think you told me once but I've forgotten if you did, and more details about that might make a more compelling case for needing |
Yes. In SNES-Dev, I use linker scripts to define access to the MMIO registers. For stuff that is specific to SNES-Dev, the address is even actualy known at compile time, as it is generally assigned by the linker itself, and given a special type in the mapping table. |
That part sounds similar to GBA dev, but with GBA it's generally done with just |
@Dirbaio in your example, MyRegisterBlock is no longer a ZST. Can that approach work without holding on to the pointer? |
Thanks for the mention! As far as I know, volatile cells are still commonly used in the Rust embedded an OS ecosystems. For example:
Most of these projects are aware that this approach is not sound, but still haven't changed their code. I assume that it's a mix of personal preference (structs are much easier to write than doing manual pointer arithmetic) and migration cost (requires breaking changes across the entire ecosystem). So I think that there is definitely demand for a sound |
I agree that it's less of an issue when the pointer arithmetic is generated by tools. However, I think most people write things by hand first and then create tools to autogenerate the boilerplate code later using the same design. So the design that is easier to write by hand is often used for code generation too. For example, I think |
That is not compatible with CHERI, right? For CHERI you did at least have to store a raw pointer to preserve the full capability. |
in the struct approach, you use a HAL wouldn't store a
from the Embedded WG's perspective, we were definitely aware of this and studying possible solutions (it's one of the reasons chiptool exists for example). The push for it has essentially stopped once rust-lang/rust#98017 was merged, which meant VolatileCell is no longer unsound in the current implementation. It was not an "okay, now let's push to make VolatileCell sound in the memory model" decision, it was more like "okay, it's less urgent now, and we don't have much manpower so let's leave it for now". Moving to pointers is still something I'd personally like to see done, due to the other advantages I mention in this thread. (this is my personal opinion though, not the WG's)
my experience is the contrary, everyone starts adding support for a new chip by grabbing a I disagree reg structs are easier to write by hand, too. Documentation always says "register FOO is at offset 0x4c". Translating that to pointer math is trivial, you just add 0x4c. Translating that to a struct you find yourself counting registers manually to infer at which offset each field is, and manually calculating sizes of dummy padding hole arrays. I think people write registers structs simply because that's how it's always been done in C. There's a good reason to use structs in C: you can use fields with |
You may be right. I've never seen a hardware manual for any CHERI device. |
That is concernying from a t-opsem perspective, this is certainly not the outcome we hoped for when resolving the Anyway, from my perspective -- what's missing here is a VolatileCell RFC. This doesn't seem like a gap in the language that needs solving to gain some crucial expressiveness, so I don't have writing such an RFC anywhere on my own roadmap. t-opsem would be involved only insofar as we'd make the memory model actually support the desired semantics, but until such an RFC is accepted, I think this is S-status-not-opsem. |
For the SNES-Dev specific parts, they don't have a fixed address and just live where the mapping table puts them. BEing able to float that arround the rest of the cartridge mapped memory allows for more compact programs and more effective bank-sensitive relaxations. And having the SNES stuff also be defined in the linker, depsite having fixed addresses, is good for consistency.
I'd say there were a lot of consequences from removing dereferenceable from |
A Our use case is writing MMIO drivers based on manually written structs. We are currently in an unsound situation in our codebase, anticipating helpers to make volatile accesses easier before rewriting everything with raw pointers. It would be a blessing if the language allowed to write I would be genuinely interested in driving this forward and perhaps writing an RFC. The right way to start drafting would be the |
Field projections would also be useful for |
latest I am aware of is rust-lang/rfcs#3318 |
Field projections is orthogonal to the issue at hand though? The question is "do we add some special case to opsem to guarantee no speculative reads on |
The answer isn't just a yes or no, it's actually "we don't right now, but we could do that, but it's non-zero work to do, and the work won't be of much value without also having field projection, so we should evaluate the cost/benefits with all that in mind" |
As Ralf said, the opsem side of things is pretty cut-and-dried. If lang approves a |
ah okay! I see the plan now, thanks for clarifying. Just to confirm:
|
I think so, yes.
As an pub struct VolatileMem<T> {
ptr: NonNull<T>,
}
impl<T: Copy> VolatileMem<T> {
pub fn get(&self) -> T;
pub fn put(&mut self, val: T);
pub fn map<F: Field<Base = T>>(self) -> VolatileMem<F::Type>;
}
|
Looks like we should have a thread for VolatileCell API design somewhere -- this thread here was intended for the opsem questions around it, which are quite orthogonal. (Or we declare the opsem parts resolved and re-purpose this here about API design -- including whether that design even needs a |
Has anyone considered using My understanding is that Maybe a new type is needed to say "this is not actually dereferenceable" (in addition to (I'm sorry if this has already been answered or if that's not the right place to ask) edit: seems that Redox is using |
|
Has there been any interest in making progress on |
I think you'd need to post some kind of proposal (maybe an RFC) that involves T-lang and probably T-libs-api. |
There are some open opsem questions though -- specifically, how should types like |
I think that without some sort of field projection and/or indexing projection system (which is being discussed elsewhere, but which I think hasn't even turned into an RFC yet), then VolatileCell can't really provide a significant improvement over the current capabilities. |
Field projection can be (awkwardly) implemented in userspace via macros (e.g. the There is one capability I'm imagining that |
I don't think we can have such an operation without LLVM exposing it as a new primitive. If memory is ever dereferenceable, then we consider it to always remain dereferenceable; this is not something you can ever undo (except maybe with something like rust-lang/rfcs#3700, but that cannot be soundly used on a reference you got from someone else). |
I don't think we should have VolatileCell. Reasons:
IMO the "make a Also there's been some news from svd2rust since I last posted here:
|
Yeah the |
Wouldn't field projections (at least the kind proposed on rust-lang/rfcs#3735) work for a |
Yes, that's basically what I was saying: VolatileCell / VolatilePtr / WhateverYouCallIt would be an improvement with field projection capability, but it is not an improvement until then. |
I am quite confused by this reply. This discussion is about whether VolatilePtr (which can already be defined by users as a newtype around IOW:
Which improvements can it provide with field projections, given that VolatilePtr with field projections already seems to be pretty nice? |
The Rust OSDev community has also moved away from |
Assuming we have field projections, one area where impl<'a> VolatilePtrMut<'a, T> {
fn reborrow<'b>(&'b mut self) -> VolatilePtrMut<'b, T> {
todo!()
}
} Using |
I assume |
Yeah, |
We should definitely fix that. |
The current implementation guarantees volatile ops up to a single word are compiled to a single LDR or STR instruction, so they're already atomic. I'm not sure if rust opsem formally guarantees this or not? if not, it should. Otherwise pretty much all existing MMIO code is broken. So, as long as you only write words (which is usually the case when you do MMIO) you don't need exclusive access to make it sound, it's not UB. (with some caveats, you need extra conditions so it's not UB from the hardware, such as making accesses in uncahced device memory to avoid cache coherency issues. If you're doing MMIO you already fulfill these conditions) |
From the std docs:
Not sure if this can be removed and things will "just work" |
Yeah those docs are kinda old and conservatively designed, they're more like a lower bound on what's possible to be confident about right now. We could definitely work to improve the situation. |
It's probably worth noting as an aside that it may be desirable to create an exclusive borrow of a MMIO register for API design reasons (statically enforcing invariants etc.) |
Honestly we should make creation of the VolatileWhatever be unsafe so that usage can be kept safe as often as possible. That plus an atomic flag or whatever and you've got your "lock this device" api designed already. |
The current implementation [guarantees](https://llvm.org/docs/LangRef.html#volatile-memory-accesses) volatile ops up to a single word are compiled to a single LDR or STR instruction, so they're already atomic.
You misunderstood what "atomic" means. It is not about how many asm instructions are emitted, it is about how it interacts with the Rust memory model.
There have been plenty of discussions around volatile atomic on this issue tracker, please have a look at the context here.
|
I meant "single ldr/str" instruction does make it "atomic" in the sense as "two racing word-sized reads/writes to the same memory address are not UB". So, similar to Relaxed atomics. This guarantee is enough to make register access APIs safe without needing exclusive/mutable references or owned singletons. and about "how it interacts with the Rust memory model", the answer is just "it doesn't"? you never mix volatile accesses and regular accesses to MMIO registers. If you do want it to interact then yes you do need "real" volatile atomics. (for example do regular writes to a buffer then do an volatile atomic Release write to a DMA register to ensure DMA start happens after the buffer is filled) |
The docs still reflect the current status though. We'd have to change our LLVM codegen to make these operations actually atomic, but that doesn't work with our current API -- my hope is that we can use bytewise atomic volatile memcpy once rust-lang/rfcs#3301 becomes a thing.
What the hardware does is not the point. These operations are not marked as atomic in the memory model so they must not race. It may be possible to carve out an exception for locations which are never pointed-to by a reference and never accessed using non-volatile loads/stores. (Not sure if we have to worry about |
This question is intended to replace #33 and #265, and ask the general question of whether or not we can have a type, either user-defined or language-provided, that, when wrapped in a shared reference, guarantees that no accesses are introduced at the operational semantics level that are not part of the original program (and thus, if volatile accesses only are used, an implementation won't introduce any speculative reads).
If such a type can exist, the second question is what is the operational semantics of retagging as a shared reference to such a type.
The text was updated successfully, but these errors were encountered: