From b7f765e3f6d2debd0ee71589602415fae1df762d Mon Sep 17 00:00:00 2001 From: Stu Black Date: Wed, 12 Nov 2025 16:33:48 -0500 Subject: [PATCH] Rewrite WINGs/notification.c in Rust. --- WINGs/Makefile.am | 1 - WINGs/WINGs/WUtil.h | 2 - WINGs/notification.c | 278 -------------------------- WINGs/wapplication.c | 5 +- wutil-rs/Makefile.am | 1 + wutil-rs/src/lib.rs | 1 + wutil-rs/src/notification.rs | 368 +++++++++++++++++++++++++++++++++++ 7 files changed, 371 insertions(+), 285 deletions(-) delete mode 100644 WINGs/notification.c create mode 100644 wutil-rs/src/notification.rs diff --git a/WINGs/Makefile.am b/WINGs/Makefile.am index 74b3ffea..8a1a839e 100644 --- a/WINGs/Makefile.am +++ b/WINGs/Makefile.am @@ -73,7 +73,6 @@ libWUtil_la_SOURCES = \ menuparser.h \ menuparser_macros.c \ misc.c \ - notification.c \ proplist.c \ userdefaults.c \ userdefaults.h \ diff --git a/WINGs/WINGs/WUtil.h b/WINGs/WINGs/WUtil.h index f290f187..d4c499f1 100644 --- a/WINGs/WINGs/WUtil.h +++ b/WINGs/WINGs/WUtil.h @@ -599,8 +599,6 @@ void WMTreeWalk(WMTreeNode *aNode, WMTreeWalkProc * walk, void *data); /* ---[ WINGs/notification.c ]---------------------------------------------------- */ -void WMReleaseNotification(WMNotification *notification); - void* WMGetNotificationClientData(WMNotification *notification); void* WMGetNotificationObject(WMNotification *notification); diff --git a/WINGs/notification.c b/WINGs/notification.c deleted file mode 100644 index 335189d7..00000000 --- a/WINGs/notification.c +++ /dev/null @@ -1,278 +0,0 @@ - -#include -#include -#include -#include - -#include "WUtil.h" -#include "WINGsP.h" - - -typedef struct W_Notification { - const char *name; - void *object; - void *clientData; - int refCount; -} Notification; - - -static WMNotification* WMRetainNotification(WMNotification *notification); - -const char *WMGetNotificationName(WMNotification * notification) -{ - return notification->name; -} - -void *WMGetNotificationObject(WMNotification * notification) -{ - return notification->object; -} - -void *WMGetNotificationClientData(WMNotification * notification) -{ - return notification->clientData; -} - -WMNotification *WMCreateNotification(const char *name, void *object, void *clientData) -{ - Notification *nPtr; - - nPtr = wmalloc(sizeof(Notification)); - nPtr->name = name; - nPtr->object = object; - nPtr->clientData = clientData; - nPtr->refCount = 1; - - return nPtr; -} - -void WMReleaseNotification(WMNotification * notification) -{ - notification->refCount--; - - if (notification->refCount < 1) { - wfree(notification); - } -} - -static WMNotification *WMRetainNotification(WMNotification * notification) -{ - notification->refCount++; - - return notification; -} - -/***************** Notification Center *****************/ - -typedef struct NotificationObserver { - WMNotificationObserverAction *observerAction; - void *observer; - - const char *name; - void *object; - - struct NotificationObserver *prev; /* for tables */ - struct NotificationObserver *next; - struct NotificationObserver *nextAction; /* for observerTable */ -} NotificationObserver; - -typedef struct W_NotificationCenter { - WMHashTable *nameTable; /* names -> observer lists */ - WMHashTable *objectTable; /* object -> observer lists */ - NotificationObserver *nilList; /* obervers that catch everything */ - - WMHashTable *observerTable; /* observer -> NotificationObserver */ -} NotificationCenter; - -/* default (and only) center */ -static NotificationCenter *notificationCenter = NULL; - -void W_InitNotificationCenter(void) -{ - notificationCenter = wmalloc(sizeof(NotificationCenter)); - notificationCenter->nameTable = WMCreateStringHashTable(); - notificationCenter->objectTable = WMCreateIdentityHashTable(); - notificationCenter->nilList = NULL; - notificationCenter->observerTable = WMCreateIdentityHashTable(); -} - -void W_ReleaseNotificationCenter(void) -{ - if (notificationCenter) { - if (notificationCenter->nameTable) - WMFreeHashTable(notificationCenter->nameTable); - if (notificationCenter->objectTable) - WMFreeHashTable(notificationCenter->objectTable); - if (notificationCenter->observerTable) - WMFreeHashTable(notificationCenter->observerTable); - - wfree(notificationCenter); - notificationCenter = NULL; - } -} - -void -WMAddNotificationObserver(WMNotificationObserverAction * observerAction, - void *observer, const char *name, void *object) -{ - NotificationObserver *oRec, *rec; - - oRec = wmalloc(sizeof(NotificationObserver)); - oRec->observerAction = observerAction; - oRec->observer = observer; - oRec->name = name; - oRec->object = object; - oRec->next = NULL; - oRec->prev = NULL; - - /* put this action in the list of actions for this observer */ - rec = (NotificationObserver *) WMHashInsert(notificationCenter->observerTable, observer, oRec); - - if (rec) { - /* if this is not the first action for the observer */ - oRec->nextAction = rec; - } else { - oRec->nextAction = NULL; - } - - if (!name && !object) { - /* catch-all */ - oRec->next = notificationCenter->nilList; - if (notificationCenter->nilList) { - notificationCenter->nilList->prev = oRec; - } - notificationCenter->nilList = oRec; - } else if (!name) { - /* any message coming from object */ - rec = (NotificationObserver *) WMHashInsert(notificationCenter->objectTable, object, oRec); - oRec->next = rec; - if (rec) { - rec->prev = oRec; - } - } else { - /* name && (object || !object) */ - rec = (NotificationObserver *) WMHashInsert(notificationCenter->nameTable, name, oRec); - oRec->next = rec; - if (rec) { - rec->prev = oRec; - } - } -} - -void WMPostNotification(WMNotification * notification) -{ - NotificationObserver *orec, *tmp; - - WMRetainNotification(notification); - - /* tell the observers that want to know about a particular message */ - orec = (NotificationObserver *) WMHashGet(notificationCenter->nameTable, notification->name); - - while (orec) { - tmp = orec->next; - - if (!orec->object || !notification->object || orec->object == notification->object) { - /* tell the observer */ - if (orec->observerAction) { - (*orec->observerAction) (orec->observer, notification); - } - } - - orec = tmp; - } - - /* tell the observers that want to know about an object */ - orec = (NotificationObserver *) WMHashGet(notificationCenter->objectTable, notification->object); - - while (orec) { - tmp = orec->next; - - /* tell the observer */ - if (orec->observerAction) { - (*orec->observerAction) (orec->observer, notification); - } - orec = tmp; - } - - /* tell the catch all observers */ - orec = notificationCenter->nilList; - while (orec) { - tmp = orec->next; - - /* tell the observer */ - if (orec->observerAction) { - (*orec->observerAction) (orec->observer, notification); - } - orec = tmp; - } - - WMReleaseNotification(notification); -} - -void WMRemoveNotificationObserver(void *observer) -{ - NotificationObserver *orec, *tmp, *rec; - - /* get the list of actions the observer is doing */ - orec = (NotificationObserver *) WMHashGet(notificationCenter->observerTable, observer); - - /* - * FOREACH orec IN actionlist for observer - * DO - * remove from respective lists/tables - * free - * END - */ - while (orec) { - tmp = orec->nextAction; - - if (!orec->name && !orec->object) { - /* catch-all */ - if (notificationCenter->nilList == orec) - notificationCenter->nilList = orec->next; - } else if (!orec->name) { - /* any message coming from object */ - rec = (NotificationObserver *) WMHashGet(notificationCenter->objectTable, orec->object); - if (rec == orec) { - /* replace table entry */ - if (orec->next) { - WMHashInsert(notificationCenter->objectTable, orec->object, orec->next); - } else { - WMHashRemove(notificationCenter->objectTable, orec->object); - } - } - } else { - /* name && (object || !object) */ - rec = (NotificationObserver *) WMHashGet(notificationCenter->nameTable, orec->name); - if (rec == orec) { - /* replace table entry */ - if (orec->next) { - WMHashInsert(notificationCenter->nameTable, orec->name, orec->next); - } else { - WMHashRemove(notificationCenter->nameTable, orec->name); - } - } - } - if (orec->prev) - orec->prev->next = orec->next; - if (orec->next) - orec->next->prev = orec->prev; - - wfree(orec); - - orec = tmp; - } - - WMHashRemove(notificationCenter->observerTable, observer); -} - -void WMPostNotificationName(const char *name, void *object, void *clientData) -{ - WMNotification *notification; - - notification = WMCreateNotification(name, object, clientData); - - WMPostNotification(notification); - - WMReleaseNotification(notification); -} diff --git a/WINGs/wapplication.c b/WINGs/wapplication.c index 45070a22..312cbee9 100644 --- a/WINGs/wapplication.c +++ b/WINGs/wapplication.c @@ -44,9 +44,6 @@ void WMInitializeApplication(const char *applicationName, int *argc, char **argv WMApplication.argv[i] = wstrdup(argv[i]); } WMApplication.argv[i] = NULL; - - /* initialize notification center */ - W_InitNotificationCenter(); } void WMReleaseApplication(void) { @@ -60,7 +57,7 @@ void WMReleaseApplication(void) { */ w_save_defaults_changes(); - W_ReleaseNotificationCenter(); + W_ClearNotificationCenter(); if (WMApplication.applicationName) { wfree(WMApplication.applicationName); diff --git a/wutil-rs/Makefile.am b/wutil-rs/Makefile.am index e2ee2d06..ab11b9c9 100644 --- a/wutil-rs/Makefile.am +++ b/wutil-rs/Makefile.am @@ -10,6 +10,7 @@ RUST_SOURCES = \ src/hash_table.rs \ src/lib.rs \ src/memory.rs \ + src/notification.rs \ src/prop_list.rs \ src/string.rs src/tree.rs diff --git a/wutil-rs/src/lib.rs b/wutil-rs/src/lib.rs index ab94d66d..d2798268 100644 --- a/wutil-rs/src/lib.rs +++ b/wutil-rs/src/lib.rs @@ -5,6 +5,7 @@ pub mod defines; pub mod find_file; pub mod hash_table; pub mod memory; +pub mod notification; pub mod prop_list; pub mod string; pub mod tree; diff --git a/wutil-rs/src/notification.rs b/wutil-rs/src/notification.rs new file mode 100644 index 00000000..dc56c51e --- /dev/null +++ b/wutil-rs/src/notification.rs @@ -0,0 +1,368 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + ffi::{c_void, CStr}, + hash, + ptr::{self, NonNull}, + sync::{Mutex, OnceLock}, +}; + +/// Lightweight message from object to another. When a notification is sent, +/// registered [`Action`]s are called. +/// +/// Use [`ffi::WMAddNotificationObserver`] or [`NotificationCenter::register`] +/// to request notifications. +/// +/// ## Safety +/// +/// `Notification` encapsulates two data pointers. The Rust implementation +/// explicitly supports notifications across threads. To uphold Rust's safety +/// rules, `Notification`s must only be created with pointers that can be sent +/// across threads. The [`Sendable`] struct is provided to guarantee this. +/// +/// ## Rust rewrite notes +/// +/// This was originally a reference-counted structure, but it is so lightweight +/// (consisting of three pointers) that the Rust version is `Copy`. This +/// simplifies things --- each Rust recipient of a `Notification` owns it, and +/// there is no need to coordinate how it is cleaned up. +/// +/// In unported C code, a notificaton's `name` may be compared against a string +/// constant using pointer equality rather than string equality. This has +/// negative implications for sending notifications across the Rust/C +/// boundary. For the time being, notifications are generated and received by +/// code written in C, but care should be taken that notification names are +/// checked properly when porting notification registration in the future. (We +/// will ideally move to a different data type for identifying notifications, +/// too.) +#[derive(Clone, Copy)] +pub struct Notification { + name: &'static CStr, + /// The object that generated the notification. This may be `None` for + /// notifications that are about global state. + source: Option, + /// Optional side-channel data provided to notification listeners. + client_data: Option, +} + +/// Callback that notifies `observer` (which may be null) of `notification` (which won't be). +pub type Action = unsafe extern "C" fn(observer: *mut c_void, notification: *const Notification); + +/// Wraps a type-erased pointer (which it does not own) and marks it as `Send`. +/// +/// The `Send`-ability of the wrapped pointer must be guaranteed code that +/// instantiates a `Sendable`. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct Sendable { + ptr: NonNull, +} + +impl Sendable { + /// Creates a `Sendable` wrapping `ptr`. + /// + /// ## Safety + /// + /// `ptr` must be safe to send across threads. + pub unsafe fn new(ptr: NonNull) -> Self { + Sendable { ptr } + } +} + +impl hash::Hash for Sendable { + fn hash(&self, h: &mut H) { + h.write_usize(self.ptr.as_ptr().addr()); + } +} + +// Guaranteed by `Sendable::new`. +unsafe impl Send for Sendable {} + +#[derive(Default)] +pub struct NotificationCenter { + /// Notification subscriptions that match on name and source. + exact: HashMap<(&'static CStr, Sendable), Vec<(Option, Action)>>, + /// Notification subscriptions that match on name. + by_name: HashMap<&'static CStr, Vec<(Option, Action)>>, + /// Notification subscriptions that match on source. + by_source: HashMap, Action)>>, + /// Notification subscriptions that match all notifications. + universal: Vec<(Option, Action)>, +} + +// It is safe to send NotificationCenter across threads as long as the contract +// on Sendable is respected. +unsafe impl Send for NotificationCenter {} + +impl NotificationCenter { + /// Creates a new `NotificationCenter`. + pub fn new() -> Self { + Self::default() + } + + /// Provides access to the default, process-wide notification center. The + /// FFI C API uses this notification center. This is protected behind a + /// mutex that is held while `f` is run, so panicking inside of `f` should + /// be avoided. + pub fn with_global_default(f: impl FnOnce(&mut Self) -> R) -> R { + static INSTANCE: OnceLock> = OnceLock::new(); + f(&mut INSTANCE + .get_or_init(|| Mutex::new(NotificationCenter::new())) + .try_lock() + .unwrap()) + } + + /// Registers `action` to be invoked and invoked on `observer` when + /// notifications named `name` are fired from `source`. + pub fn register_exact( + &mut self, + name: &'static CStr, + source: Sendable, + observer: Option, + action: Action, + ) { + match self.exact.entry((name, source)) { + Entry::Occupied(mut o) => { + o.get_mut().push((observer, action)); + } + Entry::Vacant(v) => { + v.insert(vec![(observer, action)]); + } + } + } + + /// Registers `action` to be invoked on `observer` when notifications are + /// fired by `source` (regardless of the notification name). + pub fn register_by_source( + &mut self, + source: Sendable, + observer: Option, + action: Action, + ) { + match self.by_source.entry(source) { + Entry::Occupied(mut o) => { + o.get_mut().push((observer, action)); + } + Entry::Vacant(v) => { + v.insert(vec![(observer, action)]); + } + } + } + + /// Registers `action` to be invoked on `observer` for all notifications + /// named `name`. + pub fn register_by_name( + &mut self, + name: &'static CStr, + observer: Option, + action: Action, + ) { + match self.by_name.entry(name) { + Entry::Occupied(mut o) => { + o.get_mut().push((observer, action)); + } + Entry::Vacant(v) => { + v.insert(vec![(observer, action)]); + } + } + } + + /// Registers `action` to be invoked on `observer` for all notifications, + /// regardless of the notification's name or source. + pub fn register_universal(&mut self, observer: Option, action: Action) { + self.universal.push((observer, action)); + } + + /// Dispatches `notification` with registered actions. + pub fn dispatch(&mut self, notification: Notification) { + if let Some(observers) = self.by_name.get_mut(notification.name) { + for (observer, action) in observers { + let observer = observer.map(|x| x.ptr.as_ptr()).unwrap_or(ptr::null_mut()); + unsafe { + (action)(observer, ¬ification); + } + } + } + + if let Some(source) = notification.source { + if let Some(observers) = self.exact.get_mut(&(notification.name, source)) { + for (observer, action) in observers { + let observer = observer.map(|x| x.ptr.as_ptr()).unwrap_or(ptr::null_mut()); + unsafe { + (action)(observer, ¬ification); + } + } + } + if let Some(observers) = self.by_source.get_mut(&source) { + for (observer, action) in observers { + let observer = observer.map(|x| x.ptr.as_ptr()).unwrap_or(ptr::null_mut()); + unsafe { + (action)(observer, ¬ification); + } + } + } + } + + for (observer, action) in &mut self.universal { + let observer = observer.map(|x| x.ptr.as_ptr()).unwrap_or(ptr::null_mut()); + unsafe { + (action)(observer, ¬ification); + } + } + } + + /// Removes all notification subscriptions that would notify `observer` if they fired. + pub fn remove_observer(&mut self, observer: Sendable) { + self.exact.retain(|_, values| { + values.retain(|(o, _)| *o != Some(observer)); + !values.is_empty() + }); + self.by_name.retain(|_, values| { + values.retain(|(o, _)| *o != Some(observer)); + !values.is_empty() + }); + self.by_source.retain(|_, values| { + values.retain(|(o, _)| *o != Some(observer)); + !values.is_empty() + }); + self.universal.retain(|(o, _)| *o != Some(observer)); + } + + /// Clears all registered notification listeners and resets `self` to its + /// default state. + pub fn clear(&mut self) { + *self = Self::default(); + } +} + +pub mod ffi { + use super::{Action, Notification, NotificationCenter, Sendable}; + + use std::{ + ffi::{c_char, c_void, CStr}, + ptr::{self, NonNull}, + }; + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetNotificationClientData( + notification: *mut Notification, + ) -> *mut c_void { + if notification.is_null() { + return ptr::null_mut(); + } + unsafe { + (*notification) + .client_data + .map(|x| x.ptr.as_ptr()) + .unwrap_or(ptr::null_mut()) + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetNotificationObject( + notification: *mut Notification, + ) -> *mut c_void { + if notification.is_null() { + return ptr::null_mut(); + } + unsafe { + (*notification) + .source + .map(|x| x.ptr.as_ptr()) + .unwrap_or(ptr::null_mut()) + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetNotificationName( + notification: *mut Notification, + ) -> *const c_char { + if notification.is_null() { + return ptr::null_mut(); + } + unsafe { (*notification).name.as_ptr() } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMAddNotificationObserver( + action: Option, + observer: *mut c_void, + name: *const c_char, + object: *mut c_void, + ) { + let Some(action) = action else { + return; + }; + let observer = NonNull::new(observer).map(|x| unsafe { Sendable::new(x) }); + let source = NonNull::new(object); + NotificationCenter::with_global_default(|c| { + if name.is_null() { + match source { + Some(source) => { + c.register_by_source(unsafe { Sendable::new(source) }, observer, action); + } + None => c.register_universal(observer, action), + } + } else { + let name = unsafe { CStr::from_ptr(name) }; + match source { + Some(source) => { + c.register_exact(name, unsafe { Sendable::new(source) }, observer, action); + } + None => c.register_by_name(name, observer, action), + } + } + }); + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMRemoveNotificationObserver(observer: *mut c_void) { + let Some(observer) = NonNull::new(observer) else { + return; + }; + + NotificationCenter::with_global_default(|c| { + c.remove_observer(unsafe { Sendable::new(observer) }) + }); + } + + /// Posts a notification from `object` with the given `name` and `client_data`. + /// + /// ## Safety + /// + /// `name` must be a non-null string constant or some other pointer with a + /// static lifetime. + /// + /// `object` and `client_data` must be safe to send across threads (per the + /// contract of [`Sendable`]). + /// + /// ## Rust rewrite notes + /// + /// This originally took a heap-allocated `*mut Notification`, but now the + /// constructed `Notification` parameters are passed directly. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMPostNotificationName( + name: *const c_char, + object: *mut c_void, + client_data: *mut c_void, + ) { + if name.is_null() { + return; + } + let name = unsafe { CStr::from_ptr(name) }; + let source = NonNull::new(object).map(|x| unsafe { Sendable::new(x) }); + let client_data = NonNull::new(client_data).map(|x| unsafe { Sendable::new(x) }); + NotificationCenter::with_global_default(|c| { + c.dispatch(Notification { + name, + source, + client_data, + }) + }); + } + + /// Resets all notifications for the global notification center. Used by + /// `WApplication` teardown code. This is a private, WINGs-internal API. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn W_ClearNotificationCenter() { + NotificationCenter::with_global_default(|c| c.clear()); + } +}