-
-
Notifications
You must be signed in to change notification settings - Fork 51
Add the Static<T>
type to make global mutable state easier
#103
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
Conversation
I seem to recall investigating this a bit previously. I'll do a full check later, but isn't the main advantage over Cell that we can mark it as Sync so that it can be used in a static value? |
Yes tbh that's basically the only difference but I had to reimplement the API because I can't just |
I think there also need to be compiler fences all over the place if this is to be shared with the interrupt handler and main program. I'll try to find my notes when i get home tonight. |
This is necessary to prevent the compiler from introducing optimizations that would break control flow when using `Static<T>` from within the irq handler
Ok so I found that in a quick little example program I wrote the compiler actually seemed to break the program when using a |
Ok so I wrote a little example program to show the bug that this change fixed: #![no_std]
#![feature(start)]
#![forbid(unsafe_code)]
use gba::{
fatal,
io::{
display::{DisplayStatusSetting, DISPSTAT},
irq::{set_irq_handler, IrqEnableSetting, IrqFlags, BIOS_IF, IE, IME},
},
Static,
};
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
fatal!("{}", info);
loop {}
}
static SHOULD_UPDATE: Static<bool> = Static::new(false);
extern "C" fn irq_handler(flags: IrqFlags) {
static VBLANK: Static<u16> = Static::new(0);
if flags.vblank() {
BIOS_IF.write(BIOS_IF.read().with_vblank(true));
if VBLANK.update(|n| n + 1) == 10 {
gba::warn!("irq says should update");
VBLANK.set(0);
SHOULD_UPDATE.set(true);
}
}
}
#[start]
fn main(_: isize, _: *const *const u8) -> isize {
IME.write(IrqEnableSetting::IRQ_YES);
IE.write(IrqFlags::new().with_vblank(true));
DISPSTAT.write(DisplayStatusSetting::new().with_vblank_irq_enable(true));
set_irq_handler(irq_handler);
loop {
while !SHOULD_UPDATE.get() {}
SHOULD_UPDATE.set(false);
gba::warn!("main says should update")
}
} If you run this inside mGBA with gba = { git = "https://github.com/Sp00ph/gba", rev = "54ba8e5"} as a dependency, which is the commit without the fix, the logs will only show gba = { git = "https://github.com/Sp00ph/gba"} which has the fix, the logs will say both |
This still needs compiler fences if it's being used as a way to communicate between main and interrupt, and needs to enforce that they're always placed into the code, so I don't think that you can allow a |
What exactly does this fencing look like? I assume it would be possible to return a pub fn with<U, F: FnOnce(&mut T) -> U>(&mut self, f: F) -> U where any fencing operations are handled in the |
https://doc.rust-lang.org/beta/core/sync/atomic/fn.compiler_fence.html you gotta call this guy once after each write to the location, and i think possibly after each read, so that standard memory accesses can't be moved to before the volatile memory access. |
So does that mean that |
So the thing is that volatile accesses are only ordered relative to other volatile accesses. Standard accesses can be moved to before or after a volatile access if the optimizer thinks it's a good idea. The compiler fence isn't an actual assembly operation, it's just a sign for rustc/llvm that it can't move any operations across the fence during optimizations. I'm not an expert, but this is what I've been told by others who do know what they're doing. Theoretically a non-volatile op could be moved around a volatile op and then if your stack data and global data were supposed to stay in sync, the interrupt could witness a moment of desync. And this is deeply unlikely and probably not a big deal, but we should still prevent such problems entirely if we can. As for ordering, SeqCst is kinda the "Default" to pick if you don't want to think about it too hard. It's never wrong, but sometimes it blocks a potential optimization here and there, so it might be slower in some situations. |
I'm not sure I understand this quite yet. If I remove the |
tentative "yes". I'll try to sit down and have a careful look when I'm not on my phone, but that should basically be all you need to do. There's also the issue that if the value isn't 1, 2, or 4 bytes big then it can't be written as a single instruction and so you could get an interrupt as the write is happening and see a tear. What I did to handle that was just make it explicitly always a u32 value, and then the user can cast to/from u32 on their own (eg |
Ok so what about the following idea:
This idea has the advantage that for the primitives, there should be basically no overhead over using a |
Also I just noticed that you couldn't even do the assertion in the constructor because you can't assert in a |
to assert in const fn you do some dumb nonsense: // this array can be any type, as long as the length is 1
[()][!(test_here) as usize] But it does panic on test failure. |
This is necessary to ensure memory safety
From what I found online, |
Ok so I just modified the small example program from above to check whether these things actually work, and as it turns out, you get a linker error:
I would guess that this is because of the |
Ok so as it turns out the errors are because of the |
Very odd indeed that compiler_fence causes problems. I was told that it's an intrinsics that emits no code. |
I also find that very weird considering that even in the |
Yea I think the way it is implemented in #107 should be enough for most applications. Closing this now |
This is useful for example when setting a flag inside an interrupt handler to then execute something later. This was previously only possible using
static mut
variables, which can't be accessed safely and can lead to memory unsoundness like reference invalidation.