Skip to content

Commit

Permalink
implement sync_ipc_handler for Windows
Browse files Browse the repository at this point in the history
or: a trip through the horrors of COM
  • Loading branch information
Arc-blroth committed Aug 26, 2022
1 parent 83b6a8d commit ecc7d97
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 1 deletion.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ workspace = { }
[package]
name = "wry"
version = "0.20.2"
authors = [ "Tauri Programme within The Commons Conservancy" ]
authors = [ "Tauri Programme within The Commons Conservancy", "The RaptorFX Team" ]
edition = "2021"
license = "Apache-2.0 OR MIT"
description = "Cross-platform WebView rendering library"
Expand Down Expand Up @@ -57,14 +57,17 @@ gtk = "0.15"
gdk = "0.15"

[target."cfg(target_os = \"windows\")".dependencies]
const-utf16 = "0.2.1"
webview2-com = "0.16.0"
windows-implement = "0.37.0"
windows-interface = "0.37.0"

[target."cfg(target_os = \"windows\")".dependencies.windows]
version = "0.37.0"
features = [
"alloc",
"implement",
"interface",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
Expand Down
14 changes: 14 additions & 0 deletions src/webview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ pub struct WebViewAttributes {
///
/// Both functions return promises but `notify()` resolves immediately.
pub ipc_handler: Option<Box<dyn Fn(&Window, String)>>,
/// Set the blocking IPC handler to receive a message from Javascript and send back a response.
/// The message sent from webview should call `let response = window.ipc.postSyncMessage("message");`.
pub sync_ipc_handler: Option<Box<dyn Fn(&Window, String) -> String>>,
/// Set a handler closure to process incoming [`FileDropEvent`] of the webview.
///
/// # Blocking OS Default Behavior
Expand Down Expand Up @@ -175,6 +178,7 @@ impl Default for WebViewAttributes {
initialization_scripts: vec![],
custom_protocols: vec![],
ipc_handler: None,
sync_ipc_handler: None,
file_drop_handler: None,
navigation_handler: None,
new_window_req_handler: None,
Expand Down Expand Up @@ -275,6 +279,16 @@ impl<'a> WebViewBuilder<'a> {
self
}

/// Set the blocking IPC handler to receive a message from Javascript and send back a response.
/// The message sent from webview should call `let response = window.ipc.postSyncMessage("message");`.
pub fn with_sync_ipc_handler<F>(mut self, handler: F) -> Self
where
F: (Fn(&Window, String) -> String) + 'static,
{
self.webview.sync_ipc_handler = Some(Box::new(handler));
self
}

/// Set a handler closure to process incoming [`FileDropEvent`] of the webview.
///
/// # Blocking OS Default Behavior
Expand Down
9 changes: 9 additions & 0 deletions src/webview/webview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
// SPDX-License-Identifier: MIT

mod file_drop;
mod sync_ipc;

use crate::{
webview::{WebContext, WebViewAttributes},
Error, Result,
};

use file_drop::FileDropController;
use sync_ipc::SyncIPCHandler;

use std::{collections::HashSet, mem::MaybeUninit, rc::Rc, sync::mpsc};

Expand Down Expand Up @@ -66,6 +68,9 @@ impl InnerWebView {
let file_drop_handler = attributes.file_drop_handler.take();
let file_drop_window = window.clone();

let sync_ipc_handler = attributes.sync_ipc_handler.take();
let sync_ipc_window = window.clone();

let env = Self::create_environment(&web_context)?;
let controller = Self::create_controller(hwnd, &env)?;
let webview = Self::init_webview(window, hwnd, attributes, &env, &controller)?;
Expand All @@ -76,6 +81,10 @@ impl InnerWebView {
let _ = file_drop_controller.set(controller);
}

if let Some(handler) = sync_ipc_handler {
SyncIPCHandler::new(sync_ipc_window, handler)?.inject(&webview)?;
}

Ok(Self {
controller,
webview,
Expand Down
242 changes: 242 additions & 0 deletions src/webview/webview2/sync_ipc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright 2022 The RaptorFX Team, ReMod Software
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{
mem::{size_of, ManuallyDrop},
rc::Rc,
};

use windows::{
core::{IUnknown, IUnknownVtbl, Interface, GUID, PCWSTR, PWSTR},
Win32::{
Foundation::{BSTR, DISP_E_BADINDEX, DISP_E_UNKNOWNINTERFACE},
Globalization::{LocaleNameToLCID, LOCALE_NAME_INVARIANT},
System::{
Com::{
IDispatch, IDispatch_Impl, ITypeInfo, CC_STDCALL, DISPPARAMS, EXCEPINFO, VARIANT,
VARIANT_0, VARIANT_0_0, VARIANT_0_0_0,
},
Ole::{
CreateDispTypeInfo, DispGetIDsOfNames, DispInvoke, DISPATCH_METHOD, INTERFACEDATA,
METHODDATA, PARAMDATA, VT_BSTR, VT_DISPATCH,
},
},
},
};

use windows_implement::implement;
use windows_interface::interface;

use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2;

use crate::application::window::Window;

macro_rules! pwstr {
($string:literal) => {{
const UTF16: &[u16] = ::const_utf16::encode_null_terminated!($string);
static mut MUTABLE_UTF16: [u16; UTF16.len()] = {
let mut out = [0; UTF16.len()];
unsafe {
::std::ptr::copy_nonoverlapping(UTF16.as_ptr(), out.as_mut_ptr(), UTF16.len());
}
out
};
unsafe { ::windows::core::PWSTR(&mut MUTABLE_UTF16 as *mut _) }
}};
}

#[interface("e0912f1d-f683-40cd-94c6-20a1d7e96bdc")]
unsafe trait ISyncIPCHandler: IUnknown {
unsafe fn PostSyncMessage(&self, message: BSTR) -> BSTR;
}

#[implement(IDispatch, ISyncIPCHandler)]
pub(crate) struct SyncIPCHandler {
type_info: ITypeInfo,
window: Rc<Window>,
handler: Box<dyn Fn(&Window, String) -> String>,
}

impl SyncIPCHandler {
pub(crate) fn new(
window: Rc<Window>,
handler: Box<dyn Fn(&Window, String) -> String>,
) -> webview2_com::Result<Self> {
// Safety: we never mutate SyncIPCHandler's type information, so the below statics can be safely Sync
#[repr(transparent)]
struct SyncStatic<T>(T);

unsafe impl<T> Sync for SyncStatic<T> {}

static mut DISPATCH_INTERFACE_POST_SYNC_MESSAGE_PARAMS: SyncStatic<[PARAMDATA; 1]> =
SyncStatic([PARAMDATA {
szName: pwstr!("message"),
vt: VT_BSTR.0 as u16,
}]);

static mut DISPATCH_INTERFACE_METHODS: SyncStatic<[METHODDATA; 1]> = SyncStatic([METHODDATA {
szName: pwstr!("PostSyncMessage"),
ppdata: unsafe { &mut DISPATCH_INTERFACE_POST_SYNC_MESSAGE_PARAMS.0 as *mut _ },
dispid: 0,
// PostSyncMessage is the first method in ISyncIPCHandler
#[allow(clippy::identity_op)]
iMeth: (size_of::<IUnknownVtbl>() / size_of::<fn()>() + 0) as u32,
cc: CC_STDCALL,
cArgs: unsafe { DISPATCH_INTERFACE_METHODS.0.len() as u32 },
wFlags: DISPATCH_METHOD as u16,
vtReturn: VT_BSTR.0 as u16,
}]);

static mut DISPATCH_INTERFACE: SyncStatic<INTERFACEDATA> = SyncStatic(INTERFACEDATA {
pmethdata: unsafe { &mut DISPATCH_INTERFACE_METHODS.0 as *mut _ },
cMembers: unsafe { DISPATCH_INTERFACE_METHODS.0.len() as u32 },
});

// Safety: WinAPI calls are unsafe
let type_info = unsafe {
let invariant_locale = LocaleNameToLCID(LOCALE_NAME_INVARIANT, 0);
let mut type_info = None;
CreateDispTypeInfo(
&mut DISPATCH_INTERFACE.0 as *mut _,
invariant_locale,
&mut type_info as *mut _,
)?;
type_info.unwrap()
};

Ok(Self {
type_info,
window,
handler,
})
}

pub(crate) fn inject(self, webview: &ICoreWebView2) -> webview2_com::Result<()> {
let handler: IDispatch = self.into();

// Safety: WinAPI calls are unsafe
unsafe {
// wrapper struct to ensure that VARIANT ManuallyDrops are actually dropped
#[repr(transparent)]
struct IDispatchVariant(VARIANT);

impl Drop for IDispatchVariant {
fn drop(&mut self) {
unsafe {
if self.0.Anonymous.Anonymous.vt == VT_DISPATCH.0 as u16 {
ManuallyDrop::drop(&mut (&mut self.0.Anonymous.Anonymous).Anonymous.pdispVal);
}
ManuallyDrop::drop(&mut self.0.Anonymous.Anonymous);
}
}
}

let mut remote_obj = IDispatchVariant(VARIANT {
Anonymous: VARIANT_0 {
Anonymous: ManuallyDrop::new(VARIANT_0_0 {
vt: VT_DISPATCH.0 as u16,
Anonymous: VARIANT_0_0_0 {
pdispVal: ManuallyDrop::new(Some(handler)),
},
..VARIANT_0_0::default()
}),
},
});

webview
.AddHostObjectToScript(
PCWSTR(const_utf16::encode_null_terminated!("ipc") as *const _),
&mut remote_obj.0 as *mut _,
)
.map_err(webview2_com::Error::WindowsError)
}
}
}

#[allow(non_snake_case)]
impl IDispatch_Impl for SyncIPCHandler {
fn GetTypeInfoCount(&self) -> windows::core::Result<u32> {
Ok(1)
}

fn GetTypeInfo(&self, itinfo: u32, _lcid: u32) -> windows::core::Result<ITypeInfo> {
if itinfo != 0 {
Err(DISP_E_BADINDEX.into())
} else {
Ok(self.type_info.clone())
}
}

fn GetIDsOfNames(
&self,
riid: *const GUID,
rgsznames: *const PWSTR,
cnames: u32,
_lcid: u32,
rgdispid: *mut i32,
) -> windows::core::Result<()> {
// Safety: riid is checked for null before deref + WinAPI calls are unsafe
unsafe {
if riid.is_null() || *riid != GUID::default() {
Err(DISP_E_UNKNOWNINTERFACE.into())
} else {
DispGetIDsOfNames(&self.type_info, rgsznames, cnames, rgdispid)
}
}
}

fn Invoke(
&self,
dispidmember: i32,
riid: *const GUID,
_lcid: u32,
wflags: u16,
pdispparams: *const DISPPARAMS,
pvarresult: *mut VARIANT,
pexcepinfo: *mut EXCEPINFO,
puargerr: *mut u32,
) -> windows::core::Result<()> {
// Safety: pointers are checked for null before deref + WinAPI calls are unsafe
unsafe {
if riid.is_null() || *riid != GUID::default() {
Err(DISP_E_UNKNOWNINTERFACE.into())
} else {
let this: ISyncIPCHandler = self.cast()?;

// Invoke takes a *const DISPPARAMS but DispInvoke wants a *mut DISPPARAMS ???
let mut dispparams = if pdispparams.is_null() {
None
} else {
Some(*pdispparams)
};
let pdispparams_mut = dispparams
.as_mut()
.map(|x| x as _)
.unwrap_or(std::ptr::null_mut());

DispInvoke(
this.as_raw(),
&self.type_info,
dispidmember,
wflags,
pdispparams_mut,
pvarresult,
pexcepinfo,
puargerr,
)
}
}
}
}

#[allow(non_snake_case)]
impl ISyncIPCHandler_Impl for SyncIPCHandler {
unsafe fn PostSyncMessage(&self, message: BSTR) -> BSTR {
if let Ok(utf8_message) = message.try_into() {
(self.handler)(&self.window, utf8_message).into()
} else {
BSTR::default()
}
}
}

0 comments on commit ecc7d97

Please sign in to comment.