blob: c453cceae5c7d8cab8d622740161c05c9b589526 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This module provides a safe, high-level `Trap` API that *does not* require
//! any management of raw `context` objects.
//!
//! This is the preferred trap API unless callers have constraints that make the
//! overhead of the heap allocations/tracking untenable - in that case the
//! lower-level `RawTrap` API may be considered.
//!
//! # Implementation details
//!
//! Manually setting RawTraps is potentially error-prone/unsafe due to
//! the requirement that, when adding a trigger to a trap, users provide
//! a raw `context` value (typically a pointer).
//!
//! This means users of RawTraps must manually
//! ensure the object pointed to by `context` outlives the trigger (and
//! do the associated pointer-casting/management).
//!
//! When we make a Trap with this interface:
//! 1. The user provides a closure (of type FnMut(&TrapEvent)) to `add_trigger`.
//! 2. That closure is encapsulated in a `Trigger` struct and moved onto the
//! heap.
//! 3. We take the memory address of `Trigger` and pass *that* to the C API as
//! the raw `context`.
//! 4. We return an opaque `TriggerId` to the user and use *that* to safely
//! track triggers.
//!
//! A diagram of what these relationships look like is as follows:
//!
//! +-------+
//! | Trap |
//! +-------+
//! / |
//! / |
//! / trigger_registry: Arc<TriggerRegistry>
//! / |
//! raw_trap |
//! | |
//! | |
//! | |
//! | |
//! v v
//! +----------+ +-------------------------------+
//! | RawTrap | | TriggerRegistry |
//! +----------+ +-------------------------------+
//! | handle | | trigger_map: |
//! +----------+ | HashMap<usize, Arc<Trigger> |
//! | | next_trigger_id: usize |
//! | +-------------------------------+
//! v | |
//! +------------+ v |
//! | MojoHandle | next_trigger_id |
//! +------------+ | |
//! | |
//! | +-----------------------------+
//! | | HashMap<usize, Arc<Trigger>>|------+
//! | +-----------------------------+ |
//! | | | v
//! | | | (other Triggers...)
//! v v
//! +------------------------------+ +--------------------+
//! | Trigger | | other Triggers... |
//! +------------------------------+ +--------------------+
//! | callback: FnMut(&TrapEvent) |
//! | owner: weak pointer to owning|
//! | TriggerRegistry |
//! | handle: MojoHandle |
//! | trigger_id |
//! +------------------------------+
//!
//! Two notes on the above:
//!
//! * Trigger objects hold weak pointers to their owning TriggerRegistry. This
//! allows Trigger objects to reach "up" and manage/cleanup tracking state
//! after being triggered.
//!
//! * Upon creating a RawTrap (which is done implicitly/"under the hood" when
//! creating a Trap), the underlying C API call MojoCreateTrap takes the
//! passed-in MojoTrapEventHandler function pointer and stores it in a place
//! to be managed by the Mojo system itself. It will be freed when the
//! MojoHandle for Trap is released.
// FOR_RELEASE: There are a lot of implementation details even in this file.
// Find a way to present *just* the API to end users.
pub use crate::raw_trap::TriggerCondition;
use crate::raw_trap::*;
use std::collections::HashMap;
use std::mem;
use std::sync::{Arc, Mutex, Weak};
chromium::import! {
"//mojo/public/rust/system:ffi_bindings" as mojo_ffi;
}
use mojo_ffi::{MojoError, MojoResult};
/// Unique ID for a trigger added to a `Trap`.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TriggerId(usize);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrapError {
/// The trigger was removed (explicitly or by closure).
Cancelled,
/// The signal can no longer happen on the handle.
FailedPrecondition,
}
/// This policy specifies how the trap should behave if there are blocking
/// events when it is initially armed by the user.
///
/// If there are no blocking events, the trap will immediately become armed
/// regardless of this policy.
pub enum InitialArmingPolicy {
/// React to each blocking event as if it had occurred after arming, by
/// running the corresponding trigger's callback. Continue doing so until
/// no blocking events remain, at which point the trap will be armed.
///
/// The trigger _must_ at least try to make progress on clearing the
/// blocking events, or else it will keep looping indefinitely.
RunTriggersOnBlockingEvents,
/// Do not arm the trap, and return a list of blocking
/// events for the caller to handle themselves.
ReturnBlockingEvents,
}
/// This policy specifies whether the trap should automatically re-arm itself
/// after a trigger fires. If so, it will repeatedly fire all its triggers
/// until all blocking events are cleared. If not, it must be re-armed
/// manually after firing (often as part of the user-provided handler).
///
/// The `Automatic` policy is analogous to the `RunTriggersOnBlockingEvents`
/// initial arming policy, and has the same caveat: triggers must try to make
/// progress on clearing blocking events, or risk looping indefinitely.
///
/// FOR_RELEASE: Perhaps this is similar enough to the other arming policy that
/// they can be combined, but there are some differences. Decide on that.
pub enum RearmingPolicy {
Manual,
Automatic,
}
// Represents a trap event, e.g. a trigger firing.
#[derive(Clone, Copy, Debug)]
pub struct TrapEvent {
signals_state: SignalsState,
result: MojoResult<()>,
}
impl TrapEvent {
/// Why the trigger fired:
/// * Okay: a specified signal occurred.
/// * FailedPrecondition: a signal can no longer happen on the handle.
/// * Cancelled: the trigger was removed (explicitly or by closure).
// FOR_RELEASE: Can we just store the Result<(), TrapError> in the TrapEvent to
// start with?
pub fn result(&self) -> Result<(), TrapError> {
match self.result {
Ok(()) => Ok(()),
Err(MojoError::Cancelled) => Err(TrapError::Cancelled),
Err(MojoError::FailedPrecondition) => Err(TrapError::FailedPrecondition),
unhandled_result => {
panic!("TrapEvent received an unhandled MojoResult: {:?}", unhandled_result)
}
}
}
/// The handle's current and possible signals as of triggering.
pub fn signals_state(&self) -> SignalsState {
self.signals_state
}
}
impl From<&RawTrapEvent> for TrapEvent {
fn from(raw_event: &RawTrapEvent) -> Self {
TrapEvent { signals_state: raw_event.signals_state(), result: raw_event.result() }
}
}
/// A safe Trap object that invokes user-defined handler functions (on an
/// arbitrary thread) whenever its trigger conditions are met.
///
/// To use the trap, add a "Trigger": a combination of a `FnMut` handler and
/// a specification of when it should fire, then arm the trap. Once it's armed,
/// it will invoke the handler whenever conditions are met. The trap can either
/// re-arm itself automatically after each trigger, or require manual rearming.
pub struct Trap {
// The trap needs to share its internal state with its handler, so we make
// separate internal structs wrapped in synchronization primitives.
// This one is immutable so no need for a mutex.
shared_data: Arc<TrapSharedData>,
// This data holds a strong ref for each trigger, to prevent them from dying early.
// Each trigger holds a circular reference to this data so it can delete itself.
trigger_registry: Arc<Mutex<TriggerRegistry>>,
}
impl Trap {
/// Create a new trap with no triggers.
///
/// # Possible Error Codes:
/// - `ResourceExhausted`: If the trap handler was unable to be created
/// (e.g. because the process ran out of possible handle values)
pub fn new(rearming_policy: RearmingPolicy) -> MojoResult<Self> {
Ok(Trap {
shared_data: Arc::new(TrapSharedData {
// SAFETY: The requirements of `handle_event_from_callback` are
// guaranteed by the C trap API
raw_trap: RawTrap::new(Self::handle_event_from_callback)?,
rearming_policy,
}),
trigger_registry: Arc::new(Mutex::new(TriggerRegistry {
trigger_map: HashMap::new(),
next_trigger_id: 0,
})),
})
}
/// The underlying C trap object expects to get a single handler function
/// which is invoked by all triggers. This is that function.
///
/// This function's job is to take the provided `context` information from
/// the `RawTrapEvent` and use it to invoke the actual user-provided
/// handler. It does so by interpreting the `context` as a weak pointer to
/// the a specific `Trigger` which contains the rest of the data.
///
/// # Safety consideration
///
/// This function must not be called concurrently with the same `context`
/// value. After it is called with a `Cancelled` result, it must never be
/// called again with the same `context` value.
extern "C" fn handle_event_from_callback(raw_event: &RawTrapEvent) {
// Get the Arc<Trigger> from the raw context.
// SAFETY: Same conditions as this function
let trigger_data = unsafe { Self::get_trigger_data_from_event(raw_event) };
// These upgrades should only fail if the trap has been dropped,
// in which case there's nothing left for us to do.
let Some(trigger_data) = trigger_data else {
return;
};
let Some(owner) = trigger_data.owner.upgrade() else {
return;
};
let Some(shared_data) = trigger_data.shared_data.upgrade() else {
return;
};
// Call the user's callback, acquiring the associated lock.
(trigger_data.callback.lock().unwrap())(&raw_event.into());
if raw_event.result() == Err(MojoError::Cancelled) {
let trigger_id = trigger_data.trigger_id;
// Drop our `trigger_data` Arc *before* calling, so we don't have a
// strong ref on the stack during removal.
drop(trigger_data);
Self::remove_cancelled_trigger(&mut owner.lock().unwrap(), trigger_id);
}
// If the precondition is failed, we can't ever make progress again, so
// remove the trigger. This will trigger an immediate handler with
// `Cancelled` that will actually do the cleanup.
if raw_event.result() == Err(MojoError::FailedPrecondition) {
shared_data.raw_trap.remove_trigger(raw_event.trigger_context()).unwrap();
}
// Re-arm the trap if the rearming policy is set to automatic
if matches!(shared_data.rearming_policy, RearmingPolicy::Automatic) {
// The only way this can fail is if the trap has no triggers. This
// is possible if we were the last trigger and just got cancelled.
let _ = Self::handle_blocking_events_until_armed(&shared_data.raw_trap);
}
}
// # Safety consideration
// The callback passed in to add_trigger should be able to account for
// both an Ok() MojoResult (the "happy" path) and the following TrapErrors:
// * `Cancelled``, which is returned every time a Trap goes out of scope, and
// * `FailedPrecondition`, which is returned if the conditions for this trigger
// ever become impossible (e.g., if one end of a pipe is closed, such that the
// other end will never again become readable)
// You can find examples in `core_api_unittests.rs`, e.g. in
// `test_trap_multiple_blocking_events`
pub fn add_trigger(
&self,
handle_to_trap: &impl Trappable,
signals: HandleSignals,
condition: TriggerCondition,
callback: impl FnMut(&TrapEvent) + Send + 'static,
) -> TriggerId {
let (trigger_data_ptr, id): (*const Trigger, _) = {
let mut trigger_registry = self.trigger_registry.lock().unwrap();
let id = TriggerId(trigger_registry.next_trigger_id);
trigger_registry.next_trigger_id += 1;
let trigger_data = Arc::new(Trigger {
callback: Mutex::new(Box::new(callback)),
owner: Arc::downgrade(&self.trigger_registry),
shared_data: Arc::downgrade(&self.shared_data),
trigger_id: id,
});
if trigger_registry.trigger_map.insert(id, trigger_data.clone()).is_some() {
panic!("trigger_id unexpectedly already exists in trigger_map")
}
(Arc::downgrade(&trigger_data).into_raw(), id)
};
// Note: The mojo API says that it's invalid to add multiple traps on the
// same handle, but it seems that requirement was silently dropped when
// we transferred to ipcz.
self.shared_data
.raw_trap
.add_trigger(handle_to_trap, signals, condition, trigger_data_ptr as usize)
.expect("The Trap class handles all possible failures when adding a trigger");
return id;
}
/// Remove the trigger with the given ID from the trap.
///
/// The handler will be invoked one last time with a `Cancelled` result.
/// Note that the handler is invoked asynchronously, and may not finish
/// before this function returns.
///
/// # Possible Error Codes:
/// - `NotFound`: If the trigger with that ID has already been removed
pub fn remove_trigger(&mut self, trigger_id: TriggerId) -> MojoResult<()> {
// The handler will do cleanup of the trigger's weak pointer and
// remove the key from our internal map.
let context = Arc::as_ptr(
self.trigger_registry
.lock()
.unwrap()
.trigger_map
.get(&trigger_id)
.ok_or(MojoError::NotFound)?,
) as usize;
self.shared_data.raw_trap.remove_trigger(context)
}
/// Removes *all* triggers from this Trap.
///
/// See the documentation of `remove_trigger` for caveats.
pub fn clear_triggers(&mut self) {
let triggers_to_remove: Vec<TriggerId> = {
let registry = self.trigger_registry.lock().unwrap();
registry.trigger_map.keys().copied().collect()
};
for trigger_id in triggers_to_remove {
// A failure here means the trigger was already removed
let _ = self.remove_trigger(trigger_id);
}
}
/// Attempt to arm the trap. The arming policy determines what to do if the
/// trap would immediately fire upon arming:
///
/// - `RunTriggersOnBlockingEvents`: Repeatedly run the handler of each
/// event until none remain. This will cause an infinite loop if the
/// handler does not remove events (e.g. by reading the corresponding
/// message from a pipe)!
/// - `ReturnBlockingEvents`: Return a list of blocking events for the
/// caller to handle manually (FOR_RELEASE: implement this!)
///
/// # Possible Error Codes
/// - `NotFound` if the trap has no triggers.
pub fn arm(&self, arming_policy: InitialArmingPolicy) -> MojoResult<()> {
match arming_policy {
InitialArmingPolicy::RunTriggersOnBlockingEvents => {
Self::handle_blocking_events_until_armed(&self.shared_data.raw_trap)
}
InitialArmingPolicy::ReturnBlockingEvents => {
todo!()
}
}
}
fn handle_blocking_events_until_armed(raw_trap: &RawTrap) -> MojoResult<()> {
const MAX_BLOCKING_EVENTS: usize = 16;
let mut buf = [const { mem::MaybeUninit::uninit() }; MAX_BLOCKING_EVENTS];
loop {
let blocking_events: &[RawTrapEvent] = match raw_trap.arm(Some(&mut buf)) {
ArmResult::Blocked(events) => events,
ArmResult::Armed => return Ok(()),
ArmResult::Failed(e) => {
return Err(e);
}
};
for blocking_event in blocking_events {
// SAFETY: Removing a trigger requires `&mut self`, so the trap won't fire a
// `Cancelled` event while we're doing this. It won't fire any other event
// because it's disarmed.
// FOR_RELEASE: Verify that if a trigger gets cancelled, any blocking events for
// it get removed from the queue. And ideally that happens immediately so
// there's no risk of the cancelled handler running while we're looking at
// these events.
let trigger_data = unsafe { Self::get_trigger_data_from_event(blocking_event) };
// The unwraps should succeed because the trap is still alive
(trigger_data.unwrap().callback.lock().unwrap())(&blocking_event.into());
if blocking_event.result() == Err(MojoError::FailedPrecondition) {
// We know the trigger hasn't been removed yet because it's blocking!
raw_trap.remove_trigger(blocking_event.trigger_context()).unwrap();
}
}
}
}
/// Takes a given RawTrapEvent and determines the associated Trigger. Also
/// decrements the trigger's `Weak` count if the trigger will never again
/// be called.
///
/// # Safety
///
/// This function must not be called concurrently with the same `context`
/// value. After it is called with a `Cancelled` result, it must never be
/// called again with the same context.
unsafe fn get_trigger_data_from_event(raw_event: &RawTrapEvent) -> Option<Arc<Trigger>> {
// The context value is really a weak pointer to this event's trigger.
let trigger_data_ptr = raw_event.trigger_context() as *const Trigger;
// SAFETY: This pointer was created by `Weak::into_raw` in `add_trigger`
// The function's safety requirements guarantee that the pointer is still
// valid and no other `Weak` owns its ref-count. If the result isn't
// `Cancelled, we'll release our hold on the ref-count when we forget this
// below.
let trigger_data_weak = unsafe { Weak::from_raw(trigger_data_ptr) };
let trigger_data = trigger_data_weak.upgrade();
// Any result besides `Cancelled` means that this `context` value might
// be re-used, so make sure we don't decrement the weak count when our
// reconstituted `Weak` pointer is dropped.
if raw_event.result() != Err(MojoError::Cancelled) {
mem::forget(trigger_data_weak);
}
return trigger_data;
}
fn remove_cancelled_trigger(trigger_registry: &mut TriggerRegistry, trigger_id: TriggerId) {
// If the `unwrap` panics, we've tried to remove a nonexistent handle, which
// should be impossible unelss there is an error in how we are handling
// handles.
let trigger_data: Arc<_> = trigger_registry.trigger_map.remove(&trigger_id).unwrap();
// If the caller managed the ref counts correctly, `trigger_data`'s inner
// data should be dropped after this call.
debug_assert_eq!(1, Arc::strong_count(&trigger_data), "unexpected strong ref");
debug_assert_eq!(0, Arc::weak_count(&trigger_data), "unexpected weak ref");
}
}
impl Drop for Trap {
fn drop(&mut self) {
// The C API will do this for us, but doing it here means we don't
// depend on drop order to make sure Arcs are alive.
self.clear_triggers();
}
}
struct TrapSharedData {
// The actual underlying trap object.
raw_trap: RawTrap,
// Whether the trap should automatically rearm each time a trigger fires or not
rearming_policy: RearmingPolicy,
}
struct TriggerRegistry {
// Represents a registry of triggers associated with a Trap.
//
// Data in the TriggerRegistry may be accessed on multiple threads.
//
// Triggers are identified by an ID: `add_trigger` returns the ID, and
// the client can later call `remove_trigger` on said ID to unsubscribe from
// events on the associated handle. To support removal we maintain a mapping
// from the client's IDs to our internal per-handle data.
//
// Each `Trigger` is owned through an `Arc` since we use `Weak` refs
// that we pass to the C side. The `Weak` refs are
// converted to raw pointers with `Weak::into_raw()`, passed to the C API,
// and reconstituted by `Weak::from_raw()` when passed to us by callback.
trigger_map: HashMap<TriggerId, Arc<Trigger>>,
// Field for a unique ID, generated per trigger.
next_trigger_id: usize,
}
// Internal state for a single active trigger.
//
// While `RawTrap` wraps the C Mojo API functions, this struct
// serves as the object *referenced by* the opaque `context` integer
// passed to that API.
//
// Thus this struct is referenced when making the necessary
// RawTrap calls "under the hood".
struct Trigger {
// FOR_RELEASE: These Mutexes *may* be imposing an unnecessary overhead
// on our end users (though "traps can be called back on any thread" is
// a pretty tricky constraint).
//
// Revisit here once we've got end-to-end Traps working, with an eye for
// seeing how the C++ API works in this case to guide a potential improved
// design. It may be possible to send these to a fixed sequence or
// the sequence the trap was armed on (once we have sequences available
// in Rust).
#[allow(clippy::type_complexity)]
callback: Mutex<Box<dyn FnMut(&TrapEvent) + Send + 'static>>,
// Trigger points back to the TriggerRegistry that "owns" it.
owner: Weak<Mutex<TriggerRegistry>>,
shared_data: Weak<TrapSharedData>,
trigger_id: TriggerId,
}