Implementation Details
Relevant source files
This document provides a deep technical analysis of the HandlerTable<N> implementation, covering the internal architecture, atomic operations strategy, and lock-free design patterns. It explains how function pointers are stored atomically, the memory ordering guarantees, and the safety mechanisms that enable concurrent access without locks.
For high-level usage information, see User Guide. For atomic operations theory, see Atomic Operations.
Core Data Structure
The HandlerTable<N> struct implements a fixed-size, lock-free event handler registry using a compile-time constant generic parameter N to define the maximum number of event handlers.
HandlerTable Structure
flowchart TD
subgraph subGraph2["Value Interpretation"]
Zero["0 = Empty Slot"]
NonZero["Non-zero = Function Pointer"]
Transmute["unsafe transmute"]
end
subgraph subGraph1["Handler Storage"]
H0["handlers[0]: AtomicUsize"]
H1["handlers[1]: AtomicUsize"]
HN["handlers[N-1]: AtomicUsize"]
end
subgraph HandlerTable<N>["HandlerTable"]
HT["HandlerTable"]
Array["handlers: [AtomicUsize; N]"]
end
Array --> H0
Array --> H1
Array --> HN
H0 --> Zero
H1 --> NonZero
HN --> Zero
HT --> Array
NonZero --> Transmute
The fundamental design stores function pointers as usize values within AtomicUsize containers. An empty slot is represented by the value 0, while registered handlers store the function pointer cast to usize. This design enables atomic operations on what are essentially function pointer references.
Sources: src/lib.rs(L14 - L16) src/lib.rs(L21 - L23)
Initialization Pattern
The constructor uses a const-evaluated array initialization pattern that ensures all atomic slots begin in the empty state:
flowchart TD
subgraph subGraph1["Memory Layout"]
Slot0["AtomicUsize(0)"]
Slot1["AtomicUsize(0)"]
SlotN["AtomicUsize(0)"]
end
subgraph subGraph0["new() Construction"]
New["HandlerTable::new()"]
ConstExpr["const { AtomicUsize::new(0) }"]
ArrayInit["[...; N]"]
end
ArrayInit --> Slot0
ArrayInit --> Slot1
ArrayInit --> SlotN
ConstExpr --> ArrayInit
New --> ConstExpr
The const { AtomicUsize::new(0) } expression leverages Rust's const evaluation to initialize each array element at compile time, ensuring zero-cost initialization and immediate readiness for concurrent access.
Sources: src/lib.rs(L19 - L24)
Atomic Operations Strategy
The implementation employs three distinct atomic operations, each optimized for its specific use case within the lock-free protocol.
Operation Mapping
| Method | Atomic Operation | Success Condition | Failure Behavior |
|---|---|---|---|
| register_handler | compare_exchange | Slot was empty (0) | Returns false |
| unregister_handler | swap | Always succeeds | Returns None if empty |
| handle | load | Always succeeds | Returns false if empty |
Registration Protocol
sequenceDiagram
participant Client as Client
participant HandlerTable as HandlerTable
participant AtomicSlothandlersidx as AtomicSlot["handlers[idx]"]
participant AtomicSlot as AtomicSlot
Client ->> HandlerTable: "register_handler(idx, handler)"
HandlerTable ->> HandlerTable: "Check idx < N"
HandlerTable ->> AtomicSlot: "compare_exchange(0, handler as usize, Acquire, Relaxed)"
alt "Slot was empty"
AtomicSlot -->> HandlerTable: "Ok(0)"
HandlerTable -->> Client: "true"
else "Slot occupied"
AtomicSlot -->> HandlerTable: "Err(current_value)"
HandlerTable -->> Client: "false"
end
The compare_exchange operation provides atomic test-and-set functionality, ensuring that only one thread can successfully register a handler for any given index. The operation uses Ordering::Acquire for success to establish happens-before relationships with subsequent operations.
Sources: src/lib.rs(L30 - L37)
Deregistration Protocol
The swap operation unconditionally exchanges the current value with 0, returning the previous value. This ensures atomic removal regardless of the current state:
flowchart TD
subgraph subGraph0["unregister_handler Flow"]
Input["unregister_handler(idx)"]
BoundsCheck["idx < N ?"]
SwapOp["swap(0, Ordering::Acquire)"]
ReturnNone["None"]
CheckResult["handler != 0 ?"]
Transmute["unsafe transmute"]
ReturnSome["Some(handler)"]
end
BoundsCheck --> ReturnNone
BoundsCheck --> SwapOp
CheckResult --> ReturnNone
CheckResult --> Transmute
Input --> BoundsCheck
SwapOp --> CheckResult
Transmute --> ReturnSome
Sources: src/lib.rs(L42 - L52)
Function Pointer Storage and Safety
The core challenge in this implementation is safely storing and retrieving function pointers through atomic operations. Since AtomicPtr<fn()> doesn't exist in stable Rust, the implementation uses AtomicUsize with unsafe transmutation.
Pointer-to-Integer Conversion
flowchart TD
subgraph subGraph1["Safety Invariants"]
NonNull["Non-null Function Pointers"]
ValidCode["Valid Code Addresses"]
SameLifetime["Pointer Lifetime ≥ Table Lifetime"]
end
subgraph subGraph0["Storage Mechanism"]
FnPtr["fn() - Function Pointer"]
AsUsize["handler as usize"]
AtomicStore["AtomicUsize::store/load"]
TransmuteBack["transmute"]
CallHandler["handler()"]
end
AsUsize --> AtomicStore
AtomicStore --> TransmuteBack
FnPtr --> AsUsize
FnPtr --> NonNull
FnPtr --> SameLifetime
FnPtr --> ValidCode
TransmuteBack --> CallHandler
The implementation relies on several critical safety assumptions:
- Function pointers are never null (guaranteed by Rust's type system)
- Function pointers have the same size as
usizeon the target platform - Function addresses remain valid for the lifetime of the
HandlerTable - The transmutation between
usizeandfn()preserves the pointer value
Sources: src/lib.rs(L48) src/lib.rs(L64)
Event Handling Execution
The handle method demonstrates the complete pointer recovery and execution sequence:
flowchart TD
subgraph subGraph0["handle Method Flow"]
LoadPtr["load(Ordering::Acquire)"]
CheckZero["handler == 0 ?"]
Transmute["unsafe transmute(handler)"]
ReturnFalse["return false"]
Execute["handler()"]
ReturnTrue["return true"]
end
CheckZero --> ReturnFalse
CheckZero --> Transmute
Execute --> ReturnTrue
LoadPtr --> CheckZero
Transmute --> Execute
The atomic load with Ordering::Acquire ensures that any writes to memory performed by the registering thread before the compare_exchange are visible to the handling thread.
Sources: src/lib.rs(L58 - L70)
Memory Ordering Guarantees
The implementation uses a carefully chosen set of memory orderings to balance performance with correctness guarantees.
Ordering Strategy
| Operation | Success Ordering | Failure Ordering | Rationale |
|---|---|---|---|
| compare_exchange | Acquire | Relaxed | Synchronize with prior writes |
| swap | Acquire | N/A | Ensure visibility of handler execution |
| load | Acquire | N/A | See registration writes |
Happens-Before Relationships
flowchart TD
subgraph subGraph1["Thread B - Execution"]
HandleCall["handle(idx)"]
LoadHandler["load(Ordering::Acquire)"]
ExecuteHandler["handler()"]
end
subgraph subGraph0["Thread A - Registration"]
SetupHandler["Setup handler environment"]
RegisterCall["register_handler(idx, fn)"]
CompareExchange["compare_exchange(..., Acquire, Relaxed)"]
end
subgraph subGraph2["Memory Ordering Chain"]
HappensBefore["Happens-Before Edge"]
end
CompareExchange --> LoadHandler
LoadHandler --> ExecuteHandler
RegisterCall --> CompareExchange
SetupHandler --> RegisterCall
The Acquire ordering on successful registration creates a happens-before relationship with the Acquire load during event handling, ensuring that any memory operations completed before registration are visible during handler execution.
Sources: src/lib.rs(L35) src/lib.rs(L46) src/lib.rs(L62)
Lock-free Design Patterns
The HandlerTable implementation demonstrates several fundamental lock-free programming patterns adapted for event handling scenarios.
Compare-and-Swap Pattern
The registration mechanism implements the classic compare-and-swap pattern for atomic updates:
flowchart TD
subgraph subGraph0["CAS Loop Pattern"]
ReadCurrent["Read Current Value"]
PrepareNew["Prepare New Value"]
CASAttempt["Compare-and-Swap"]
Success["Success"]
Retry["Retry"]
end
CASAttempt --> Retry
CASAttempt --> Success
PrepareNew --> CASAttempt
ReadCurrent --> PrepareNew
Retry --> ReadCurrent
However, the handler table simplifies this pattern by only allowing transitions from empty (0) to occupied (non-zero), eliminating the need for retry loops.
ABA Problem Avoidance
The design inherently avoids the ABA problem through its state transition model:
| From State | To State | Operation | ABA Risk |
|---|---|---|---|
| Empty (0) | Handler (ptr) | register_handler | None - single transition |
| Handler (ptr) | Empty (0) | unregister_handler | None - swap is atomic |
| Any | Any | handle | None - read-only |
The absence of complex state transitions and the use of swap for deregistration eliminate scenarios where the ABA problem could manifest.
Sources: src/lib.rs(L30 - L37) src/lib.rs(L42 - L52) src/lib.rs(L58 - L70)