1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use std::os::raw::c_void;
use cogcore_sys as sys;
use crate::{object::ObjectPtr, raw};
/// Owned wrapper around a WebKitUserContentManager.
#[derive(Clone)]
pub struct UserContentManager {
ptr: ObjectPtr<sys::WebKitUserContentManager>,
}
impl UserContentManager {
pub(crate) fn from_borrowed(
ptr: *mut sys::WebKitUserContentManager,
context: &'static str,
) -> crate::Result<Self> {
Ok(Self {
ptr: ObjectPtr::from_borrowed(ptr, context)?,
})
}
/// Adds a user script to be injected into every page loaded by the owning view.
pub fn add_script(&self, script: &crate::UserScript) {
// SAFETY: manager and script are live GObject references for the duration of this call.
unsafe { sys::webkit_user_content_manager_add_script(self.as_ptr(), script.as_ptr()) };
}
/// Registers a named JS→Rust message channel.
///
/// After this call JS can call:
/// `window.webkit.messageHandlers.<name>.postMessage(data)`
pub fn register_script_message_handler(&self, name: &str) -> crate::Result<()> {
let name = raw::cstring(name)?;
// SAFETY: manager is a live GObject and name is a valid NUL-terminated string.
unsafe {
sys::webkit_user_content_manager_register_script_message_handler(
self.as_ptr(),
name.as_ptr(),
std::ptr::null(),
)
};
Ok(())
}
/// Connects `callback` to receive JS messages posted to the channel named `name`.
///
/// `name` must match a prior call to [`register_script_message_handler`]. The
/// closure receives the raw string passed to `postMessage` from JS.
pub fn connect_script_message(
&self,
name: &str,
callback: impl Fn(String) + 'static,
) -> crate::Result<()> {
let signal = format!("script-message-received::{name}");
let signal_cstr = raw::cstring(&signal)?;
let boxed: Box<dyn Fn(String) + 'static> = Box::new(callback);
let data = Box::into_raw(Box::new(boxed)).cast::<c_void>();
// SAFETY: GCallback is a type-erased C function pointer; the actual ABI
// matches the script-message-received signal: (manager, value, user_data).
let cb: unsafe extern "C" fn() = unsafe {
std::mem::transmute::<
unsafe extern "C" fn(
*mut sys::WebKitUserContentManager,
*mut sys::JSCValue,
sys::gpointer,
),
unsafe extern "C" fn(),
>(script_message_trampoline)
};
// SAFETY: manager is a live GObject; data is a leaked Box valid until
// destroy_closure_data is called by GLib when the signal is disconnected.
unsafe {
sys::g_signal_connect_data(
self.as_ptr().cast::<c_void>(),
signal_cstr.as_ptr(),
Some(cb),
data,
Some(destroy_closure_data),
0,
)
};
Ok(())
}
/// Returns the underlying pointer.
pub fn as_ptr(&self) -> *mut sys::WebKitUserContentManager {
self.ptr.as_ptr()
}
}
unsafe extern "C" fn script_message_trampoline(
_manager: *mut sys::WebKitUserContentManager,
value: *mut sys::JSCValue,
user_data: sys::gpointer,
) {
// SAFETY: user_data is a leaked Box<Box<dyn Fn(String)>> from connect_script_message,
// kept alive by g_signal_connect_data until destroy_closure_data is called.
let callback = unsafe { &*(user_data as *const Box<dyn Fn(String) + 'static>) };
if value.is_null() {
return;
}
// SAFETY: value is a live JSCValue delivered by WebKit's signal dispatch.
let str_ptr = unsafe { sys::jsc_value_to_string(value) };
let message = raw::string_from_owned(str_ptr, "jsc_value_to_string").unwrap_or_default();
callback(message);
}
unsafe extern "C" fn destroy_closure_data(data: sys::gpointer, _closure: *mut sys::GClosure) {
// SAFETY: data is a leaked Box<Box<dyn Fn(String)>> from connect_script_message.
// GLib calls this exactly once when the signal connection is destroyed.
let boxed = unsafe { Box::from_raw(data as *mut Box<dyn Fn(String) + 'static>) };
drop(boxed);
}