diff --git a/Cargo.lock b/Cargo.lock index 2547e04c0..c16b2611a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2529,6 +2529,7 @@ dependencies = [ "exitcode", "futures-channel", "futures-util", + "generational-box", "global-hotkey", "infer 0.11.0", "muda", @@ -2662,6 +2663,7 @@ dependencies = [ "enumset", "euclid", "futures-channel", + "generational-box", "keyboard-types", "rfd", "serde", @@ -2725,6 +2727,7 @@ dependencies = [ "dioxus-interpreter-js", "futures-channel", "futures-util", + "generational-box", "minify-js", "once_cell", "pretty_env_logger", @@ -2912,6 +2915,7 @@ dependencies = [ "dioxus-signals", "fern", "fs_extra", + "generational-box", "http 0.2.11", "lru 0.10.1", "rustc-hash", @@ -2962,6 +2966,7 @@ dependencies = [ "dioxus-web", "futures-channel", "futures-util", + "generational-box", "gloo-dialogs", "gloo-timers", "js-sys", diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 9670f13d7..fdb68d734 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -20,6 +20,7 @@ dioxus-html = { workspace = true, features = [ dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } dioxus-cli-config = { workspace = true } +generational-box = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 6faf4a1eb..1a7b6aa6a 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,7 +1,5 @@ -#![allow(clippy::await_holding_refcell_ref)] -use async_trait::async_trait; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; -use std::{cell::RefCell, rc::Rc}; +use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use crate::{query::Query, DesktopContext}; @@ -17,55 +15,57 @@ impl DesktopEvalProvider { } impl EvalProvider for DesktopEvalProvider { - fn new_evaluator(&self, js: String) -> Result, EvalError> { - Ok(Box::new(DesktopEvaluator::new( - self.desktop_ctx.clone(), - js, - ))) + fn new_evaluator(&self, js: String) -> Result>, EvalError> { + Ok(DesktopEvaluator::create(self.desktop_ctx.clone(), js)) } } /// Represents a desktop-target's JavaScript evaluator. pub(crate) struct DesktopEvaluator { - query: Rc>>, + query: Query, } impl DesktopEvaluator { /// Creates a new evaluator for desktop-based targets. - pub fn new(desktop_ctx: DesktopContext, js: String) -> Self { + pub fn create(desktop_ctx: DesktopContext, js: String) -> GenerationalBox> { let ctx = desktop_ctx.clone(); let query = desktop_ctx.query.new_query(&js, ctx); - Self { - query: Rc::new(RefCell::new(query)), - } + // We create a generational box that is owned by the query slot so that when we drop the query slot, the generational box is also dropped. + let owner = UnsyncStorage::owner(); + let query_id = query.id; + let query = owner.insert(Box::new(DesktopEvaluator { query }) as Box); + desktop_ctx.query.active_requests.slab.borrow_mut()[query_id].owner = Some(owner); + + query } } -#[async_trait(?Send)] impl Evaluator for DesktopEvaluator { - async fn join(&self) -> Result { + fn poll_join( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { self.query - .borrow_mut() - .result() - .await + .poll_result(cx) .map_err(|e| EvalError::Communication(e.to_string())) } /// Sends a message to the evaluated JavaScript. fn send(&self, data: serde_json::Value) -> Result<(), EvalError> { - if let Err(e) = self.query.borrow_mut().send(data) { + if let Err(e) = self.query.send(data) { return Err(EvalError::Communication(e.to_string())); } Ok(()) } /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript. - async fn recv(&mut self) -> Result { + fn poll_recv( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { self.query - .borrow_mut() - .recv() - .await + .poll_recv(cx) .map_err(|e| EvalError::Communication(e.to_string())) } } diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 9470c972a..0e5ecbe98 100644 --- a/packages/desktop/src/query.rs +++ b/packages/desktop/src/query.rs @@ -1,7 +1,8 @@ use std::{cell::RefCell, rc::Rc}; use crate::DesktopContext; -use futures_util::StreamExt; +use futures_util::{FutureExt, StreamExt}; +use generational_box::Owner; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use slab::Slab; @@ -43,8 +44,8 @@ let dioxus = { /// Tracks what query ids are currently active -struct SharedSlab { - slab: Rc>>, +pub(crate) struct SharedSlab { + pub slab: Rc>>, } impl Clone for SharedSlab { @@ -63,9 +64,10 @@ impl Default for SharedSlab { } } -struct QueryEntry { +pub(crate) struct QueryEntry { channel_sender: futures_channel::mpsc::UnboundedSender, return_sender: Option>, + pub owner: Option, } const QUEUE_NAME: &str = "__msg_queues"; @@ -73,7 +75,7 @@ const QUEUE_NAME: &str = "__msg_queues"; /// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. #[derive(Clone, Default)] pub(crate) struct QueryEngine { - active_requests: SharedSlab, + pub active_requests: SharedSlab, } impl QueryEngine { @@ -88,6 +90,7 @@ impl QueryEngine { let request_id = self.active_requests.slab.borrow_mut().insert(QueryEntry { channel_sender: tx, return_sender: Some(return_tx), + owner: None, }); // start the query @@ -161,7 +164,7 @@ pub(crate) struct Query { slab: SharedSlab, receiver: futures_channel::mpsc::UnboundedReceiver, return_receiver: Option>, - id: usize, + pub id: usize, phantom: std::marker::PhantomData, } @@ -198,9 +201,14 @@ impl Query { Ok(()) } - /// Receive a message from the query - pub async fn recv(&mut self) -> Result { - self.receiver.next().await.ok_or(QueryError::Recv) + /// Poll the query for a message + pub fn poll_recv( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.receiver + .poll_next_unpin(cx) + .map(|result| result.ok_or(QueryError::Recv)) } /// Receive the result of the query @@ -210,6 +218,17 @@ impl Query { None => Err(QueryError::Finished), } } + + /// Poll the query for a result + pub fn poll_result( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.return_receiver.as_mut() { + Some(receiver) => receiver.poll_unpin(cx).map_err(|_| QueryError::Recv), + None => std::task::Poll::Ready(Err(QueryError::Finished)), + } + } } impl Drop for Query { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 8b8bd2df7..0963407e9 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["dom", "ui", "gui", "react"] dioxus-core = { workspace = true } dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true } dioxus-html-internal-macro = { workspace = true } +generational-box = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index 7e86246b9..f0e924aa9 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -1,26 +1,32 @@ #![allow(clippy::await_holding_refcell_ref)] -use async_trait::async_trait; use dioxus_core::prelude::*; -use std::future::{Future, IntoFuture}; +use generational_box::GenerationalBox; +use std::future::{poll_fn, Future, IntoFuture}; use std::pin::Pin; use std::rc::Rc; +use std::task::{Context, Poll}; /// A struct that implements EvalProvider is sent through [`ScopeState`]'s provide_context function /// so that [`use_eval`] can provide a platform agnostic interface for evaluating JavaScript code. pub trait EvalProvider { - fn new_evaluator(&self, js: String) -> Result, EvalError>; + fn new_evaluator(&self, js: String) -> Result>, EvalError>; } /// The platform's evaluator. -#[async_trait(?Send)] pub trait Evaluator { /// Sends a message to the evaluated JavaScript. fn send(&self, data: serde_json::Value) -> Result<(), EvalError>; /// Receive any queued messages from the evaluated JavaScript. - async fn recv(&mut self) -> Result; + fn poll_recv( + &mut self, + context: &mut Context<'_>, + ) -> Poll>; /// Gets the return value of the JavaScript - async fn join(&self) -> Result; + fn poll_join( + &mut self, + context: &mut Context<'_>, + ) -> Poll>; } type EvalCreator = Rc Result>; @@ -54,28 +60,36 @@ pub fn eval(script: &str) -> Result { /// A wrapper around the target platform's evaluator. pub struct UseEval { - evaluator: Box, + evaluator: GenerationalBox>, } impl UseEval { /// Creates a new UseEval - pub fn new(evaluator: Box) -> Self { + pub fn new(evaluator: GenerationalBox>) -> Self { Self { evaluator } } /// Sends a [`serde_json::Value`] to the evaluated JavaScript. pub fn send(&self, data: serde_json::Value) -> Result<(), EvalError> { - self.evaluator.send(data) + self.evaluator.read().send(data) } /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript. pub async fn recv(&mut self) -> Result { - self.evaluator.recv().await + poll_fn(|cx| match self.evaluator.try_write() { + Ok(mut evaluator) => evaluator.poll_recv(cx), + Err(_) => Poll::Ready(Err(EvalError::Finished)), + }) + .await } /// Gets the return value of the evaluated JavaScript. pub async fn join(self) -> Result { - self.evaluator.join().await + poll_fn(|cx| match self.evaluator.try_write() { + Ok(mut evaluator) => evaluator.poll_join(cx), + Err(_) => Poll::Ready(Err(EvalError::Finished)), + }) + .await } } @@ -92,7 +106,7 @@ impl IntoFuture for UseEval { #[derive(Debug)] pub enum EvalError { /// The platform does not support evaluating JavaScript. - Unspported, + Unsupported, /// The provided JavaScript has already been ran. Finished, diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 86c71650f..c523d08be 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -29,6 +29,7 @@ dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } dioxus-cli-config = { workspace = true } +generational-box = { workspace = true } # warp warp = { version = "0.3.3", optional = true } diff --git a/packages/liveview/src/eval.rs b/packages/liveview/src/eval.rs index 860dc96a4..e521b35dc 100644 --- a/packages/liveview/src/eval.rs +++ b/packages/liveview/src/eval.rs @@ -1,9 +1,7 @@ -#![allow(clippy::await_holding_refcell_ref)] - -use async_trait::async_trait; use dioxus_core::ScopeId; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; -use std::{cell::RefCell, rc::Rc}; +use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; +use std::rc::Rc; use crate::query::{Query, QueryEngine}; @@ -14,48 +12,51 @@ pub fn init_eval() { ScopeId::ROOT.provide_context(provider); } -/// Reprents the desktop-target's provider of evaluators. +/// Reprints the desktop-target's provider of evaluators. pub struct DesktopEvalProvider { query: QueryEngine, } impl EvalProvider for DesktopEvalProvider { - fn new_evaluator(&self, js: String) -> Result, EvalError> { - Ok(Box::new(DesktopEvaluator::new(self.query.clone(), js))) + fn new_evaluator(&self, js: String) -> Result>, EvalError> { + Ok(DesktopEvaluator::create(self.query.clone(), js)) } } /// Reprents a desktop-target's JavaScript evaluator. pub(crate) struct DesktopEvaluator { - query: Rc>>, + query: Query, } impl DesktopEvaluator { /// Creates a new evaluator for desktop-based targets. - pub fn new(query: QueryEngine, js: String) -> Self { - let query = query.new_query(&js); + pub fn create(query_engine: QueryEngine, js: String) -> GenerationalBox> { + let query = query_engine.new_query(&js); + // We create a generational box that is owned by the query slot so that when we drop the query slot, the generational box is also dropped. + let owner = UnsyncStorage::owner(); + let query_id = query.id; + let query = owner.insert(Box::new(DesktopEvaluator { query }) as Box); + query_engine.active_requests.slab.borrow_mut()[query_id].owner = Some(owner); - Self { - query: Rc::new(RefCell::new(query)), - } + query } } -#[async_trait(?Send)] impl Evaluator for DesktopEvaluator { /// # Panics /// This will panic if the query is currently being awaited. - async fn join(&self) -> Result { + fn poll_join( + &mut self, + context: &mut std::task::Context<'_>, + ) -> std::task::Poll> { self.query - .borrow_mut() - .result() - .await + .poll_result(context) .map_err(|e| EvalError::Communication(e.to_string())) } /// Sends a message to the evaluated JavaScript. fn send(&self, data: serde_json::Value) -> Result<(), EvalError> { - if let Err(e) = self.query.borrow_mut().send(data) { + if let Err(e) = self.query.send(data) { return Err(EvalError::Communication(e.to_string())); } Ok(()) @@ -65,11 +66,12 @@ impl Evaluator for DesktopEvaluator { /// /// # Panics /// This will panic if the query is currently being awaited. - async fn recv(&mut self) -> Result { + fn poll_recv( + &mut self, + context: &mut std::task::Context<'_>, + ) -> std::task::Poll> { self.query - .borrow_mut() - .recv() - .await + .poll_recv(context) .map_err(|e| EvalError::Communication(e.to_string())) } } diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs index c99dd5151..ee8709182 100644 --- a/packages/liveview/src/query.rs +++ b/packages/liveview/src/query.rs @@ -1,5 +1,7 @@ use std::{cell::RefCell, rc::Rc}; +use futures_util::FutureExt; +use generational_box::{Owner, UnsyncStorage}; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use slab::Slab; @@ -42,8 +44,8 @@ let dioxus = { /// Tracks what query ids are currently active -struct SharedSlab { - slab: Rc>>, +pub(crate) struct SharedSlab { + pub(crate) slab: Rc>>, } impl Clone for SharedSlab { @@ -62,9 +64,10 @@ impl Default for SharedSlab { } } -struct QueryEntry { +pub(crate) struct QueryEntry { channel_sender: tokio::sync::mpsc::UnboundedSender, return_sender: Option>, + pub(crate) owner: Option>, } const QUEUE_NAME: &str = "__msg_queues"; @@ -72,7 +75,7 @@ const QUEUE_NAME: &str = "__msg_queues"; /// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. #[derive(Clone)] pub(crate) struct QueryEngine { - active_requests: SharedSlab, + pub(crate) active_requests: SharedSlab, query_tx: tokio::sync::mpsc::UnboundedSender, } @@ -91,6 +94,7 @@ impl QueryEngine { let request_id = self.active_requests.slab.borrow_mut().insert(QueryEntry { channel_sender: tx, return_sender: Some(return_tx), + owner: None, }); // start the query @@ -162,7 +166,7 @@ pub(crate) struct Query { query_engine: QueryEngine, pub receiver: tokio::sync::mpsc::UnboundedReceiver, pub return_receiver: Option>, - id: usize, + pub id: usize, phantom: std::marker::PhantomData, } @@ -198,12 +202,14 @@ impl Query { Ok(()) } - /// Receive a message from the query - pub async fn recv(&mut self) -> Result { + /// Poll the query for a message + pub fn poll_recv( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { self.receiver - .recv() - .await - .ok_or(QueryError::Recv(RecvError::Closed)) + .poll_recv(cx) + .map(|result| result.ok_or(QueryError::Recv(RecvError::Closed))) } /// Receive the result of the query @@ -215,6 +221,19 @@ impl Query { None => Err(QueryError::Finished), } } + + /// Poll the query for a result + pub fn poll_result( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.return_receiver.as_mut() { + Some(receiver) => receiver + .poll_unpin(cx) + .map_err(|_| QueryError::Recv(RecvError::Closed)), + None => std::task::Poll::Ready(Err(QueryError::Finished)), + } + } } impl Drop for Query { diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 24b005a26..89cc10edc 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["eval"]} +generational-box = { workspace = true } askama_escape = "0.10.3" thiserror = "1.0.23" rustc-hash = "1.1.0" diff --git a/packages/ssr/src/eval.rs b/packages/ssr/src/eval.rs index 8b5d05b50..c55b7167f 100644 --- a/packages/ssr/src/eval.rs +++ b/packages/ssr/src/eval.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use dioxus_core::ScopeId; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; +use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use std::rc::Rc; /// Provides the SSREvalProvider through [`cx.provide_context`]. @@ -12,8 +13,9 @@ pub fn init_eval() { /// Reprents the ssr-target's provider of evaluators. pub struct SSREvalProvider; impl EvalProvider for SSREvalProvider { - fn new_evaluator(&self, _: String) -> Result, EvalError> { - Ok(Box::new(SSREvaluator) as Box) + fn new_evaluator(&self, _: String) -> Result>, EvalError> { + let owner = UnsyncStorage::owner(); + Ok(owner.insert(Box::new(SSREvaluator) as Box)) } } @@ -23,20 +25,22 @@ pub struct SSREvaluator; // In ssr rendering we never run or resolve evals. #[async_trait(?Send)] impl Evaluator for SSREvaluator { - /// Runs the evaluated JavaScript. - async fn join(&self) -> Result { - std::future::pending::<()>().await; - unreachable!() - } - /// Sends a message to the evaluated JavaScript. fn send(&self, _el: serde_json::Value) -> Result<(), EvalError> { Ok(()) } - /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript. - async fn recv(&mut self) -> Result { - std::future::pending::<()>().await; - unreachable!() + fn poll_recv( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Pending + } + + fn poll_join( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Pending } } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 8bc53ab9d..6c45fbe85 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -16,6 +16,7 @@ dioxus-interpreter-js = { workspace = true, features = [ "minimal_bindings", "webonly", ] } +generational-box = { workspace = true } js-sys = "0.3.56" wasm-bindgen = { workspace = true } diff --git a/packages/web/src/eval.rs b/packages/web/src/eval.rs index 3c65b900d..21806925b 100644 --- a/packages/web/src/eval.rs +++ b/packages/web/src/eval.rs @@ -1,10 +1,10 @@ -use async_trait::async_trait; use dioxus_core::ScopeId; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; use futures_util::StreamExt; +use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use js_sys::Function; use serde_json::Value; -use std::{cell::RefCell, rc::Rc, str::FromStr}; +use std::{rc::Rc, str::FromStr}; use wasm_bindgen::prelude::*; /// Provides the WebEvalProvider through [`cx.provide_context`]. @@ -16,8 +16,8 @@ pub fn init_eval() { /// Represents the web-target's provider of evaluators. pub struct WebEvalProvider; impl EvalProvider for WebEvalProvider { - fn new_evaluator(&self, js: String) -> Result, EvalError> { - WebEvaluator::new(js).map(|eval| Box::new(eval) as Box) + fn new_evaluator(&self, js: String) -> Result>, EvalError> { + WebEvaluator::create(js) } } @@ -30,19 +30,23 @@ const PROMISE_WRAPPER: &str = r#" "#; /// Represents a web-target's JavaScript evaluator. -pub struct WebEvaluator { +struct WebEvaluator { dioxus: Dioxus, channel_receiver: futures_channel::mpsc::UnboundedReceiver, - result: RefCell>, + result: Option, } impl WebEvaluator { /// Creates a new evaluator for web-based targets. - pub fn new(js: String) -> Result { + fn create(js: String) -> Result>, EvalError> { let (mut channel_sender, channel_receiver) = futures_channel::mpsc::unbounded(); + let owner = UnsyncStorage::owner(); + let invalid = owner.invalid(); // This Rc cloning mess hurts but it seems to work.. let recv_value = Closure::::new(move |data| { + // Drop the owner when the sender is dropped. + let _ = &owner; match serde_wasm_bindgen::from_value::(data) { Ok(data) => _ = channel_sender.start_send(data), Err(e) => { @@ -84,19 +88,27 @@ impl WebEvaluator { } }; - Ok(Self { + invalid.set(Box::new(Self { dioxus, channel_receiver, - result: RefCell::new(Some(result)), - }) + result: Some(result), + }) as Box); + + Ok(invalid) } } -#[async_trait(?Send)] impl Evaluator for WebEvaluator { /// Runs the evaluated JavaScript. - async fn join(&self) -> Result { - self.result.take().ok_or(EvalError::Finished) + fn poll_join( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if let Some(result) = self.result.take() { + std::task::Poll::Ready(Ok(result)) + } else { + std::task::Poll::Ready(Err(EvalError::Finished)) + } } /// Sends a message to the evaluated JavaScript. @@ -111,11 +123,15 @@ impl Evaluator for WebEvaluator { } /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript. - async fn recv(&mut self) -> Result { - self.channel_receiver - .next() - .await - .ok_or_else(|| EvalError::Communication("failed to receive data from js".to_string())) + fn poll_recv( + &mut self, + context: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.channel_receiver.poll_next_unpin(context).map(|poll| { + poll.ok_or_else(|| { + EvalError::Communication("failed to receive data from js".to_string()) + }) + }) } }