-
Notifications
You must be signed in to change notification settings - Fork 19
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
New design based on *mut T
#22
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apart from from_ref
and from_mut_ref
I think the new interface doesn't invoke any UB.
@RalfJung volatile accesses to |
I mean, the access itself is fine, but if you are doing MMIO then you don't want to use a reference. References are considered "dereferencable" and the compiler may insert spurious reads. See e.g. here for more details. |
@RalfJung Thanks for taking a look! I pushed a new commit that switches from
The problem with pushing the volatile to the lowest level (e.g. struct fields) is that it doesn't work well for slices/arrays. For example, we don't want to write each byte of a What I'm trying with this crate is to allow putting the volatile wrapper around on the outer level and then providing methods to narrow it down to specific slice ranges (through |
Yes. Basically, "use raw pointers and only raw pointers" is currently the only option for spec-compliant MMIO in Rust. But I'm really not the expert here, it'd be great if @Lokathor could take a look. :) |
I will have a look later today if I can get the chance, but if you want a comparison point you can look at yourself the Edit: oh, you've probably already read my crate though XD |
It seems like you could simplify the marker traits/structs to avoid duplication (and making things like pub struct NoAccess;
pub struct UnsafeAccess;
pub struct SafeAccess;
pub trait Unsafe {}
pub trait Safe: Unsafe {}
impl Unsafe for UnsafeAccess {}
impl Unsafe for SafeAccess {}
impl Safe for SafeAccess {}
pub struct Access<R, W> {
pub read: R,
pub write: W,
}
pub type ReadOnly = Access<SafeAccess, NoAccess>;
pub type WriteOnly = Access<NoAccess, SafeAccess>;
pub type ReadWrite = Access<SafeAccess, SafeAccess>; Then the implementation would be fairly straightforward: impl<T: ?Sized> Volatile<T> {
pub const unsafe fn new<A>(pointer: *mut T) -> Volatile<T, A> {
Volatile { pointer, access: PhantomData }
}
pub const unsafe fn from_ptr(pointer: *const T) -> Volatile<T, ReadOnly> {
Volatile::new(pointer as *mut T)
}
pub const unsafe fn from_mut_ptr(pointer: *mut T) -> Volatile<T, ReadWrite> {
Volatile::new(pointer)
}
}
impl<T, Read, Write> Volatile<T, Access<Read, Write>> {
pub unsafe fn read_unsafe(&self) -> T
where
Read: Unsafe,
{
ptr::read_volatile(self.pointer)
}
pub fn read(&self) -> T
where
Read: Safe,
{
unsafe { self.read_unsafe() }
}
pub unsafe fn write_unsafe(&self, val: T)
where
Write: Unsafe,
{
ptr::write_volatile(self.pointer, val)
}
pub fn write(&self, val: T)
where
Write: Safe,
{
unsafe { self.write_unsafe(val) }
}
pub unsafe fn update_unsafe(&mut self, f: impl FnOnce(&mut T))
where
Read: Unsafe,
Write: Unsafe,
{
let mut value = self.read_unsafe();
f(&mut value);
self.write_unsafe(value);
}
pub fn update(&mut self, f: impl FnOnce(&mut T))
where
Read: Safe,
Write: Safe,
{
unsafe { self.update_unsafe(f) }
}
} |
I thought about removing the pub fn read_only() -> Access<SafeAccess, NoAccess> {
Access {
read: SafeAccess,
write: NoAccess,
}
} |
The lifetime parameter is required to make `map_mut` sound (otherwise we might get mutable aliasing). Also: - add new `from_ref` and `from_mut_ref` contructor methods again. - add a `new_generic` constructor method to construct a `VolatilePtr` with arbitrary lifetime and access parameters.
I'm not sure what this means. Do you mean if you use an alias like pub type MyPhantom = PhantomData<u8>; then you can't use #[allow(non_upper_case_globals)]
pub const MyPhantom: MyPhantom = PhantomData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really looking good, mostly just questions about new_generic
vs. new_with_access
I'm not sure it makes sense to have both.
I put together a commit that shows what the crate would look like without new_with_access
:
josephlr@952b7cd
// Todo: is there a way to check that a compile error occurs when trying to use | ||
// unavailable methods (e.g. `write` when write permission is `UnsafeAccess`)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can have a failing doctest? That might be the easiest way to do it.
volatile.index_mut(..4); | ||
} | ||
|
||
#[cfg(feature = "unstable")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there plans to have tests for the "very_unstable"
features?" Maybe just doc-tests are enough?
reference, | ||
access: PhantomData, | ||
} | ||
pub const unsafe fn new_with_access<A>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we were to just delete this function and only have VolatilePtr::new_generic
, would we really lose much? It seems like you would end up writing about the same amount of code.
let access = Access {
read: UnsafeAccess,
write: SafeAccess,
};
let mut volatile = unsafe { VolatilePtr::new_with_access(NonNull::from(&mut val), access) };
vs
type Custom = Access<UnsafeAccess, SafeAccess>;
let mut volatile = unsafe { VolatilePtr::new_generic::<Custom>(NonNull::from(&mut val)) };
// Or just define inline
let mut volatile: VolatilePtr<_, Access<UnsafeAccess, SafeAccess>> = unsafe { VolatilePtr::new_generic(NonNull::from(&mut val)) };
It seems like having to pass the marker type by value isn't really necessary.
pub struct Access<R, W> { | ||
pub read: R, | ||
pub write: W, | ||
} | ||
|
||
impl Access<SafeAccess, NoAccess> { | ||
pub const fn read_only() -> ReadOnly { | ||
Access { | ||
read: SafeAccess, | ||
write: NoAccess, | ||
} | ||
} | ||
|
||
pub const fn write_only() -> WriteOnly { | ||
Access { | ||
read: NoAccess, | ||
write: SafeAccess, | ||
} | ||
} | ||
|
||
pub const fn read_write() -> ReadWrite { | ||
Access { | ||
read: SafeAccess, | ||
write: SafeAccess, | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we got rid of the need to pass Access
by value and instead just referred to the type, we could delete all these type methods, and also just define Access
like:
pub struct Access<R, W>(R, W);
I think we should either:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments (I don't currently have time for a full review)
#![cfg_attr(feature = "very_unstable", feature(const_slice_ptr_len))] | ||
#![cfg_attr(feature = "very_unstable", feature(const_panic))] | ||
#![cfg_attr(feature = "very_unstable", feature(const_fn_trait_bound))] | ||
#![cfg_attr(feature = "very_unstable", feature(const_fn_fn_ptr_basics))] | ||
#![cfg_attr(feature = "very_unstable", feature(const_trait_impl))] | ||
#![cfg_attr(feature = "very_unstable", feature(const_mut_refs))] | ||
#![cfg_attr(feature = "very_unstable", allow(incomplete_features))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we drop the const_fn
stuff and "very_unstable"
feature for this review. That stuff might be easier to review if we do it in a followup PR. We would probably want to make certain functions const fn
if a certain feature is enabled, rather than having additional functions. This is what x86_64
does.
unsafe { self.split_at_mut_unchecked(mid) } | ||
} | ||
|
||
unsafe fn split_at_unchecked( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add the various "unchecked" methods in a followup PR? This will allow us to focus on having the "right" API for the checked methods. Once we have the "base" PR merged, adding the unchecked methods shouldn't be too bad.
value, | ||
self.reference.len(), | ||
); | ||
intrinsics::volatile_set_memory(self.pointer.as_mut_ptr(), value, self.pointer.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like for this method (and all the other methods that use volatile intrinsic), we should just have a stable, normal loop that just does read_volatile
/write_volatile
by default.
We could then add a nightly-only "slice_optimizations"
feature, that changes the implementation to use the intrinsics. Then, when rust-lang/rfcs#2728 gets stabilized, we can switch to unconditionally using the stabilized methods.
src/lib.rs
Outdated
where | ||
R: Deref<Target = [T; N]>, | ||
{ | ||
impl<T, R, W, const N: usize> VolatilePtr<'_, [T; N], Access<R, W>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm confused, but why does as_slice
need the 'unstable"
feature?
It's mostly a question of if we can get this PR small enough so that I, @Lokathor, or someone else can review the API. We would probably also want to prototype some use-cases with this API to make sure things are ergonomic. |
I think in the interest of getting this review though, we should postpone discussions of |
This PR is rather old and since it was opened I've experimentally added a "volatile region" type to So, that's just how I thought things could work. Maybe it can help as a point of reference. I set it up so that it's never a dynamically sized type, because it's a pain in the butt and adds essentially zero value for my purposes.
This is generally the right call, because (unlike Copy) if someone "knows what they're doing" and wants to override your decision they can just go ahead and do that. |
src/lib.rs
Outdated
pub fn split_at_mut( | ||
&mut self, | ||
mid: usize, | ||
) -> ( | ||
VolatilePtr<[T], Access<R, W>>, | ||
VolatilePtr<[T], Access<R, W>>, | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this method take Self
as an owned value? A problem with the current implementation is that the returned VolatilePtr
s can't outlive the reference to the VolatilePtr
, but they really only need to not outlive the lifetime on the VolatilePtr
.
This leads to problems in practice:
fn fetch_more_data() -> [u8; 4] {
todo!("fetch some data")
}
fn read_some_data(mut dest: VolatilePtr<[u8], Access<SafeAccess, SafeAccess>>) {
while dest.len() != 0 {
let chunk = fetch_more_data();
let len = cmp::min(chunk.len(), dest.len());
let (mut head, tail) = dest.split_at_mut(len); // <-- `dest` does not live long enough
head[..len].copy_from_slice(&chunk[..len]);
dest = tail; // <-- cannot assign to `dest` because it is borrowed
}
}
(The same code works just fine with &mut [u8]
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been trying to prototype an updated implementation for this, and I've also realized that having all the methods take self
is probably what we want to do.
Conceptually, it makes sense for our VolatilePtr
have self
methods as it's a Copy
type. It also reduces the overall number of mut
vs not-mut
method variants.
Is there a good way to make splitting mutable borrows work? We could maybe achieve this by implementing destructuring in a macro and getting access to the fields that way though it's not quite the same a splitting borrows. |
Short answer: Not with just a Fundamentally, rust considers a mutable borrow exclusive, and volatile is used on things that you never have exclusive control over. So if you want to build some sort of access control layer to the API using the borrow checker, you should build it "above" this base layer of abstraction. |
I simply would not write that code to begin with. These pointers should be Copy and they should take
I completely agree, that code should work. |
I explored writing such an api in #28 though @phil-opp correctly pointed out in #28 (comment) that that would require |
Well according to LLVM it's up to the platform how sync a volatile access is or not, so.... [thumbs up sign while doing a nervous smile] I guess good luck with that if your platform is multi-core and doesn't let them be sync. That sounds terrible to me. |
According to https://doc.rust-lang.org/core/ptr/fn.read_volatile.html#safety data races caused by volatile reads and writes are UB:
But yeah, I totally get what you're saying though, for some applications (especially single threaded ones) having a volatile pointer be |
Hmmmmmm, well LLVM has a rather more flexible definition:
I think the stdlib docs are actually just in the wrong here. Particularly because it makes interrupt handlers and signal handlers literally impossible to write if the target doesn't have atomics. |
various improvements for the new design
With the current implementation the |
After reading through this discussion again, I think there are two possible designs. We can either implement Both approaches seem valid to me, so I implemented them both in #29, as a base for further discussion. The question is now: Which type do we prefer? Or does it make sense to provide them both? Please let me know what you think in #29, so that we can finally replace the current unsound implementation. |
TODO:
cc #13 (comment)