spawn_local: initialize the platform if not initialized before the call

Take the opportunity to refactor a bit the way the global platform or
context is accessed

Fixes: #5871
This commit is contained in:
Olivier Goffart 2024-08-20 18:00:20 +02:00
parent d978a856fc
commit 43c7f57b0f
17 changed files with 163 additions and 145 deletions

View file

@ -251,6 +251,93 @@ pub fn run_event_loop_until_quit() -> Result<(), PlatformError> {
})
}
/// Spawns a [`Future`](core::future::Future) to execute in the Slint event loop.
///
/// This function is intended to be invoked only from the main Slint thread that runs the event loop.
///
/// For spawning a `Send` future from a different thread, this function should be called from a closure
/// passed to [`invoke_from_event_loop()`].
///
/// This function is typically called from a UI callback.
///
/// # Example
///
/// ```rust,no_run
/// slint::spawn_local(async move {
/// // your async code goes here
/// }).unwrap();
/// ```
///
/// # Compatibility with Tokio and other runtimes
///
/// The runtime used to execute the future on the main thread is platform-dependent,
/// for instance, it could be the winit event loop. Therefore, futures that assume a specific runtime
/// may not work. This may be an issue if you call `.await` on a future created by another
/// runtime, or pass the future directly to `spawn_local`.
///
/// Futures from the [smol](https://docs.rs/smol/latest/smol/) runtime always hand off their work to
/// separate I/O threads that run in parallel to the Slint event loop.
///
/// The [Tokio](https://docs.rs/tokio/latest/tokio/index.html) runtime is to the following constraints:
///
/// * Tokio futures require entering the context of a global Tokio runtime.
/// * Tokio futures aren't guaranteed to hand off their work to separate threads and may therefore not complete, because
/// the Slint runtime can't drive the Tokio runtime.
/// * Tokio futures require regular yielding to the Tokio runtime for fairness, a constraint that also can't be met by Slint.
/// * Tokio's [current-thread schedule](https://docs.rs/tokio/latest/tokio/runtime/index.html#current-thread-scheduler)
/// cannot be used in Slint main thread, because Slint cannot yield to it.
///
/// To address these constraints, use [async_compat](https://docs.rs/async-compat/latest/async_compat/index.html)'s [Compat::new()](https://docs.rs/async-compat/latest/async_compat/struct.Compat.html#method.new)
/// to implicitly allocate a shared, multi-threaded Tokio runtime that will be used for Tokio futures.
///
/// The following little example demonstrates the use of Tokio's [`TcpStream`](https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html) to
/// read from a network socket. The entire future passed to `spawn_local()` is wrapped in `Compat::new()` to make it run:
///
/// ```rust,no_run
/// // A dummy TCP server that once reports "Hello World"
/// # i_slint_backend_testing::init_integration_test_with_mock_time();
/// use std::io::Write;
///
/// let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
/// let local_addr = listener.local_addr().unwrap();
/// let server = std::thread::spawn(move || {
/// let mut stream = listener.incoming().next().unwrap().unwrap();
/// stream.write("Hello World".as_bytes()).unwrap();
/// });
///
/// let slint_future = async move {
/// use tokio::io::AsyncReadExt;
/// let mut stream = tokio::net::TcpStream::connect(local_addr).await.unwrap();
/// let mut data = Vec::new();
/// stream.read_to_end(&mut data).await.unwrap();
/// assert_eq!(data, "Hello World".as_bytes());
/// slint::quit_event_loop().unwrap();
/// };
///
/// // Wrap the future that includes Tokio futures in async_compat's `Compat` to ensure
/// // presence of a Tokio run-time.
/// slint::spawn_local(async_compat::Compat::new(slint_future)).unwrap();
///
/// slint::run_event_loop_until_quit().unwrap();
///
/// server.join().unwrap();
/// ```
///
/// The use of `#[tokio::main]` is **not recommended**. If it's necessary to use though, wrap the call to enter the Slint
/// event loop in a call to [`tokio::task::block_in_place`](https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html):
///
/// ```rust, no_run
/// // Wrap the call to run_event_loop to ensure presence of a Tokio run-time.
/// tokio::task::block_in_place(slint::run_event_loop).unwrap();
/// ```
#[cfg(target_has_atomic = "ptr")]
pub fn spawn_local<F: core::future::Future + 'static>(
fut: F,
) -> Result<JoinHandle<F::Output>, EventLoopError> {
i_slint_backend_selector::with_global_context(|ctx| ctx.spawn_local(fut))
.map_err(|_| EventLoopError::NoEventLoopProvider)?
}
/// Include the code generated with the slint-build crate from the build script. After calling `slint_build::compile`
/// in your `build.rs` build script, the use of this macro includes the generated Rust code and makes the exported types
/// available for you to instantiate.

View file

@ -41,6 +41,19 @@ mod executor {
fn main() {
i_slint_backend_testing::init_integration_test_with_mock_time();
// test_spawn_local_from_thread
std::thread::spawn(|| {
assert_eq!(
slint::spawn_local(async {
panic!("the future shouldn't be run since we're in a thread")
})
.map(drop),
Err(slint::EventLoopError::NoEventLoopProvider)
);
})
.join()
.unwrap();
slint::invoke_from_event_loop(|| {
let handle = slint::spawn_local(async { String::from("Hello") }).unwrap();
slint::spawn_local(async move { panic!("Aborted task") }).unwrap().abort();

View file

@ -18,6 +18,7 @@ extern crate alloc;
use alloc::boxed::Box;
use i_slint_core::platform::Platform;
use i_slint_core::platform::PlatformError;
use i_slint_core::SlintContext;
#[cfg(all(feature = "i-slint-backend-qt", not(no_qt), not(target_os = "android")))]
fn create_qt_backend() -> Result<Box<dyn Platform + 'static>, PlatformError> {
@ -133,8 +134,14 @@ cfg_if::cfg_if! {
pub fn with_platform<R>(
f: impl FnOnce(&dyn Platform) -> Result<R, PlatformError>,
) -> Result<R, PlatformError> {
with_global_context(|ctx| f(ctx.platform()))?
}
/// Run the callback with the [`SlintContext`].
/// Create the backend if it does not exist yet
pub fn with_global_context<R>(f: impl FnOnce(&SlintContext) -> R) -> Result<R, PlatformError> {
let mut platform_created = false;
let result = i_slint_core::with_platform(
let result = i_slint_core::with_global_context(
|| {
let backend = create_backend();
platform_created = backend.is_ok();

View file

@ -664,9 +664,9 @@ impl ElementHandle {
/// Simulates a double click (or touch tap) on the element at its center point.
pub async fn double_click(&self, button: i_slint_core::platform::PointerEventButton) {
let Ok(click_interval) = i_slint_core::with_platform(
let Ok(click_interval) = i_slint_core::with_global_context(
|| Err(i_slint_core::platform::PlatformError::NoPlatform),
|platform| Ok(platform.click_interval()),
|ctx| ctx.platform().click_interval(),
) else {
return;
};

View file

@ -70,16 +70,21 @@ impl TestingClient {
let this = self.clone();
self.message_loop_future.get_or_init(|| {
i_slint_core::future::spawn_local({
let this = this.clone();
async move {
message_loop(&this.server_addr, |request| {
let this = this.clone();
Box::pin(async move { this.handle_request(request).await })
})
.await;
}
})
i_slint_core::with_global_context(
|| panic!("uninitialized platform"),
|context| {
let this = this.clone();
context
.spawn_local(async move {
message_loop(&this.server_addr, |request| {
let this = this.clone();
Box::pin(async move { this.handle_request(request).await })
})
.await;
})
.unwrap()
},
)
.unwrap()
});
}

View file

@ -217,7 +217,7 @@ impl Instant {
fn duration_since_start() -> core::time::Duration {
crate::context::GLOBAL_CONTEXT
.with(|p| p.get().map(|p| p.0.platform.duration_since_start()))
.with(|p| p.get().map(|p| p.platform().duration_since_start()))
.unwrap_or_default()
}

View file

@ -16,7 +16,7 @@ thread_local! {
}
pub(crate) struct SlintContextInner {
pub(crate) platform: Box<dyn Platform>,
platform: Box<dyn Platform>,
pub(crate) window_count: core::cell::RefCell<isize>,
/// This property is read by all translations, and marked dirty when the language change
/// so that every translated string gets re-translated
@ -42,6 +42,11 @@ impl SlintContext {
}))
}
/// Return a reference to the platform abstraction
pub fn platform(&self) -> &dyn Platform {
&*self.0.platform
}
/// Return an event proxy
// FIXME: Make EvenLoopProxy clonable, and maybe wrap in a struct
pub fn event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
@ -49,7 +54,7 @@ impl SlintContext {
}
#[cfg(target_has_atomic = "ptr")]
/// Context specific version of [`slint::spawn_local`](crate::future::spawn_local)
/// Context specific version of `slint::spawn_local`
pub fn spawn_local<F: core::future::Future + 'static>(
&self,
fut: F,
@ -62,18 +67,18 @@ impl SlintContext {
}
}
/// Internal function to access the platform abstraction.
/// Internal function to access the context.
/// The factory function is called if the platform abstraction is not yet
/// initialized, and should be given by the platform_selector
pub fn with_platform<R>(
pub fn with_global_context<R>(
factory: impl FnOnce() -> Result<Box<dyn Platform + 'static>, PlatformError>,
f: impl FnOnce(&dyn Platform) -> Result<R, PlatformError>,
f: impl FnOnce(&SlintContext) -> R,
) -> Result<R, PlatformError> {
GLOBAL_CONTEXT.with(|p| match p.get() {
Some(ctx) => f(&*ctx.0.platform),
Some(ctx) => Ok(f(ctx)),
None => {
crate::platform::set_platform(factory()?).map_err(PlatformError::SetPlatformError)?;
f(&*p.get().unwrap().0.platform)
Ok(f(p.get().unwrap()))
}
})
}

View file

@ -87,7 +87,7 @@ impl<T: 'static> Wake for FutureRunner<T> {
}
}
/// The return value of the [`spawn_local()`] function
/// The return value of the `spawn_local()` function
///
/// Can be used to abort the future, or to get the value from a different thread with `.await`
///
@ -128,95 +128,7 @@ impl<T> JoinHandle<T> {
// Safety: JoinHandle doesn't access the future, only the
unsafe impl<T: Send> Send for JoinHandle<T> {}
/// Spawns a [`Future`] to execute in the Slint event loop.
///
/// This function is intended to be invoked only from the main Slint thread that runs the event loop.
/// The event loop must be initialized prior to calling this function.
///
/// For spawning a `Send` future from a different thread, this function should be called from a closure
/// passed to [`invoke_from_event_loop()`](crate::api::invoke_from_event_loop).
///
/// This function is typically called from a UI callback.
///
/// # Example
///
/// ```rust,no_run
/// slint::spawn_local(async move {
/// // your async code goes here
/// }).unwrap();
/// ```
///
/// # Compatibility with Tokio and other runtimes
///
/// The runtime used to execute the future on the main thread is platform-dependent,
/// for instance, it could be the winit event loop. Therefore, futures that assume a specific runtime
/// may not work. This may be an issue if you call `.await` on a future created by another
/// runtime, or pass the future directly to `spawn_local`.
///
/// Futures from the [smol](https://docs.rs/smol/latest/smol/) runtime always hand off their work to
/// separate I/O threads that run in parallel to the Slint event loop.
///
/// The [Tokio](https://docs.rs/tokio/latest/tokio/index.html) runtime is to the following constraints:
///
/// * Tokio futures require entering the context of a global Tokio runtime.
/// * Tokio futures aren't guaranteed to hand off their work to separate threads and may therefore not complete, because
/// the Slint runtime can't drive the Tokio runtime.
/// * Tokio futures require regular yielding to the Tokio runtime for fairness, a constraint that also can't be met by Slint.
/// * Tokio's [current-thread schedule](https://docs.rs/tokio/latest/tokio/runtime/index.html#current-thread-scheduler)
/// cannot be used in Slint main thread, because Slint cannot yield to it.
///
/// To addresse these constraints, use [async_compat](https://docs.rs/async-compat/latest/async_compat/index.html)'s [Compat::new()](https://docs.rs/async-compat/latest/async_compat/struct.Compat.html#method.new)
/// to implicitly allocate a shared, multi-threaded Tokio runtime that will be used for Tokio futures.
///
/// The following little example demonstrates the use of Tokio's [`TcpStream`](https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html) to
/// read from a network socket. The entire future passed to `spawn_local()` is wrapped in `Compat::new()` to make it run:
///
/// ```rust,no_run
/// // A dummy TCP server that once reports "Hello World"
/// # i_slint_backend_testing::init_integration_test_with_mock_time();
/// use std::io::Write;
///
/// let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
/// let local_addr = listener.local_addr().unwrap();
/// let server = std::thread::spawn(move || {
/// let mut stream = listener.incoming().next().unwrap().unwrap();
/// stream.write("Hello World".as_bytes()).unwrap();
/// });
///
/// let slint_future = async move {
/// use tokio::io::AsyncReadExt;
/// let mut stream = tokio::net::TcpStream::connect(local_addr).await.unwrap();
/// let mut data = Vec::new();
/// stream.read_to_end(&mut data).await.unwrap();
/// assert_eq!(data, "Hello World".as_bytes());
/// slint::quit_event_loop().unwrap();
/// };
///
/// // Wrap the future that includes Tokio futures in async_compat's `Compat` to ensure
/// // presence of a Tokio run-time.
/// slint::spawn_local(async_compat::Compat::new(slint_future)).unwrap();
///
/// slint::run_event_loop_until_quit().unwrap();
///
/// server.join().unwrap();
/// ```
///
/// The use of `#[tokio::main]` is **not recommended**. If it's necessary to use though, wrap the call to enter the Slint
/// event loop in a call to [`tokio::task::block_in_place`](https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html):
///
/// ```rust, no_run
/// // Wrap the call to run_event_loop to ensure presence of a Tokio run-time.
/// tokio::task::block_in_place(slint::run_event_loop).unwrap();
/// ```
pub fn spawn_local<F: Future + 'static>(fut: F) -> Result<JoinHandle<F::Output>, EventLoopError> {
// ensure we are in the backend's thread
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let ctx = ctx.get().ok_or(EventLoopError::NoEventLoopProvider)?;
spawn_local_with_ctx(ctx, fut)
})
}
/// Implementation for [SlintContext::spawn_locale]
/// Implementation for [`SlintContext::spawn_local`]
pub(crate) fn spawn_local_with_ctx<F: Future + 'static>(
ctx: &SlintContext,
fut: F,
@ -232,16 +144,3 @@ pub(crate) fn spawn_local_with_ctx<F: Future + 'static>(
arc.wake_by_ref();
Ok(JoinHandle(arc))
}
#[test]
fn test_spawn_local_from_thread() {
std::thread::spawn(|| {
assert_eq!(
spawn_local(async { panic!("the future shouldn't be run since we're in a thread") })
.map(drop),
Err(EventLoopError::NoEventLoopProvider)
);
})
.join()
.unwrap();
}

View file

@ -1532,8 +1532,7 @@ impl TextInput {
WindowInner::from_pub(window_adapter.window())
.ctx
.0
.platform
.platform()
.set_clipboard_text(&text[anchor..cursor], clipboard);
}
@ -1548,7 +1547,7 @@ impl TextInput {
clipboard: Clipboard,
) {
if let Some(text) =
WindowInner::from_pub(window_adapter.window()).ctx.0.platform.clipboard_text(clipboard)
WindowInner::from_pub(window_adapter.window()).ctx.platform().clipboard_text(clipboard)
{
self.preedit_text.set(Default::default());
self.insert(&text, window_adapter, self_rc);

View file

@ -82,7 +82,7 @@ pub use graphics::PathData;
#[doc(inline)]
pub use graphics::BorderRadius;
pub use context::{with_platform, SlintContext};
pub use context::{with_global_context, SlintContext};
#[cfg(not(slint_int_coord))]
pub type Coord = f32;

View file

@ -249,7 +249,7 @@ pub fn update_timers_and_animations() {
pub fn duration_until_next_timer_update() -> Option<core::time::Duration> {
crate::timers::TimerList::next_timeout().map(|timeout| {
let duration_since_start = crate::context::GLOBAL_CONTEXT
.with(|p| p.get().map(|p| p.0.platform.duration_since_start()))
.with(|p| p.get().map(|p| p.platform().duration_since_start()))
.unwrap_or_default();
core::time::Duration::from_millis(
timeout.0.saturating_sub(duration_since_start.as_millis() as u64),

View file

@ -92,7 +92,7 @@ pub extern "C" fn send_keyboard_string_sequence(
#[doc(hidden)]
pub fn debug_log_impl(args: core::fmt::Arguments) {
crate::context::GLOBAL_CONTEXT.with(|p| match p.get() {
Some(ctx) => ctx.0.platform.debug_log(args),
Some(ctx) => ctx.platform().debug_log(args),
None => default_debug_log(args),
});
}

View file

@ -554,7 +554,7 @@ impl WindowInner {
crate::animations::update_animations();
// handle multiple press release
event = self.click_state.check_repeat(event, self.ctx.0.platform.click_interval());
event = self.click_state.check_repeat(event, self.ctx.platform().click_interval());
let pressed_event = matches!(event, MouseEvent::Pressed { .. });
let released_event = matches!(event, MouseEvent::Released { .. });
@ -625,7 +625,7 @@ impl WindowInner {
if last_top_item != mouse_input_state.top_item_including_delayed() {
self.click_state.reset();
self.click_state.check_repeat(event, self.ctx.0.platform.click_interval());
self.click_state.check_repeat(event, self.ctx.platform().click_interval());
}
self.mouse_input_state.set(mouse_input_state);

View file

@ -12,6 +12,7 @@ use i_slint_core::window::WindowInner;
use i_slint_core::{PathData, SharedVector};
use std::borrow::Cow;
use std::collections::HashMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::rc::Rc;
@ -572,11 +573,8 @@ impl ComponentCompiler {
/// was not in place (i.e: load from the file system following the include paths)
pub fn set_file_loader(
&mut self,
file_loader_fallback: impl Fn(
&Path,
) -> core::pin::Pin<
Box<dyn core::future::Future<Output = Option<std::io::Result<String>>>>,
> + 'static,
file_loader_fallback: impl Fn(&Path) -> core::pin::Pin<Box<dyn Future<Output = Option<std::io::Result<String>>>>>
+ 'static,
) {
self.config.open_import_fallback =
Some(Rc::new(move |path| file_loader_fallback(Path::new(path.as_str()))));
@ -737,11 +735,8 @@ impl Compiler {
/// was not in place (i.e: load from the file system following the include paths)
pub fn set_file_loader(
&mut self,
file_loader_fallback: impl Fn(
&Path,
) -> core::pin::Pin<
Box<dyn core::future::Future<Output = Option<std::io::Result<String>>>>,
> + 'static,
file_loader_fallback: impl Fn(&Path) -> core::pin::Pin<Box<dyn Future<Output = Option<std::io::Result<String>>>>>
+ 'static,
) {
self.config.open_import_fallback =
Some(Rc::new(move |path| file_loader_fallback(Path::new(path.as_str()))));
@ -1572,6 +1567,14 @@ pub fn run_event_loop() -> Result<(), PlatformError> {
i_slint_backend_selector::with_platform(|b| b.run_event_loop())
}
/// Spawns a [`Future`] to execute in the Slint event loop.
///
/// See the documentation of `slint::spawn_local()` for more info
pub fn spawn_local<F: Future + 'static>(fut: F) -> Result<JoinHandle<F::Output>, EventLoopError> {
i_slint_backend_selector::with_global_context(|ctx| ctx.spawn_local(fut))
.map_err(|_| EventLoopError::NoEventLoopProvider)?
}
#[cfg(all(feature = "internal", target_arch = "wasm32"))]
/// Spawn the event loop.
///

View file

@ -53,7 +53,7 @@ pub fn run_in_ui_thread<F: Future<Output = ()> + 'static>(
}
}
i_slint_core::api::invoke_from_event_loop(move || {
i_slint_core::future::spawn_local(create_future()).unwrap();
slint::spawn_local(create_future()).unwrap();
})
.unwrap();
Ok(())

View file

@ -180,7 +180,7 @@ fn invoke_from_event_loop_wrapped_in_promise(
pub fn run_in_ui_thread<F: Future<Output = ()> + 'static>(
create_future: impl Send + FnOnce() -> F + 'static,
) -> Result<(), String> {
i_slint_core::future::spawn_local(create_future()).map_err(|e| e.to_string())?;
slint::spawn_local(create_future()).map_err(|e| e.to_string())?;
Ok(())
}

View file

@ -284,7 +284,7 @@ fn start_fswatch_thread(args: Cli) -> Result<Arc<Mutex<notify::RecommendedWatche
let args = args.clone();
let w2 = w2.clone();
i_slint_core::api::invoke_from_event_loop(move || {
i_slint_core::future::spawn_local(reload(args, w2)).unwrap();
slint_interpreter::spawn_local(reload(args, w2)).unwrap();
})
.unwrap();
}