Skip to content

Commit 858fd20

Browse files
committed
Implement std::thread-like wrappers around Furi Thread APIs
1 parent 6c2ca6b commit 858fd20

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
lines changed

crates/flipperzero/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ alloc = []
3535
[[example]]
3636
name = "dialog"
3737
required-features = ["alloc"]
38+
39+
[[example]]
40+
name = "threads"
41+
required-features = ["alloc"]
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Demonstrates use of threads on the Flipper Zero.
2+
3+
#![no_main]
4+
#![no_std]
5+
6+
// Required for panic handler
7+
extern crate flipperzero_rt;
8+
9+
// Required for allocator
10+
extern crate flipperzero_alloc;
11+
12+
extern crate alloc;
13+
14+
use alloc::borrow::ToOwned;
15+
use core::time::Duration;
16+
17+
use flipperzero::{furi::thread, println};
18+
use flipperzero_rt::{entry, manifest};
19+
20+
// Define the FAP Manifest for this application
21+
manifest!(name = "Threads example");
22+
23+
// Define the entry function
24+
entry!(main);
25+
26+
// Entry point
27+
fn main(_args: *mut u8) -> i32 {
28+
println!("Main app started!");
29+
30+
let first = thread::spawn(|| {
31+
println!("First thread started!");
32+
thread::sleep(Duration::from_secs(5));
33+
println!("First thread finished!");
34+
0
35+
});
36+
37+
thread::sleep(Duration::from_secs(1));
38+
39+
let second = thread::Builder::new()
40+
.name("Flipper".to_owned())
41+
.expect("name is valid")
42+
.spawn(|| {
43+
println!("Second thread started!");
44+
thread::sleep(Duration::from_secs(2));
45+
println!("Second thread finished!");
46+
0
47+
});
48+
49+
for (i, thread) in [&thread::current(), first.thread(), second.thread()]
50+
.into_iter()
51+
.enumerate()
52+
{
53+
if let Some(name) = thread.name() {
54+
println!("Running thread {} ({})", i, name);
55+
} else {
56+
println!("Running unnamed thread {}", i);
57+
}
58+
}
59+
60+
first.join();
61+
second.join();
62+
63+
println!("Main app finished!");
64+
0
65+
}

crates/flipperzero/src/furi/thread.rs

+252
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,130 @@
11
//! Furi Thread API.
22
3+
use core::{
4+
ffi::{c_void, CStr},
5+
fmt, str,
6+
};
7+
8+
#[cfg(feature = "alloc")]
9+
use alloc::{
10+
boxed::Box,
11+
ffi::{CString, NulError},
12+
string::String,
13+
};
14+
315
use flipperzero_sys as sys;
416

17+
const MIN_STACK_SIZE: usize = 1024;
18+
19+
/// Thread factory, which can be used in order to configure the properties of a new thread.
20+
#[cfg(feature = "alloc")]
21+
pub struct Builder {
22+
/// Guaranteed to be UTF-8.
23+
name: Option<CString>,
24+
stack_size: Option<usize>,
25+
}
26+
27+
#[cfg(feature = "alloc")]
28+
impl Builder {
29+
/// Generates the base configuration for spawning a thread, from which configuration
30+
/// methods can be chained.
31+
pub fn new() -> Self {
32+
Self {
33+
name: None,
34+
stack_size: None,
35+
}
36+
}
37+
38+
/// Names the thread-to-be.
39+
///
40+
/// Returns an error if the name contains null bytes (`\0`).
41+
pub fn name(mut self, name: String) -> Result<Self, NulError> {
42+
CString::new(name).map(|name| {
43+
self.name = Some(name);
44+
self
45+
})
46+
}
47+
48+
/// Sets the size of the stack (in bytes) for the new thread.
49+
pub fn stack_size(mut self, size: usize) -> Self {
50+
self.stack_size = Some(size);
51+
self
52+
}
53+
54+
/// Spawns a new thread by taking ownership of the `Builder`, and returns its
55+
/// [`JoinHandle`].
56+
pub fn spawn<F>(self, f: F) -> JoinHandle
57+
where
58+
F: FnOnce() -> i32,
59+
F: Send + 'static,
60+
{
61+
let Builder { name, stack_size } = self;
62+
let thread = Thread::new(name, stack_size);
63+
64+
// We need to box twice because trait objects are fat pointers, so we need the
65+
// second box to obtain a thin pointer to use as the context.
66+
type ThreadBody = Box<dyn FnOnce() -> i32>;
67+
let thread_body: Box<ThreadBody> = Box::new(Box::new(f));
68+
unsafe extern "C" fn run_thread_body(context: *mut c_void) -> i32 {
69+
let thread_body = unsafe { Box::from_raw(context as *mut ThreadBody) };
70+
thread_body()
71+
}
72+
73+
let callback: sys::FuriThreadCallback = Some(run_thread_body);
74+
let context = Box::into_raw(thread_body);
75+
unsafe {
76+
sys::furi_thread_set_callback(thread.thread, callback);
77+
sys::furi_thread_set_context(thread.thread, context as *mut c_void);
78+
sys::furi_thread_start(thread.thread);
79+
}
80+
81+
JoinHandle(thread)
82+
}
83+
}
84+
85+
/// Spawns a new thread, returning a [`JoinHandle`] for it.
86+
///
87+
/// This call will create a thread using default parameters of [`Builder`]. If you want to
88+
/// specify the stack size or the name of the thread, use that API instead.
89+
#[cfg(feature = "alloc")]
90+
pub fn spawn<F>(f: F) -> JoinHandle
91+
where
92+
F: FnOnce() -> i32,
93+
F: Send + 'static,
94+
{
95+
Builder::new().spawn(f)
96+
}
97+
98+
/// Gets a handle to the thread that invokes it.
99+
#[cfg(feature = "alloc")]
100+
pub fn current() -> Thread {
101+
use alloc::borrow::ToOwned;
102+
103+
let thread = unsafe { sys::furi_thread_get_current() };
104+
105+
let name = {
106+
let name = unsafe { sys::furi_thread_get_name(sys::furi_thread_get_current_id()) };
107+
(!name.is_null())
108+
.then(|| {
109+
// SAFETY: The Flipper Zero firmware ensures that all thread names have a
110+
// null terminator.
111+
unsafe { CStr::from_ptr(name) }.to_owned()
112+
})
113+
.and_then(|name| {
114+
// Ensure that the name is valid UTF-8. This will be true for threads
115+
// created via `Builder`, but may not be true for the current thread.
116+
name.to_str().is_ok().then_some(name)
117+
})
118+
};
119+
120+
Thread { name, thread }
121+
}
122+
123+
/// Cooperatively gives up a timeslice to the OS scheduler.
124+
pub fn yield_now() {
125+
unsafe { sys::furi_thread_yield() };
126+
}
127+
5128
/// Puts the current thread to sleep for at least the specified amount of time.
6129
pub fn sleep(duration: core::time::Duration) {
7130
unsafe {
@@ -13,3 +136,132 @@ pub fn sleep(duration: core::time::Duration) {
13136
}
14137
}
15138
}
139+
140+
/// A unique identifier for a running thread.
141+
#[cfg(feature = "alloc")]
142+
pub struct ThreadId(sys::FuriThreadId);
143+
144+
/// A handle to a thread.
145+
#[cfg(feature = "alloc")]
146+
pub struct Thread {
147+
/// Guaranteed to be UTF-8.
148+
name: Option<CString>,
149+
thread: *mut sys::FuriThread,
150+
}
151+
152+
#[cfg(feature = "alloc")]
153+
impl Drop for Thread {
154+
fn drop(&mut self) {
155+
// TODO: is this safe to do while the thread is running?
156+
unsafe { sys::furi_thread_free(self.thread) };
157+
}
158+
}
159+
160+
#[cfg(feature = "alloc")]
161+
impl Thread {
162+
fn new(name: Option<CString>, stack_size: Option<usize>) -> Self {
163+
let stack_size = stack_size.unwrap_or(MIN_STACK_SIZE);
164+
165+
unsafe {
166+
let thread = sys::furi_thread_alloc();
167+
if let Some(name) = name.as_deref() {
168+
sys::furi_thread_set_name(thread, name.as_ptr());
169+
}
170+
sys::furi_thread_set_stack_size(thread, stack_size);
171+
Thread { name, thread }
172+
}
173+
}
174+
175+
/// Gets the thread's unique identifier.
176+
///
177+
/// Returns `None` if the thread has terminated.
178+
pub fn id(&self) -> Option<ThreadId> {
179+
// TODO: The Rust stdlib generates its own unique IDs for threads that are valid
180+
// even after a thread terminates.
181+
let id = unsafe { sys::furi_thread_get_id(self.thread) };
182+
if id.is_null() {
183+
None
184+
} else {
185+
Some(ThreadId(id))
186+
}
187+
}
188+
189+
/// Gets the thread's name.
190+
///
191+
/// Returns `None` if the thread has terminated, or is unnamed, or has a name that is
192+
/// not valid UTF-8.
193+
pub fn name(&self) -> Option<&str> {
194+
self.cname()
195+
.map(|s| unsafe { str::from_utf8_unchecked(s.to_bytes()) })
196+
}
197+
198+
fn cname(&self) -> Option<&CStr> {
199+
self.name.as_deref()
200+
}
201+
}
202+
203+
#[cfg(feature = "alloc")]
204+
impl fmt::Debug for Thread {
205+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206+
f.debug_struct("Thread")
207+
.field("name", &self.name())
208+
.finish_non_exhaustive()
209+
}
210+
}
211+
212+
#[cfg(feature = "alloc")]
213+
impl ufmt::uDebug for Thread {
214+
fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
215+
where
216+
W: ufmt::uWrite + ?Sized,
217+
{
218+
// TODO: ufmt doesn't provide an impl of uDebug for &str.
219+
f.debug_struct("Thread")?.finish()
220+
}
221+
}
222+
223+
/// An owned permission to join on a thread (block on its termination).
224+
#[cfg(feature = "alloc")]
225+
pub struct JoinHandle(Thread);
226+
227+
#[cfg(feature = "alloc")]
228+
impl JoinHandle {
229+
/// Extracts a handle to the underlying thread.
230+
pub fn thread(&self) -> &Thread {
231+
&self.0
232+
}
233+
234+
/// Waits for the associated thread to finish.
235+
///
236+
/// This function will return immediately if the associated thread has already
237+
/// finished.
238+
pub fn join(self) -> i32 {
239+
let JoinHandle(Thread { thread, .. }) = self;
240+
unsafe {
241+
sys::furi_thread_join(thread);
242+
sys::furi_thread_get_return_code(thread)
243+
}
244+
}
245+
246+
/// Checks if the associated thread has finished running its main function.
247+
pub fn is_finished(&self) -> bool {
248+
self.0.id().is_none()
249+
}
250+
}
251+
252+
#[cfg(feature = "alloc")]
253+
impl fmt::Debug for JoinHandle {
254+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255+
f.debug_struct("JoinHandle").finish_non_exhaustive()
256+
}
257+
}
258+
259+
#[cfg(feature = "alloc")]
260+
impl ufmt::uDebug for JoinHandle {
261+
fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
262+
where
263+
W: ufmt::uWrite + ?Sized,
264+
{
265+
f.debug_struct("JoinHandle")?.finish()
266+
}
267+
}

0 commit comments

Comments
 (0)