tauri-apps / wry

Cross-platform WebView library in Rust for Tauri.
Apache License 2.0
3.5k stars 261 forks source link

Feature: Implement Callback Functions for JavaScript Evaluation Result #474

Open Toromyx opened 2 years ago

Toromyx commented 2 years ago

Problem I use Tauri to open windows of external sites as well as the "main" application window of local HTML/JS/CSS. I still want to interact with the DOM of those external sites and know the result of that interaction. This doesn't have to be done via JavaScript.

Solution All three webview APIs on the main operating systems support some kind of callback when executing JavaScript. For each os I added a link to the documentation and the place in the code where that callback possibility is just ignored. Is there a (security) reason this functionality is not provided?

Linux

Windows

Apple

Alternatives An alternative I considered was to build a JavaScript wrapper which sends the result of the evaluated JavaScript over RPC. But this would be a security problem because now external sites have access to RPC.

Would you assign yourself to implement this feature? I'm only just starting with Rust and would assign myself to do a proof of concept for WebView2 under Windows. I currently don't have the capability to develop under Linux and never will have the capability to develop under Apple.

wusyong commented 2 years ago

I think this one is just because of history reason. When webview repo provide the API, it just want one that can run js script. We do can support this but it won't be a trivial task IMHO. It's better wait for v1 launch and see what feature requests are wanted by most.

Toromyx commented 2 years ago

I hacked together something which "works on my machine" (meaning Windows):

I'm open for feedback, but I also understand if you say that now is not the right time to implement this feature.

yyon commented 1 year ago

Hello, I happened to implement this on all 3 OS's (using Tauri's with_webview function). If you ever happen to want to implement this feature, feel free to use this code as a reference.

let webview_result = browser_window.with_webview(|webview| {
    #[cfg(target_os = "linux")]
    {
        use webkit2gtk::traits::WebViewExt;
        use gio;
        let cancellable: Option<&gio::Cancellable> = None;
        webview.inner().run_javascript("JSON.stringify(\"Hello, world!\");", cancellable, |result| {
            if result.is_err() {
                println!("Error evaluating javascript");
                return;
            }

            let javascript_result = result.unwrap();

            let value_opt = javascript_result.js_value();

            if value_opt.is_none() {
                println!("Javascript returned nothing");
                return;
            }

            let value_val = value_opt.unwrap();

            let value_str = value_val.to_string();

            do_callback(value_str);
        });
    }

    #[cfg(windows)]
    unsafe {
        use webview2_com::{Microsoft::Web::WebView2::Win32::*, *};
        use windows::{
            core::{PCWSTR, PWSTR},
        };
        use std::os::windows::ffi::OsStrExt;

        let core_result = webview.controller().CoreWebView2();

        if core_result.is_err() {
            println!("Error getting core webview");
            return;
        }

        let core = core_result.unwrap();

        fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
            string.as_ref().encode_wide().chain(std::iter::once(0)).collect()
        }

        let _res = core.ExecuteScript(
            PCWSTR::from_raw(encode_wide("JSON.stringify(\"Hello, world!\");".to_string()).as_ptr()),
            &ExecuteScriptCompletedHandler::create(Box::new(|result, result_object_as_json: std::string::String| {
                // note: result_object_as_json is wrapped in JSON twice, as opposed to other OS's, because for some reason it can only return strings
                // so you should probably parse JSON here
                do_callback(result_object_as_json);
                Ok(())
            }))
        );
    }

    #[cfg(target_os = "macos")]
    unsafe {
        use objc;
        use objc::sel;
        use objc::sel_impl;
        use cocoa::base::id;
        const UTF8_ENCODING: usize = 4;
        struct NSString(id);
        impl NSString {
            fn new(s: &str) -> Self {
                NSString(unsafe {
                let ns_string: id = objc::msg_send![objc::class!(NSString), alloc];
                let ns_string: id = objc::msg_send![ns_string,
                                        initWithBytes:s.as_ptr()
                                        length:s.len()
                                        encoding:UTF8_ENCODING];

                let _: () = objc::msg_send![ns_string, autorelease];

                ns_string
                })
            }

            fn to_str(&self) -> &str {
                unsafe {
                let bytes: *const std::ffi::c_char = objc::msg_send![self.0, UTF8String];
                let len = objc::msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING];
                let bytes = std::slice::from_raw_parts(bytes as *const u8, len);
                std::str::from_utf8_unchecked(bytes)
                }
            }
        }
        struct NSError(id);
        use block2::{Block, ConcreteBlock};

        let controller = webview.inner();
        let javascript_string = NSString::new("JSON.stringify(\"Hello, world!\");");
        let handler = ConcreteBlock::new(|val: NSString, err| {
            do_callback(val.to_str().to_string());
        });
        let handler = handler.copy();
        let handler: &Block<(NSString, NSError), ()> = &handler;

        let _: id = objc::msg_send![controller, evaluateJavaScript:javascript_string completionHandler:handler];
    }
});