From e215157146f0eab8ee6beab0628b036c68eea108 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 4 Mar 2021 13:04:24 +0800 Subject: [PATCH] Implement draft RPC API. (#95) * Implement draft RPC API. Remove old Callback mechanism. * Remove obsolete Callback API * Remove FuncCall and RPC * Update README * Rename set_rpc_handler() to set_handler() * Use shared rpc_proxy() function for platform consistency * Improve handling of promise cleanup * Update README with RPC API info. * Panic if webview handler is set after window creation. * Improve rpc_proxy() logic, try to ensure any corresponding promise is always removed. * Remove FuncCall wrapper. * Remove set_handler(). Use second argument to add_window_with_configs() to set an RpcHandler. * Fix windows type signature. * Tidy obsolete comments and code. * Remove promise fallback clean up code. So that rust rpc handlers can perform asynchronous tasks and defer promise evaluation until later. If an rpc handler returns None then the handler takes responsibility for ensuring the corresponding promise is resolved or rejected. If the request contains an `id` then the handler *must* ensure it evaluates either `RpcResponse::into_result_script()` or `RpcResponse::into_error_script()`. * Remove Sync bound from RpcHandler. Update multiwindow example so it is slightly more illustrative of a real-world use case. Now it launches a window when a button is clicked in the main webview. Multiple windows can be launched and the URL for the new window is passed from the Javascript code. * Remove urlencoding from examples. --- Cargo.toml | 3 -- README.md | 64 ++++++++++++++++++++++ examples/multiwindow.rs | 98 ++++++++++++++++++---------------- examples/rpc.rs | 12 ++--- src/application/general.rs | 42 ++++----------- src/application/gtkrs.rs | 41 ++++----------- src/application/mod.rs | 81 ++++++++-------------------- src/lib.rs | 8 ++- src/platform/linux.rs | 105 +++++-------------------------------- src/platform/macos.rs | 51 ++++++++---------- src/platform/mod.rs | 56 ++++++++++---------- src/platform/win.rs | 39 +++++--------- src/webview.rs | 85 ++++-------------------------- 13 files changed, 257 insertions(+), 428 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 201205987..1ed3a22ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,6 @@ url = "2.2" image = "0.23" infer = "0.3" -[dev-dependencies] -urlencoding = "1" - [target.'cfg(target_os = "linux")'.dependencies] cairo-rs = "0.9" webkit2gtk = { version = "0.11", features = ["v2_8"] } diff --git a/README.md b/README.md index 424b9d5a7..3390cc813 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,68 @@ cargo run --example multiwindow For more information, please read the documentation below. +## Rust <-> Javascript + +Communication between the host Rust code and webview Javascript is done via [JSON-RPC][]. + +Embedding code should pass an `RpcHandler` to `add_window_with_configs()` to register an incoming request handler and reply with responses that are passed back to Javascript. On the Javascript side the client is exposed via `window.rpc` with two public methods + +1. The `call()` function accepts a method name and parameters and expects a reply. +2. The `notify()` function accepts a method name and parameters but does not expect a reply. + +Both functions return promises but `notify()` resolves immediately. + +For example in Rust: + +```rust +use wry::{Application, Result, WindowProxy, RpcRequest, RpcResponse}; + +fn main() -> Result<()> { + let mut app = Application::new()?; + let handler = Box::new(|proxy: &WindowProxy, mut req: RpcRequest| { + // Handle the request of type `RpcRequest` and reply with `Option`, + // use the `req.method` field to determine which action to take. + // + // If the handler returns a `RpcResponse` it is immediately evaluated + // in the calling webview. + // + // Use the `WindowProxy` to modify the window, eg: `set_fullscreen` etc. + // + // If the request is a notification (no `id` field) then the handler + // can just return `None`. + // + // If an `id` field is present and the handler wants to execute asynchronous + // code it can return `None` but then *must* later evaluate either + // `RpcResponse::into_result_script()` or `RpcResponse::into_error_script()` + // in the webview to ensure the promise is resolved or rejected and removed + // from the cache. + None + }); + app.add_window_with_configs(Default::default(), Some(handler), None)?; + app.run(); + Ok(()) +} +``` + +Then in Javascript use `call()` to call a remote method and get a response: + +```javascript +async function callRemoteMethod() { + let result = await window.rpc.call('remoteMethod', param1, param2); + // Do something with the result +} +``` + +If Javascript code wants to use a callback style it is easy to alias a function to a method call: + +```javascript +function someRemoteMethod() { + return window.rpc.call(arguments.callee.name, Array.prototype.slice(arguments, 0)); +} +``` + +See the `rpc` example for more details. + ## [Documentation](https://docs.rs/wry) ## Platform-specific notes @@ -84,3 +146,5 @@ WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7, ## License Apache-2.0/MIT + +[JSON-RPC]: https://www.jsonrpc.org diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index d985e98e1..c2ab5e078 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -1,63 +1,73 @@ use wry::Result; -use wry::{Application, Attributes, Callback}; +use wry::{Application, Attributes, WindowProxy, RpcRequest}; +use serde_json::Value; fn main() -> Result<()> { let mut app = Application::new()?; + let html = r#" + +

Multiwindow example

+ +"#; + let attributes = Attributes { - url: Some("https://tauri.studio".to_string()), + url: Some(format!("data:text/html,{}", html)), // Initialization scripts can be used to define javascript functions and variables. initialization_scripts: vec![ - String::from("breads = NaN"), - String::from("menacing = 'ゴ'"), + /* Custom initialization scripts go here */ ], ..Default::default() }; - // Callback defines a rust function to be called on javascript side later. Below is a function - // which will print the list of parameters after 8th calls. - let callback = Callback { - name: "world".to_owned(), - function: Box::new(|proxy, sequence, requests| { - // Proxy is like a window handle for you to send message events to the corresponding webview - // window. You can use it to adjust window and evaluate javascript code like below. - // This is useful when you want to perform any action in javascript. - proxy.evaluate_script("console.log(menacing);")?; - // Sequence is a number counting how many times this function being called. - if sequence < 8 { - println!("{} seconds has passed.", sequence); - } else { - // Requests is a vector of parameters passed from the caller. - println!("{:?}", requests); - } - Ok(()) - }), - }; - let window1 = app.add_window_with_configs(attributes, Some(vec![callback]), None)?; let app_proxy = app.application_proxy(); + let (window_tx, window_rx) = std::sync::mpsc::channel::(); - std::thread::spawn(move || { - for _ in 0..7 { - std::thread::sleep(std::time::Duration::from_secs(1)); - window1.evaluate_script("world()".to_string()).unwrap(); + let handler = Box::new(move |_proxy: &WindowProxy, req: RpcRequest| { + if &req.method == "openWindow" { + if let Some(params) = req.params { + if let Value::Array(mut arr) = params { + let mut param = if arr.get(0).is_none() { + None + } else { + Some(arr.swap_remove(0)) + }; + + if let Some(param) = param.take() { + if let Value::String(url) = param { + let _ = window_tx.send(url); + } + } + } + } } - std::thread::sleep(std::time::Duration::from_secs(1)); + None + }); + + let _ = app.add_window_with_configs(attributes, Some(handler), None)?; - window1.set_title("WRYYYYYYYYYYYYYYYYYYYYY").unwrap(); - let window2 = app_proxy - .add_window_with_configs( - Attributes { - width: 426., - height: 197., - title: "RODA RORA DA".into(), - url: Some("https://i.imgur.com/x6tXcr9.gif".to_string()), - ..Default::default() - }, - None, - None, - ) - .unwrap(); - println!("ID of second window: {:?}", window2.id()); + std::thread::spawn(move || { + while let Ok(url) = window_rx.recv() { + let new_window = app_proxy + .add_window_with_configs( + Attributes { + width: 426., + height: 197., + title: "RODA RORA DA".into(), + url: Some(url), + ..Default::default() + }, + None, + None, + ) + .unwrap(); + println!("ID of new window: {:?}", new_window.id()); + + } }); app.run(); diff --git a/examples/rpc.rs b/examples/rpc.rs index 2dff7e4ee..c29caa64d 100644 --- a/examples/rpc.rs +++ b/examples/rpc.rs @@ -1,5 +1,5 @@ use wry::Result; -use wry::{Application, Attributes, RpcResponse}; +use wry::{Application, Attributes, RpcResponse, RpcRequest, WindowProxy}; use serde::{Serialize, Deserialize}; use serde_json::Value; @@ -31,14 +31,12 @@ async function getAsyncRpcResult() {
"#; - let markup = urlencoding::encode(html); let attributes = Attributes { - url: Some(format!("data:text/html,{}", markup)), + url: Some(format!("data:text/html,{}", html)), ..Default::default() }; - // NOTE: must be set before calling add_window(). - app.set_rpc_handler(Box::new(|proxy, mut req| { + let handler = Box::new(|proxy: &WindowProxy, mut req: RpcRequest| { let mut response = None; if &req.method == "fullscreen" { if let Some(params) = req.params.take() { @@ -69,9 +67,9 @@ async function getAsyncRpcResult() { } response - })); + }); - app.add_window(attributes)?; + app.add_window_with_configs(attributes, Some(handler), None)?; app.run(); Ok(()) diff --git a/src/application/general.rs b/src/application/general.rs index 121c4a345..90cc8455e 100644 --- a/src/application/general.rs +++ b/src/application/general.rs @@ -1,6 +1,6 @@ use crate::{ application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes}, - ApplicationProxy, Attributes, Callback, CustomProtocol, Error, Icon, Message, Result, WebView, + ApplicationProxy, Attributes, CustomProtocol, Error, Icon, Message, Result, WebView, WebViewBuilder, WindowMessage, WindowProxy, RpcHandler, }; #[cfg(target_os = "macos")] @@ -13,7 +13,7 @@ use winit::{ window::{Fullscreen, Icon as WinitIcon, Window, WindowAttributes, WindowBuilder}, }; -use std::{sync::Arc, collections::HashMap, sync::mpsc::channel}; +use std::{collections::HashMap, sync::mpsc::channel}; #[cfg(target_os = "windows")] use { @@ -48,14 +48,14 @@ impl AppProxy for InnerApplicationProxy { fn add_window( &self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, ) -> Result { let (sender, receiver) = channel(); self.send_message(Message::NewWindow( attributes, - callbacks, sender, + rpc_handler, custom_protocol, ))?; Ok(receiver.recv()?) @@ -105,7 +105,6 @@ pub struct InnerApplication { webviews: HashMap, event_loop: EventLoop, event_loop_proxy: EventLoopProxy, - pub(crate) rpc_handler: Option>, } impl App for InnerApplication { @@ -119,14 +118,13 @@ impl App for InnerApplication { webviews: HashMap::new(), event_loop, event_loop_proxy: proxy, - rpc_handler: None, }) } fn create_webview( &mut self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, ) -> Result { let (window_attrs, webview_attrs) = attributes.split(); @@ -135,9 +133,8 @@ impl App for InnerApplication { &self.application_proxy(), window, webview_attrs, - callbacks, custom_protocol, - self.rpc_handler.clone(), + rpc_handler, )?; let id = webview.window().id(); self.webviews.insert(id, webview); @@ -153,7 +150,6 @@ impl App for InnerApplication { fn run(self) { let dispatcher = self.application_proxy(); let mut windows = self.webviews; - let rpc_handler = self.rpc_handler.clone(); self.event_loop.run(move |event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; @@ -175,7 +171,7 @@ impl App for InnerApplication { _ => {} }, Event::UserEvent(message) => match message { - Message::NewWindow(attributes, callbacks, sender, custom_protocol) => { + Message::NewWindow(attributes, sender, rpc_handler, custom_protocol) => { let (window_attrs, webview_attrs) = attributes.split(); let window = _create_window(&event_loop, window_attrs).unwrap(); sender.send(window.id()).unwrap(); @@ -183,9 +179,8 @@ impl App for InnerApplication { &dispatcher, window, webview_attrs, - callbacks, custom_protocol, - rpc_handler.clone(), + rpc_handler, ) .unwrap(); let id = webview.window().id(); @@ -347,9 +342,8 @@ fn _create_webview( dispatcher: &InnerApplicationProxy, window: Window, attributes: InnerWebViewAttributes, - callbacks: Option>, custom_protocol: Option, - rpc_handler: Option>, + rpc_handler: Option, ) -> Result { let window_id = window.id(); let rpc_win_id = window_id.clone(); @@ -359,23 +353,7 @@ fn _create_webview( for js in attributes.initialization_scripts { webview = webview.initialize_script(&js); } - if let Some(cbs) = callbacks { - for Callback { name, mut function } in cbs { - let dispatcher = dispatcher.clone(); - webview = webview.add_callback(&name, move |_, seq, req| { - function( - WindowProxy::new( - ApplicationProxy { - inner: dispatcher.clone(), - }, - window_id, - ), - seq, - req, - ) - }); - } - } + if let Some(protocol) = custom_protocol { webview = webview.register_protocol(protocol.name, protocol.handler) } diff --git a/src/application/gtkrs.rs b/src/application/gtkrs.rs index 6b246a84a..9adacb03d 100644 --- a/src/application/gtkrs.rs +++ b/src/application/gtkrs.rs @@ -1,6 +1,6 @@ use crate::{ application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes, WindowProxy}, - ApplicationProxy, Attributes, Callback, CustomProtocol, Error, Icon, Message, Result, WebView, + ApplicationProxy, Attributes, CustomProtocol, Error, Icon, Message, Result, WebView, WebViewBuilder, WindowMessage, RpcHandler, }; @@ -8,7 +8,7 @@ use std::{ cell::RefCell, collections::HashMap, rc::Rc, - sync::{Arc, Mutex, mpsc::{channel, Receiver, Sender}}, + sync::{mpsc::{channel, Receiver, Sender}}, }; use cairo::Operator; @@ -45,14 +45,14 @@ impl AppProxy for InnerApplicationProxy { fn add_window( &self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, ) -> Result { let (sender, receiver): (Sender, Receiver) = channel(); self.send_message(Message::NewWindow( attributes, - callbacks, sender, + rpc_handler, custom_protocol, ))?; Ok(receiver.recv()?) @@ -64,7 +64,6 @@ pub struct InnerApplication { app: GtkApp, event_loop_proxy: EventLoopProxy, event_loop_proxy_rx: Receiver, - pub(crate) rpc_handler: Option>, } impl App for InnerApplication { @@ -83,14 +82,13 @@ impl App for InnerApplication { app, event_loop_proxy: EventLoopProxy(event_loop_proxy_tx), event_loop_proxy_rx, - rpc_handler: None, }) } fn create_webview( &mut self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, ) -> Result { let (window_attrs, webview_attrs) = attributes.split(); @@ -100,9 +98,8 @@ impl App for InnerApplication { &self.application_proxy(), window, webview_attrs, - callbacks, custom_protocol, - self.rpc_handler.clone(), + rpc_handler, )?; let id = webview.window().get_id(); self.webviews.insert(id, webview); @@ -148,7 +145,7 @@ impl App for InnerApplication { while let Ok(message) = self.event_loop_proxy_rx.try_recv() { match message { - Message::NewWindow(attributes, callbacks, sender, custom_protocol) => { + Message::NewWindow(attributes, sender, rpc_handler, custom_protocol) => { let (window_attrs, webview_attrs) = attributes.split(); let window = _create_window(&self.app, window_attrs).unwrap(); sender.send(window.get_id()).unwrap(); @@ -156,9 +153,8 @@ impl App for InnerApplication { &proxy, window, webview_attrs, - callbacks, custom_protocol, - self.rpc_handler.clone(), + rpc_handler, ) .unwrap(); let id = webview.window().get_id(); @@ -392,9 +388,8 @@ fn _create_webview( proxy: &InnerApplicationProxy, window: ApplicationWindow, attributes: InnerWebViewAttributes, - callbacks: Option>, custom_protocol: Option, - rpc_handler: Option>, + rpc_handler: Option, ) -> Result { let window_id = window.get_id(); @@ -405,23 +400,7 @@ fn _create_webview( for js in attributes.initialization_scripts { webview = webview.initialize_script(&js); } - if let Some(cbs) = callbacks { - for Callback { name, mut function } in cbs { - let proxy = proxy.clone(); - webview = webview.add_callback(&name, move |_, seq, req| { - function( - WindowProxy::new( - ApplicationProxy { - inner: proxy.clone(), - }, - window_id, - ), - seq, - req, - ) - }); - } - } + webview = match attributes.url { Some(url) => webview.load_url(&url)?, None => webview, diff --git a/src/application/mod.rs b/src/application/mod.rs index 1e3bc8053..30fc7a628 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1,5 +1,3 @@ -use std::sync::{Arc, Mutex}; - #[cfg(not(target_os = "linux"))] mod general; #[cfg(not(target_os = "linux"))] @@ -19,32 +17,6 @@ use std::{fs::read, path::Path, sync::mpsc::Sender}; use serde_json::Value; -/// Defines a Rust callback function which can be called on Javascript side. -pub struct Callback { - /// Name of the callback function. - pub name: String, - /// The function itself takes three parameters and return a number as return value. - /// - /// [`WindowProxy`] can let you adjust the corresponding WebView window. - /// - /// The second parameter `i32` is a sequence number to count how many times this function has - /// been called. - /// - /// The last vector is the actual list of arguments passed from the caller. - /// - /// The return value of the function is a number. Return `0` indicates the call is successful, - /// and return others if not. - pub function: Box) -> Result<()> + Send>, -} - -impl std::fmt::Debug for Callback { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Callback") - .field("name", &self.name) - .finish() - } -} - pub struct CustomProtocol { pub name: String, pub handler: Box Result> + Send>, @@ -293,13 +265,12 @@ pub enum WindowMessage { } /// Describes a general message. -#[derive(Debug)] pub enum Message { Window(WindowId, WindowMessage), NewWindow( Attributes, - Option>, Sender, + Option, Option, ), } @@ -329,12 +300,12 @@ impl ApplicationProxy { pub fn add_window_with_configs( &self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, ) -> Result { let id = self .inner - .add_window(attributes, callbacks, custom_protocol)?; + .add_window(attributes, rpc_handler, custom_protocol)?; Ok(WindowProxy::new(self.clone(), id)) } } @@ -344,9 +315,8 @@ trait AppProxy { fn add_window( &self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, - //rpc_handler: Option, ) -> Result; } @@ -557,12 +527,12 @@ impl Application { pub fn add_window_with_configs( &mut self, attributes: Attributes, - callbacks: Option>, + handler: Option, custom_protocol: Option, ) -> Result { let id = self .inner - .create_webview(attributes, callbacks, custom_protocol)?; + .create_webview(attributes, handler, custom_protocol)?; Ok(self.window_proxy(id)) } @@ -579,14 +549,6 @@ impl Application { WindowProxy::new(self.application_proxy(), window_id) } - /// Set an RPC message handler. - pub fn set_rpc_handler(&mut self, handler: RpcHandler) { - // TODO: detect if webviews already exist and panic - // TODO: because this should be set before callling add_window(). - - self.inner.rpc_handler = Some(Arc::new(handler)); - } - /// Consume the application and start running it. This will hijack the main thread and iterate /// its event loop. To further control the application after running, [`ApplicationProxy`] and /// [`WindowProxy`] allow you to do so on other threads. @@ -604,9 +566,8 @@ trait App: Sized { fn create_webview( &mut self, attributes: Attributes, - callbacks: Option>, + rpc_handler: Option, custom_protocol: Option, - //rpc_handler: Option, ) -> Result; fn application_proxy(&self) -> Self::Proxy; @@ -614,22 +575,8 @@ trait App: Sized { fn run(self); } -pub(crate) const RPC_CALLBACK_NAME: &str = "__rpc__"; const RPC_VERSION: &str = "2.0"; -/// Function call from Javascript. -/// -/// If the callback name matches the name for an RPC handler -/// the payload should be passed to the handler transparently. -/// -/// Otherwise attempt to find a `Callback` with the same name -/// and pass it the payload `params`. -#[derive(Debug, Serialize, Deserialize)] -pub struct FuncCall { - pub(crate) callback: String, - pub(crate) payload: RpcRequest, -} - /// RPC request message. #[derive(Debug, Serialize, Deserialize)] pub struct RpcRequest { @@ -667,4 +614,18 @@ impl RpcResponse { result: None } } + + /// Get a script that resolves the promise with a result. + pub fn into_result_script(id: Value, result: Value) -> Result { + let retval = serde_json::to_string(&result)?; + Ok(format!("window.external.rpc._result({}, {})", + id.to_string(), retval)) + } + + /// Get a script that rejects the promise with an error. + pub fn into_error_script(id: Value, result: Value) -> Result { + let retval = serde_json::to_string(&result)?; + Ok(format!("window.external.rpc._error({}, {})", + id.to_string(), retval)) + } } diff --git a/src/lib.rs b/src/lib.rs index e3b6b5336..371d97c93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,11 +87,11 @@ pub mod platform; pub mod webview; pub use application::{ - Application, ApplicationProxy, Attributes, Callback, CustomProtocol, Icon, Message, WindowId, + Application, ApplicationProxy, Attributes, CustomProtocol, Icon, Message, WindowId, WindowMessage, WindowProxy, RpcRequest, RpcResponse, }; pub use serde_json::Value; -pub(crate) use webview::{Dispatcher, WebView, WebViewBuilder, RpcHandler}; +pub(crate) use webview::{WebView, WebViewBuilder, RpcHandler}; #[cfg(not(target_os = "linux"))] use winit::window::BadIcon; @@ -114,6 +114,8 @@ pub enum Error { GlibBoolError(#[from] glib::BoolError), #[error("Failed to initialize the script")] InitScriptError, + #[error("Bad RPC request: {0} ((1))")] + RpcScriptError(String, String), #[error(transparent)] NulError(#[from] std::ffi::NulError), #[cfg(not(target_os = "linux"))] @@ -126,6 +128,8 @@ pub enum Error { #[error("Failed to send the message")] MessageSender, #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] UrlError(#[from] ParseError), #[error("IO error: {0}")] Io(#[from] std::io::Error), diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9c7ca713e..cb7202de7 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,17 +1,14 @@ -use crate::platform::{CALLBACKS, RPC}; -use crate::application::{WindowProxy, FuncCall, RpcRequest, RpcResponse, RPC_CALLBACK_NAME}; +use crate::application::WindowProxy; use crate::mimetype::MimeType; use crate::webview::WV; -use crate::{Dispatcher, Error, Result, RpcHandler}; +use crate::{Error, Result, RpcHandler}; use std::rc::Rc; -use std::sync::Arc; -use serde_json::Value; use gdk::RGBA; use gio::Cancellable; use glib::{Bytes, FileError}; -use gtk::{ApplicationWindow as Window, ApplicationWindowExt, ContainerExt, WidgetExt}; +use gtk::{ApplicationWindow as Window, ContainerExt, WidgetExt}; use url::Url; use webkit2gtk::{ SecurityManagerExt, SettingsExt, URISchemeRequestExt, UserContentInjectedFrames, @@ -34,7 +31,7 @@ impl WV for InnerWebView { custom_protocol: Option<(String, F)>, rpc_handler: Option<( WindowProxy, - Arc, + RpcHandler, )>, ) -> Result { // Webview widget @@ -47,97 +44,19 @@ impl WV for InnerWebView { // Message handler let wv = Rc::clone(&webview); manager.register_script_message_handler("external"); - let window_id = window.get_id() as i64; manager.connect_script_message_received(move |_m, msg| { - if let Some(js) = msg.get_value() { - if let Some(context) = msg.get_global_context() { - if let Some(js) = js.to_string(&context) { - match serde_json::from_str::(&js) { - Ok(mut ev) => { - // Use `isize` to conform with existing `Callback` API but should - // really be a `u64`. Note that RPC spec allows for non-numbers - // in the `id` field! - let id: i32 = if let Some(value) = ev.payload.id.clone().take() { - if let Value::Number(num) = value { - if num.is_i64() { num.as_i64().unwrap() as i32 } else { 0 } - } else { 0 } - } else { 0 }; - - let use_rpc = rpc_handler.is_some() && &ev.callback == RPC_CALLBACK_NAME; - - // Send to an RPC handler - if use_rpc { - let (proxy, rpc_handler) = rpc_handler.as_ref().unwrap(); - let mut response = rpc_handler(proxy, ev.payload); - if let Some(mut response) = response.take() { - if let Some(id) = response.id { - let js = if let Some(error) = response.error.take() { - match serde_json::to_string(&error) { - Ok(retval) => { - format!("window.external.rpc._error({}, {})", - id.to_string(), retval) - } - Err(_) => { - format!("window.external.rpc._error({}, null)", - id.to_string()) - } - } - } else if let Some(result) = response.result.take() { - match serde_json::to_string(&result) { - Ok(retval) => { - format!("window.external.rpc._result({}, {})", - id.to_string(), retval) - } - Err(_) => { - format!("window.external.rpc._result({}, null)", - id.to_string()) - } - } - } else { - // No error or result, assume a positive response - // with empty result (ACK) - format!("window.external.rpc._result({}, null)", - id.to_string()) - }; - - let cancellable: Option<&Cancellable> = None; - wv.run_javascript(&js, cancellable, |_| ()); - } - } - // Normal callback mechanism - } else { - let mut hashmap = CALLBACKS.lock().unwrap(); - let (f, d) = hashmap.get_mut(&(window_id, ev.callback)).unwrap(); - // TODO: update `Callback` to take a `Value`? - let raw_params = if let Some(val) = ev.payload.params.take() { - val - } else { Value::Null }; - let params = if let Value::Array(arr) = raw_params { - arr - } else { vec![raw_params] }; - - let status = f(d, id, params); - let js = match status { - Ok(()) => { - format!( - r#"window._rpc[{}].resolve("RPC call success"); window._rpc[{}] = undefined"#, - id, id - ) - } - Err(e) => { - format!( - r#"window._rpc[{}].reject("RPC call fail with error {}"); window._rpc[{}] = undefined"#, - id, e, id - ) - } - }; - + if let (Some(js), Some(context)) = (msg.get_value(), msg.get_global_context()) { + if let Some(js) = js.to_string(&context) { + if let Some((proxy, rpc_handler)) = rpc_handler.as_ref() { + match super::rpc_proxy(js, proxy, rpc_handler) { + Ok(result) => { + if let Some(ref script) = result { let cancellable: Option<&Cancellable> = None; - wv.run_javascript(&js, cancellable, |_| ()); + wv.run_javascript(script, cancellable, |_| ()); } } Err(e) => { - eprintln!("Bad Javascript function call: {} ({})", e, &js); + eprintln!("{}", e); } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index fc83d3874..4da9bfe0f 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,11 +1,9 @@ use crate::mimetype::MimeType; -use crate::platform::{CALLBACKS, RPC}; use crate::application::WindowProxy; use crate::webview::WV; -use crate::{Result, Dispatcher, RpcHandler}; +use crate::{Result, RpcHandler}; use std::{ - sync::Arc, collections::hash_map::DefaultHasher, ffi::{c_void, CStr}, hash::{Hash, Hasher}, @@ -41,7 +39,7 @@ impl WV for InnerWebView { custom_protocol: Option<(String, F)>, rpc_handler: Option<( WindowProxy, - Arc, + RpcHandler, )>, ) -> Result { let mut hasher = DefaultHasher::new(); @@ -52,33 +50,30 @@ impl WV for InnerWebView { extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { // Safety: objc runtime calls are unsafe unsafe { - let window_id = *this.get_ivar("_window_id"); + //let window_id = *this.get_ivar("_window_id"); let body: id = msg_send![msg, body]; let utf8: *const c_char = msg_send![body, UTF8String]; - let s = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string"); - let v: RPC = serde_json::from_str(&s).unwrap(); - let mut hashmap = CALLBACKS.lock().unwrap(); - let (f, d) = hashmap.get_mut(&(window_id, v.method)).unwrap(); - let status = f(d, v.id, v.params); - - let js = match status { - Ok(()) => { - format!( - r#"window._rpc[{}].resolve("RPC call success"); window._rpc[{}] = undefined"#, - v.id, v.id - ) - } - Err(e) => { - format!( - r#"window._rpc[{}].reject("RPC call fail with error {}"); window._rpc[{}] = undefined"#, - v.id, e, v.id - ) + let js = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string"); + + // FIXME: allow access to rpc_handler here? + + /* + if let Some((proxy, rpc_handler)) = rpc_handler.as_ref() { + match super::rpc_proxy(js.to_string(), proxy, rpc_handler) { + Ok(result) => { + if let Some(ref script) = result { + let wv: id = msg_send![msg, webView]; + let js = NSString::new(script); + let _: id = + msg_send![wv, evaluateJavaScript:js completionHandler:null::<*const c_void>()]; + } + } + Err(e) => { + eprintln!("{}", e); + } } - }; - let wv: id = msg_send![msg, webView]; - let js = NSString::new(&js); - let _: id = - msg_send![wv, evaluateJavaScript:js completionHandler:null::<*const c_void>()]; + } + */ } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 2dd3eead5..5e1822d95 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -20,34 +20,34 @@ pub use gtk::*; #[cfg(not(target_os = "linux"))] pub use winit::*; -use crate::{Dispatcher, Result, RpcHandler}; - -use std::{collections::HashMap, sync::Mutex}; - -use once_cell::sync::Lazy; use serde_json::Value; -pub(crate) static CALLBACKS: Lazy< - Mutex< - HashMap< - (i64, String), - ( - std::boxed::Box) -> Result<()> + Send>, - Dispatcher, - ), - >, - >, -> = Lazy::new(|| { - let m = HashMap::new(); - Mutex::new(m) -}); - -#[deprecated] -#[derive(Debug, Serialize, Deserialize)] -struct RPC { - id: i32, - method: String, - params: Vec, +use crate::{Error, Result, RpcHandler, application::{WindowProxy, RpcRequest, RpcResponse}}; + +// Helper so all platforms handle RPC messages consistently. +pub(crate) fn rpc_proxy(js: String, proxy: &WindowProxy, handler: &RpcHandler) -> Result> { + let req = serde_json::from_str::(&js).map_err(|e| { + Error::RpcScriptError(e.to_string(), js) + })?; + + let mut response = (handler)(proxy, req); + // Got a synchronous response so convert it to a script to be evaluated + if let Some(mut response) = response.take() { + if let Some(id) = response.id { + let js = if let Some(error) = response.error.take() { + RpcResponse::into_error_script(id, error)? + } else if let Some(result) = response.result.take() { + RpcResponse::into_result_script(id, result)? + } else { + // No error or result, assume a positive response + // with empty result (ACK) + RpcResponse::into_result_script(id, Value::Null)? + }; + Ok(Some(js)) + } else { + Ok(None) + } + } else { + Ok(None) + } } - - diff --git a/src/platform/win.rs b/src/platform/win.rs index cfeaa406c..90d0b418c 100644 --- a/src/platform/win.rs +++ b/src/platform/win.rs @@ -1,11 +1,9 @@ use crate::mimetype::MimeType; -use crate::platform::{CALLBACKS, RPC}; use crate::application::WindowProxy; use crate::webview::WV; -use crate::{Result, Dispatcher, RpcHandler}; +use crate::{Result, RpcHandler}; use std::{ - sync::Arc, collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, os::raw::c_void, @@ -35,7 +33,7 @@ impl WV for InnerWebView { custom_protocol: Option<(String, F)>, rpc_handler: Option<( WindowProxy, - Arc, + RpcHandler, )>, ) -> Result { let controller: Rc> = Rc::new(OnceCell::new()); @@ -79,28 +77,19 @@ impl WV for InnerWebView { // Message handler w.add_web_message_received(move |webview, args| { - let s = args.try_get_web_message_as_string()?; - let v: RPC = serde_json::from_str(&s).unwrap(); - let mut hashmap = CALLBACKS.lock().unwrap(); - let (f, d) = hashmap.get_mut(&(window_id, v.method)).unwrap(); - let status = f(d, v.id, v.params); - - let js = match status { - Ok(()) => { - format!( - r#"window._rpc[{}].resolve("RPC call success"); window._rpc[{}] = undefined"#, - v.id, v.id - ) - } - Err(e) => { - format!( - r#"window._rpc[{}].reject("RPC call fail with error {}"); window._rpc[{}] = undefined"#, - v.id, e, v.id - ) + let js = args.try_get_web_message_as_string()?; + if let Some((proxy, rpc_handler)) = rpc_handler.as_ref() { + match super::rpc_proxy(js, proxy, rpc_handler) { + Ok(result) => { + if let Some(ref script) = result { + webview.execute_script(script, |_| (Ok(())))?; + } + } + Err(e) => { + eprintln!("{}", e); + } } - }; - - webview.execute_script(&js, |_| (Ok(())))?; + } Ok(()) })?; diff --git a/src/webview.rs b/src/webview.rs index 115c538db..e14301b6a 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -1,27 +1,26 @@ //! [`WebView`] struct and associated types. -use crate::platform::{InnerWebView, CALLBACKS}; -use crate::application::{WindowProxy, RpcRequest, RpcResponse, RPC_CALLBACK_NAME}; +use crate::platform::{InnerWebView}; +use crate::application::{WindowProxy, RpcRequest, RpcResponse}; use crate::Result; -use std::sync::{Arc, mpsc::{channel, Receiver, Sender}}; +use std::sync::{mpsc::{channel, Receiver, Sender}}; #[cfg(not(target_os = "linux"))] use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, }; -use serde_json::Value; use url::Url; #[cfg(target_os = "linux")] -use gtk::{ApplicationWindow as Window, ApplicationWindowExt}; +use gtk::{ApplicationWindow as Window}; #[cfg(target_os = "windows")] use winit::platform::windows::WindowExtWindows; #[cfg(not(target_os = "linux"))] use winit::window::Window; -pub type RpcHandler = Box Option + Send + Sync>; +pub type RpcHandler = Box Option + Send>; /// Builder type of [`WebView`]. /// @@ -35,11 +34,10 @@ pub struct WebViewBuilder { initialization_scripts: Vec, window: Window, url: Option, - window_id: i64, custom_protocol: Option<(String, Box Result>>)>, rpc_handler: Option<( WindowProxy, - Arc, + RpcHandler, )>, } @@ -47,14 +45,6 @@ impl WebViewBuilder { /// Create [`WebViewBuilder`] from provided [`Window`]. pub fn new(window: Window) -> Result { let (tx, rx) = channel(); - #[cfg(not(target_os = "linux"))] - let window_id = { - let mut hasher = DefaultHasher::new(); - window.id().hash(&mut hasher); - hasher.finish() as i64 - }; - #[cfg(target_os = "linux")] - let window_id = window.get_id() as i64; Ok(Self { tx, @@ -63,7 +53,6 @@ impl WebViewBuilder { window, url: None, transparent: false, - window_id, custom_protocol: None, rpc_handler: None, }) @@ -100,63 +89,11 @@ impl WebViewBuilder { self } - /// Add a callback function to the WebView. The callback takse a dispatcher, a sequence number, - /// and a vector of arguments passed from callers as parameters. - /// - /// It uses RPC to communicate with javascript side and the sequence number is used to record - /// how many times has this callback been called. Arguments passed from callers is a vector of - /// serde values for you to decide how to handle them. IF you need to evaluate any code on - /// javascript side, you can use the dispatcher to send them. - pub fn add_callback(mut self, name: &str, f: F) -> Self - where - F: FnMut(&Dispatcher, i32, Vec) -> Result<()> + Send + 'static, - { - - let js = format!( - r#" - (function() {{ - var name = {:?}; - var RPC = window._rpc = (window._rpc || {{nextSeq: 1}}); - window[name] = function() {{ - var seq = RPC.nextSeq++; - var promise = new Promise(function(resolve, reject) {{ - RPC[seq] = {{ - resolve: resolve, - reject: reject, - }}; - }}); - window.external.invoke(JSON.stringify({{ - callback: {:?}, - payload: {{ - jsonrpc: '2.0', - id: seq, - method: name, - params: Array.prototype.slice.call(arguments), - }} - }})); - return promise; - }} - }})() - "#, - name, - name, - ); - self.initialization_scripts.push(js); - - let window_id = self.window_id; - CALLBACKS.lock().unwrap().insert( - (window_id, name.to_string()), - (Box::new(f), Dispatcher(self.tx.clone())), - ); - self - } - /// Set the RPC handler. - pub(crate) fn set_rpc_handler(mut self, proxy: WindowProxy, handler: Arc) -> Self { + pub(crate) fn set_rpc_handler(mut self, proxy: WindowProxy, handler: RpcHandler) -> Self { let js = r#" function Rpc() { - this._callback = '__rpc__'; this._promises = {}; // Private internal function called on error @@ -180,11 +117,10 @@ impl WebViewBuilder { const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); const params = Array.prototype.slice.call(arguments, 1); const payload = {jsonrpc: "2.0", id, method, params}; - const msg = {callback: this._callback, payload}; const promise = new Promise((resolve, reject) => { this._promises[id] = {resolve, reject}; }); - window.external.invoke(JSON.stringify(msg)); + window.external.invoke(JSON.stringify(payload)); return promise; } @@ -192,8 +128,7 @@ impl WebViewBuilder { this.notify = function(method) { const params = Array.prototype.slice.call(arguments, 1); const payload = {jsonrpc: "2.0", method, params}; - const msg = {callback: this._callback, payload}; - window.external.invoke(JSON.stringify(msg)); + window.external.invoke(JSON.stringify(payload)); return Promise.resolve(); } } @@ -331,7 +266,7 @@ pub(crate) trait WV: Sized { custom_protocol: Option<(String, F)>, rpc_handler: Option<( WindowProxy, - Arc, + RpcHandler, )>, ) -> Result;