Standard library / std::abort_controller

std::abort_controller

This module provides a WHATWG-style AbortController / AbortSignal pair for cooperative cancellation across async functions and OS-thread-backed task concurrency.

The goal is to make cancellation obvious and uniform across the standard library:

  • APIs that can be aborted accept an AbortSignalBorrow (usually as an optional parameter).
  • Callers create and own an AbortController, and pass controller.signal() to the operations they want to be able to abort.
  • When a signal is aborted, operations stop early and return an Aborted error-kind in their module’s native error type.

Thread-safety:

  • AbortSignal state is protected by a mutex + condition variable.
  • AbortSignalBorrow is a non-owning, copyable handle intended for sharing across OS threads and task boundaries.

Exported API#

module std::abort_controller;

import std::interfaces;
import std::memory;
import std::result;

export enum AbortReasonKind {
  Aborted,
  Message,
}

export struct AbortReason {
  kind: AbortReasonKind,
  message: string,
}

// Owns the abort state.
export struct AbortController {
  signal: AbortSignal,
}

// Owns the abort state (dropped/destroyed by the last owner).
export struct AbortSignal {
  handle: u64,
}

// Non-owning, copyable view of an abort signal.
export struct AbortSignalBorrow {
  handle: u64,
}

export type AbortControllerResult = std::result::Result(AbortController, std::memory::OutOfMemory);

impl AbortController {
  public fn init () -> AbortControllerResult;
  public fn signal (self: &AbortController) -> AbortSignalBorrow;
  public fn abort (self: &AbortController) -> void;
  public fn abort_with_message (self: &AbortController, message: string) -> std::memory::OutOfMemory?;
  public fn destroy (mut self: &AbortController) -> void;
}

impl AbortSignal {
  public fn invalid () -> AbortSignal;
  public fn is_valid (self: &AbortSignal) -> bool;
  public fn borrow (self: &AbortSignal) -> AbortSignalBorrow;
  public fn is_aborted (self: &AbortSignal) -> bool;
  public fn reason (self: &AbortSignal) -> AbortReason?;
  public fn wait_fd (self: &AbortSignal) -> int?;
  public fn wait (self: &AbortSignal) -> void;
  public fn destroy (mut self: &AbortSignal) -> void;
}

impl AbortSignalBorrow {
  public fn is_aborted (self: &AbortSignalBorrow) -> bool;
  public fn reason (self: &AbortSignalBorrow) -> AbortReason?;
  public fn wait_fd (self: &AbortSignalBorrow) -> int?;
  public fn wait (self: &AbortSignalBorrow) -> void;
}

Semantics#

  • AbortController.init() creates an un-aborted signal.
  • AbortController.signal() returns an AbortSignalBorrow that points to the controller’s underlying signal state.
  • AbortSignalBorrow is non-owning:
  • it is safe to copy and pass across tasks/threads,
  • it becomes invalid once the owning AbortController/AbortSignal is destroyed or dropped.
  • AbortController.abort() is idempotent: aborting an already-aborted signal is a no-op (the original reason is preserved).
  • abort_with_message aborts with a user-provided message. If the message cannot be copied due to allocation failure, the signal is still aborted, and the method returns Some(OutOfMemory{...}).
  • AbortSignal.reason() returns an AbortReason whose message: string view is backed by memory owned by the signal. Do not use the returned message after the signal/controller has been destroyed or dropped.
  • AbortSignalBorrow.wait_fd() returns a file descriptor that becomes readable when the signal is aborted. This is intended for poll(2)/select(2) style waiting (for example to wait on both a TTY fd and an abort signal at the same time). Do not read from or close the returned fd; it is owned by the signal and used as an internal wake mechanism.
  • AbortSignal.wait() blocks the current OS thread until the signal is aborted. In the Supported forms, this is a blocking synchronization primitive. For select-style waiting, use wait_fd() instead.

Using AbortSignalBorrow Across Tasks#

AbortSignal is an owning, droppable handle. To share a signal across tasks without transferring ownership, pass a borrow view:

match (std::abort_controller::AbortController.init()) {
  Ok(controller) => {
    let mut ctl: std::abort_controller::AbortController = controller;
    let sig: std::abort_controller::AbortSignalBorrow = ctl.signal();
    // Pass `sig` into tasks/operations while `ctl` stays alive.
    ctl.destroy();
  },
  Err(_) => {
    // Handle OutOfMemory.
  },
}

task fn worker (sig: std::abort_controller::AbortSignalBorrow) -> int {
  if sig.is_aborted() { return 0; }
  // ...
  return 1;
}

This pattern keeps ownership with the creator while allowing other threads to observe and wait on the abort signal.

If a borrow must remain in use after the initialization branch, keep the owning AbortController alive in an outer binding as well. Do not store only the AbortSignalBorrow, because the borrow becomes invalid once the owner is dropped.

Source repository · Edit this page · View Markdown