Skip to content

Commit

Permalink
Finish up docs for async binding.
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbuchan committed Jun 2, 2021
1 parent 0694577 commit 578a5bf
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 7 deletions.
5 changes: 5 additions & 0 deletions src/chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,26 @@ impl BindingContext {
}
}

/// The arguments from JS.
pub fn args(&self) -> &[JSObject] {
match &self.active {
None => &[],
Some(active) => active.payload["args"].as_array().expect("Expected array"),
}
}

/// Completes the JS function successfully. Equivalent to `complete(Ok(result))`
pub fn done(self, result: JSObject) {
self.complete(Ok(result))
}

/// Completes the JS function with an error. Equivalent to `complete(Err(error))`
pub fn err(self, error: JSObject) {
self.complete(Err(error))
}

/// Completes the JS function, either successfully or not. Takes the [`BindingContext`] by
/// value as it releases the outstanding call on the Chrome(ium) side.
pub fn complete(mut self, result: JSResult) {
if let Some(incomplete) = self.active.take() {
complete_binding(incomplete, result)
Expand Down
98 changes: 91 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@
mod chrome;
#[cfg(target_family = "windows")]
use chrome::close_handle;
use chrome::{
bind, bounds, close, eval, load, load_css, load_js, set_bounds, BindingContext, Chrome,
};
pub use chrome::{Bounds, JSError, JSObject, JSResult, WindowState};
use chrome::{bind, bounds, close, eval, load, load_css, load_js, set_bounds, Chrome};
pub use chrome::{BindingContext, Bounds, JSError, JSObject, JSResult, WindowState};
mod locate;
pub use locate::tinyfiledialogs as dialog;
use locate::{locate_chrome, LocateChromeError};
Expand Down Expand Up @@ -231,16 +229,102 @@ impl UI {
)
}

/// Bind a rust function callable from JS that can complete asynchronously.
/// The rust function will be executed in the message processing loop, and therefore should
/// avoid blocking by moving work onto another thread.
/// Bind a rust function callable from JS that can complete asynchronously. If you are using
/// [`tokio`], you will probably want to be using [`Self::bind_tokio()`] instead.
///
/// Unlike `bind()`, this passes ownership of the arguments to the callback function `f`, and
/// allows completing the javascript implementation after returning from `f`. This makes async
/// behavior much simpler to implement.
///
/// For efficency, `f` will be executed in the message processing loop, and therefore should
/// avoid blocking by moving work onto another thread, for example with an async runtime
/// spawn method.
///
/// # Arguments
///
/// * `name` - Name of the function
/// * `f` - The function. It should take a [`BindingContext`] that gives access to the
/// arguments and allows returning results.
///
/// # Examples
///
/// `bind()` approximately performs the following:
///
/// ```
/// #![windows_subsystem = "windows"]
/// use alcro::UIBuilder;
/// use serde_json::to_value;
///
/// let ui = UIBuilder::new().custom_args(&["--headless"]).run().expect("Unable to launch");
/// ui.bind_async("add", |context| {
/// std::thread::spawn(|| {
/// // imagine this is very expensive, or hits a network...
/// let mut sum = 0;
/// for arg in context.args() {
/// match arg.as_i64() {
/// Some(i) => sum+=i,
/// None => return context.err(to_value("Not number").unwrap())
/// }
/// }
///
/// context.complete(Ok(to_value(sum).unwrap()));
/// });
/// }).expect("Unable to bind function");
/// assert_eq!(ui.eval("(async () => await add(1,2,3))();").unwrap(), 6);
/// assert!(ui.eval("(async () => await add(1,2,'hi'))();").is_err());
/// ```
pub fn bind_async<F>(&self, name: &str, f: F) -> Result<(), JSError>
where
F: Fn(BindingContext) + Sync + Send + 'static,
{
bind(self.chrome.clone(), name, Arc::new(f))
}

/// Bind a rust function callable from JS that can complete asynchronously, using the [`tokio`]
/// runtime to wrap `bind_async()`, making usage more ergonomic for `tokio` users.
///
/// The callback is closer to `bind()` than `bind_async()` in that you take the JS arguments
/// and return the JS result, the main difference is that the arguments are passed by value
/// and the result is a [`Future`].
///
/// # Arguments
///
/// * `name` - Name of the function
/// * `f` - The function. It should take a [`Vec`] of [`JSObject`] arguments by value, and
/// return a [`Future`] for the [`JSResult`] (generally, by using an `async move`
/// block body)
///
/// # Examples
///
/// ```
/// #![windows_subsystem = "windows"]
/// use alcro::UIBuilder;
/// use serde_json::to_value;
///
/// # fn main() {
/// # // Ensure a tokio runtime is active for the test. A user will probably be using
/// # // #[tokio::main] instead, which doesn't work in doctests.
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # let _guard = rt.enter();
/// let ui = UIBuilder::new().custom_args(&["--headless"]).run().expect("Unable to launch");
/// ui.bind_tokio("add", |args| async move {
/// // imagine this is very expensive, or hits a network...
/// let mut sum = 0;
/// for arg in &args {
/// match arg.as_i64() {
/// Some(i) => sum+=i,
/// None => return Err(to_value("Not number").unwrap())
/// }
/// }
///
/// Ok(to_value(sum).unwrap())
/// }).expect("Unable to bind function");
/// assert_eq!(ui.eval("(async () => await add(1,2,3))();").unwrap(), 6);
/// assert!(ui.eval("(async () => await add(1,2,'hi'))();").is_err());
/// # }
/// ```
///
/// [`Future`]: std::future::Future
#[cfg(feature = "tokio")]
pub fn bind_tokio<F, R>(&self, name: &str, f: F) -> Result<(), JSError>
where
Expand Down

0 comments on commit 578a5bf

Please sign in to comment.