-
Notifications
You must be signed in to change notification settings - Fork 34
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
feat: implement wrappers for GUI APIs #29
base: main
Are you sure you want to change the base?
Conversation
baec17e
to
c4f990b
Compare
crates/gui/Cargo.toml
Outdated
@@ -0,0 +1,25 @@ | |||
[package] | |||
name = "flipperzero-gui" |
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'm trying to decide whether we should put this in the main flipperzero
crate or not.
There's some argument to be made that for putting each service under its own flipperzero-{service}
crate, but I'm wondering if it is worth the hassle (especially if it's all part of one SDK). The windows
crate takes the approach of putting each API under its own feature flag, so you don't have to build the entire set of bindings for every app.
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.
Yup, at the moment I am not that sure about the placement thus I use gui
as some form of incubator for the new APIs, but I already see this separation being a problem (as can be seen by a comment about Align
type in this Cargo.toml
). In the end I am planning to either move all this APIs to flipperzero
(probably behind feature-flags) all provide more logical separation.
6715ca3
to
c64aef9
Compare
By the way, while the API is in its early stage, I would like to hear general thoughts from you if I am working in the right direction. |
c64aef9
to
38a04c0
Compare
So far this is looking great. I think everyone will appreciate not having to use One issue I did run into in the past was how to implement GUI callbacks. It would be really nice to use Rust closures, but closures require a "fat pointer" which is tricky to use with the standard
So far everything looks good. I think even things like The one I'm not 100% sure about is |
As for now, I think about one way to implement this: theoretically, we could provide dispatcher-functions matching the C-signature and pass the actual closures via context. The dispatcher-function would simply deref context calling its actual function. It still is just a concept with which I am going to experiment (at least, I expect some
Seems true, as for now I marked them as unsafe (partially, to make myself think twice before using them), but it seems that they really aren't unsafe and ATM the safety contract I write for
Same problem :D I will have to re-read documentation about it once the API is closer to its final form. Thanks for your review and the comments! And have a happy new year by the way 🚀 |
That's where I ran into an issue: pub fn set_draw_callback(&self, callback: &dyn Fn(&Canvas)) {
// this won't compile because callback is a fat pointer (i.e. `sizeof(&dyn Fn()) > sizeof(*void)`)
let callback = callback as *mut c_void;
unsafe {
sys::view_port::draw_callback_set(self.inner, Self::draw_callback, callback);
}
} Without a fat pointer, Rust wouldn't know the correct vtable for the concrete trait implementation. |
struct CallbackWrapper<'a>(&'a dyn Fn(&Canvas));
impl<'a> CallbackWrapper<'a> {
fn call(&self, canvas: &Canvas) {
self.0(canvas) // vcall ocurrs here
}
} |
12e4972
to
aaa4eb0
Compare
Here are some good news:
Screencast.from.2023-01-03.05-52-32.webm |
This also resolves unsoundness in previous callbacks' implementation.
Most importantly, this adds support for drawing XBMs
Co-authored-by: Yaroslav Bolyukin <[email protected]>
Hi, @dcoles! Since you've added UPD: just moved |
First separated PR is ready: #81 |
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.
Flushing an initial partial review of flipperzero::gui
.
/// Furi record corresponding to GUI. | ||
pub const RECORD: *const c_char = sys::c_string!("gui"); | ||
|
||
pub fn new() -> Self { |
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.
These new public APIs should all be documented. That would also help with review, knowing the intent behind each API (particularly the ones that aren't just a type-safe wrapper around a single SDK function).
}; | ||
|
||
/// System Gui wrapper. | ||
pub struct Gui { |
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 new struct should be mentioned in the changelog in the Added
section. See prior changelog entries for the style.
crates/flipperzero/src/gui/mod.rs
Outdated
pub mod icon; | ||
pub mod icon_animation; | ||
pub mod view; | ||
pub mod view_port; | ||
pub mod xbm; |
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.
These new modules should be mentioned in the changelog under the Added
section.
// SAFETY: `RECORD` is a constant | ||
let gui = unsafe { UnsafeRecord::open(Self::RECORD) }; |
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 occurs to me that records are probably not supposed to be opened by multiple threads simultaneously. If that is the case, then separately from this PR, we might end up defining the SDK-provided records in some common location, where we can control access via a mutex. An alternative approach would be to create a harness for defining "record singletons", which would store them as heap statics (#64); then each service module would use it to define its record type.
The context for the above thought was "is it okay to call Gui::new
twice?". Nothing in the current API will prevent it, but if the backing records are changed to be mutex-gated, then Gui::new
would necessarily also be. And if we instead provide a record singleton harness, then Gui::new
would become a private method.
pub fn add_view_port<VPC: ViewPortCallbacks>( | ||
&mut self, | ||
view_port: ViewPort<VPC>, | ||
layer: GuiLayer, | ||
) -> GuiViewPort<'_, VPC> { |
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 don't see any examples with multiple view ports. Is that a thing we should expect to eventually support?
- If so, then storing
&self
insideGuiViewPort
currently prevents this (because then we can't re-take a mutable reference toself
), so we might need an alternative API for adding multiple view ports at once (or an alternative way to reference the parent instead of&self
, but that still binds to the lifetime ofSelf
rather than the&mut self
or&self
). - If not, then
Gui::add_view_port
feels misnamed, and it should instead be something likeGui::connect_view_port
.
unsafe { sys::gui_set_lockdown(raw, lockdown) } | ||
} | ||
|
||
// TODO: separate `GuiCanvas` (locking the parent) |
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.
Has this been renamed?
// TODO: separate `GuiCanvas` (locking the parent) | |
// TODO: separate `ExclusiveCanvas` (locking the parent) |
f9ded4e
to
82b54fd
Compare
82b54fd
to
64cf882
Compare
By the way, @str4d, is there any way we could add suport for running MIRI? Asking this since the whole callback magic is quite tricky from aliasing standpoint. I still am unsure which invariants should be upheld for I tried running it but the MIRId app just crashed. |
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.
You weren't kidding about this being a big code review!
That said, it's shaping up really nicely. It's really impressive to see that it's now possible to write a full graphical application without the user needing to write any unsafe code. Looking forward to merging it in. 🙌
} | ||
let view_port = ViewPort::new(State { | ||
text: CStr::from_bytes_with_nul(b"Hi there!\0").expect("correct string"), |
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 wonder if we should provide a cstr!
macro for cases like this.
The only issue I see is that I believe that #![forbid(unsafe_code)]
will also forbid macros using unsafe
, even if the macro ensures safe use of an unsafe function (e.g. calling CStr::from_bytes_with_nul_unchecked
with the guarantee of nul termination).
#[cfg(feature = "unstable_intrinsics")] | ||
{ | ||
divident.div_ceil(divisor) | ||
} | ||
#[cfg(not(feature = "unstable_intrinsics"))] | ||
{ |
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 use if cfg!
/else
here:
if cfg!(feature = "unstable_intrinsics") {
divident.div_ceil(divisor)
} else {
let quotient = divident / divisor;
let remainder = divident % divisor;
if remainder > 0 && divisor > 0 {
quotient + 1
} else {
quotient
}
}
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 don't think thjat it wik work as expected since it will expand to a boolean which will mean that the top branch will stil have to be valid which is not the case for builds on stable
(which is the whole purpose of hising this behind a feature flag).
We could try using cfg_if
, but at the moment it looks like an overkill (especially, considering that it's body is not affected by linters).
crates/flipperzero/src/kernel.rs
Outdated
} | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq)] |
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.
Does PartialEq
/Eq
make sense on this? Would you ever do a comparison?
assert!( | ||
matches!(width_ident, [.., b'_', b'w', b'i', b'd', b't', b'h']), | ||
"width identifier should end with `_width`", | ||
); |
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 wonder if we should replace these with static_assertions
(not necessarily in this PR).
I think the asserts make sense, though I do worry about the runtime/code-size overhead that we might be taking on. Probably worth doing a side-by-side with/without those asserts and see how much additional code they introduce.
Unfortunately binary size has a very direct impact on how quickly Flipper applications start.
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.
Generally, this would normally be replaced with a proc-macro in the future.
As for now, I think that it should normally be remomved by the compiler, considering that it is intended for use in const
s where the expression has to be CT-evaluated.
Just found the explicit information about aliasing rules for Another point on allowing MIRI runs on flipper. cc @mogenson, this may be important for what you currently doing with |
Co-authored-by: Mike Mogenson <@mogenson>
#[must_use] | ||
fn tick_period(&self) -> NonZeroU32 { | ||
// Some arbitrary default | ||
NonZeroU32::new(100).unwrap() |
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.
Good change. A tick period of zero means wait forever, which is probably not what the user wants if they are trying to register an on_tick callback.
|
||
pub trait ViewDispatcherCallbacks { | ||
fn on_custom(&mut self, _event: u32) -> bool { | ||
true |
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.
true
means event handled. Although these default callback implementations should never be registered, let's not have them claim to handle events.
}; | ||
use flipperzero_sys::{self as sys, ViewDispatcher as SysViewDispatcher}; | ||
|
||
pub struct ViewDispatcher<C: ViewDispatcherCallbacks, const QUEUE: bool> { |
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.
Looks like you're exposing some more customization to the user when creating this type. For configuration options like view dispatcher type, enabling queue, and tick period, what's the benefit of putting them as generic types. eg:
pub struct ViewDispatcher<C: ViewDispatcherCallbacks, const QUEUE: bool, const TYPE: ViewDispatcherType, const TICK_PERIOD: NonZeroU32>
vs new()
method arguments:
pub fn new(callbacks: C, queue: bool, view_dispatcher_type: ViewDispatcherType, tick_period: NonZeroU32) -> Self
vs callback getters:
pub trait ViewDispatcherCallbacks {
fn tick_period(&self) -> NonZeroU32
fn enable_queue(&self) -> bool
fn view_dispatcher_type(&self) -> ViewDispatcherType
}
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.
As for now, it is mostly my experiments with what we can and what we can't express with it.
My point of having queue
as a const generic is to prohibit calls to methods requiring it at the type-system level.
PS: to be fair, I've actually commmited my ViewDispatcheer
experiments by accident, and then just went with experimenting on them
|
||
pub struct ViewDispatcher<C: ViewDispatcherCallbacks, const QUEUE: bool> { | ||
raw: NonNull<SysViewDispatcher>, | ||
callbacks: BoxNonNull<C>, |
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 we need to provide a way to access a &mut ViewDispatcher
from within the ViewDispatcherCallbacks
. This is because run()
is a blocking call and the only way to exit an app is to call stop()
from some callback / event handler.
I made an attempt at adding a &mut ViewDispatcher
argument to each ViewDispatcherCallback
method by storing an NonNull<SysViewDispatcher>
and using it to construct a ViewDispatcher
in the extern "C" dispatch callbacks. You can see that here: mogenson@65bf582)
It works with the modified view_dispatcher.rs example, but I'm aware that the lack of lifetime parameters makes it really fragile.
} | ||
|
||
impl<C: ViewDispatcherCallbacks> ViewDispatcher<C, true> { | ||
pub fn run(&mut self) { |
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 run()
takes an &mut self
and blocks, does this mean that we can't safely use the instance of ViewDispatcher
created by new()
anywhere after calling run()
? Even in ViewDispatcherCallbacks
?
In the modified view_dispatcher.rs example here I'm curious why I got not compiler complains when storing an &mut ViewDispatcher
in a separate struct then calling run()
.
self.raw.as_ptr() | ||
} | ||
|
||
// pub fn add_view(&mut self,) |
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.
TODO: can you add the remaining view dispatcher methods. eg:
/// Register a view
pub fn add_view(&mut self, view_id: u32, view: *mut sys::View) {
unsafe { sys::view_dispatcher_add_view(self.raw.as_ptr(), view_id, view) };
}
/// Remove a view
pub fn remove_view(&mut self, view_id: u32) {
unsafe { sys::view_dispatcher_remove_view(self.raw.as_ptr(), view_id) };
}
/// Switch to a view
pub fn switch_to_view(&mut self, view_id: u32) {
unsafe { sys::view_dispatcher_switch_to_view(self.raw.as_ptr(), view_id) };
}
/// Send view dispatcher's view port to front
pub fn send_to_front(&mut self) {
unsafe { sys::view_dispatcher_send_to_front(self.raw.as_ptr()) };
}
/// Send view dispatcher's view port to back
pub fn send_to_back(&mut self) {
unsafe { sys::view_dispatcher_send_to_back(self.raw.as_ptr()) };
}
/// Send a custom event to the the custom event callback handler
pub fn send_custom_event(&mut self, event: u32) {
unsafe { sys::view_dispatcher_send_custom_event(self.raw.as_ptr(), event) };
}
@dcoles, could you please bump the API version? There is a number of changes in there (specifically, |
Status updateCurrently trying to understand the right mental model of aliasing with respect to Flipper's GUI lifecycle. Since there is high risk of violating Rust's aliasing guarantees, there may be a need to use |
c54e15b
to
90eb38b
Compare
Description
This is a WIP PR to provide generic safe bindings for Flipper's GUI APIs.
Current status:
ViewPort
Canvas
Gui
Icon
IconAnimation
start
andstop
methods;start
andstop
be available via view (i.e. is it sound to call them from callbacksIconAnimationCallback
).View
SceneManager
ViewDispatcher
ViewStack
Unresolved questions
Safety notes
I am trying my best to make the API as safe as possible but the chances are high that the initial implementation will contain unsound bugs. Thus intensive review will be much appreciated.