How the Handler Pattern Works in bpftime: Shared-Memory Object Management
The handler pattern in bpftime implements a type-safe variant container stored in POSIX shared memory to manage BPF maps, programs, and links across processes, enabling zero-copy access and automatic lifecycle management.
The bpftime runtime uses a sophisticated handler pattern to abstract BPF objects into a unified shared-memory registry. This design allows multiple processes to reference the same underlying eBPF maps and programs through integer file descriptors while maintaining type safety and automatic resource cleanup.
Core Components of the Handler Pattern
The handler_variant Type
At the heart of the system lies handler_variant, a std::variant defined in runtime/src/handler/handler_manager.hpp (lines 84-88). This variant can hold any concrete handler type—such as bpf_map_handler, bpf_prog_handler, or bpf_link_handler—or an empty placeholder called unused_handler. This design makes the container type-agnostic while preserving concrete type information for later dispatch.
Shared-Memory Storage with Boost.Interprocess
The variant instances are stored in a Boost.Interprocess vector named handler_variant_vector, allocated in a POSIX shared memory object called bpftime_maps_shm (or a user-provided name). As defined in runtime/src/handler/handler_manager.hpp (lines 90-94), the container size is fixed at process start based on max_fd_count, ensuring predictable memory layout across all processes.
The handler_manager Registry
The handler_manager class serves as the central registry that owns the vector, allocates free slots, inserts and removes handlers, and forwards lifecycle calls. All public methods are thin wrappers around the variant container. Key methods include find_minimal_unused_idx for slot allocation, set_handler_at_empty_slot for insertion, get_handler for lookup, and clear_id_at for deletion. The implementation resides in runtime/src/handler/handler_manager.cpp.
Concrete Handler Types
bpf_map_handler
The bpf_map_handler class, defined in runtime/src/handler/map_handler.hpp, stores map attributes, a pointer to the underlying map implementation, and a spin-lock. It supports reference counting through map_impl_ptr and map_refcnt_ptr (lines 70-77), allowing multiple handlers to share the same underlying map data structure. When a new handler is created from an existing one, the manager increments the reference count automatically.
bpf_prog_handler
Defined in runtime/src/handler/prog_handler.hpp, this handler stores the program bytecode, name, and optional AOT-compiled image. It encapsulates everything needed to execute or relocate an eBPF program within the userspace runtime.
bpf_link_handler
The bpf_link_handler, found in runtime/src/handler/link_handler.hpp, is a minimal struct that records the relationship between a program file descriptor and its attach target (such as a perf-event or tracepoint). When either side of the link is removed, the manager automatically cleans up the dependent handler.
Handler Lifecycle and Thread Safety
Object Creation and Slot Allocation
When a component such as a bpf_map_create syscall wrapper constructs a concrete handler, it passes the object to handler_manager::set_handler_at_empty_slot. Internally, find_minimal_unused_idx scans the vector for the first unused_handler entry. The index of this entry becomes the integer "fd" that callers use to reference the object.
Insertion and Initialization
The variant vector entry is overwritten via handlers[fd] = std::move(handler). If the handler is a map, map_init is called to allocate the internal data structure within the same shared memory segment, ensuring that all processes see the same physical memory.
Lookup and Dispatch
To access a handler, code calls handler_manager::get_handler(fd), which returns a const reference to the variant. Callers use std::holds_alternative<T> to check the type, then std::get<T> to dispatch to the concrete handler for operations such as map lookups or program execution.
Deletion and Automatic Cleanup
When handler_manager::clear_id_at(fd) is invoked, the manager frees internal resources (such as calling map_free or removing linked perf-events) and replaces the entry with unused_handler. The manager detects dependent handlers—such as links that point to a removed perf-event—and removes them recursively to prevent dangling references.
Concurrency Control
Each map handler owns a pthread_spinlock_t map_lock that guards its internal data. The manager itself does not use a global lock; it relies on the underlying Boost.Interprocess containers, which are safe for concurrent read/write when each process respects the fd-based exclusive ownership model.
Practical Code Examples
Installing a New BPF Map
// Assume `shm` is a boost::interprocess::managed_shared_memory already opened
size_t max_fds = 1024;
bpftime::handler_manager hm(shm, max_fds);
// Build the map attributes (same layout as bpf_map_create syscall)
bpftime::bpf_map_attr attr{};
attr.type = BPF_MAP_TYPE_ARRAY;
attr.key_size = sizeof(uint32_t);
attr.value_size= sizeof(uint64_t);
attr.max_ents = 128;
// Create the concrete handler
bpftime::bpf_map_handler map_h(-1, "my_array", shm, attr);
// Insert it, getting a new fd back
int map_fd = hm.set_handler_at_empty_slot(std::move(map_h), shm);
assert(map_fd >= 0);
Loading an eBPF Program
// Load raw bytecode (insns) from an ELF‑generated array
const ebpf_inst *insns = prog_bytes;
size_t insn_cnt = prog_len / sizeof(ebpf_inst);
// Create program handler; `prog_type` is the kernel enum value (e.g. BPF_PROG_TYPE_SOCKET_FILTER)
bpftime::bpf_prog_handler prog_h(
shm, insns, insn_cnt, "my_prog", BPF_PROG_TYPE_SOCKET_FILTER);
// Register it
int prog_fd = hm.set_handler_at_empty_slot(std::move(prog_h), shm);
Attaching a Program via a Link
bpftime::bpf_link_handler link_h(prog_fd, /*target fd*/ perf_event_fd);
int link_fd = hm.set_handler_at_empty_slot(std::move(link_h), shm);
Looking Up a Handler
const auto &variant = hm.get_handler(map_fd);
if (std::holds_alternative<bpftime::bpf_map_handler>(variant)) {
const auto &map = std::get<bpftime::bpf_map_handler>(variant);
// Example: read a value
uint64_t key = 0;
const void *val = map.map_lookup_elem(&key);
// …
}
Summary
- The handler pattern in bpftime centralizes BPF object management through a type-safe
std::variantcontainer stored in POSIX shared memory. handler_manageracts as the registry, allocating integer file descriptors as indices into a fixed-size Boost.Interprocess vector.- Concrete handlers (
bpf_map_handler,bpf_prog_handler,bpf_link_handler) encapsulate object-specific data and lifecycle methods. - Reference counting allows multiple map handlers to share underlying implementations without data copying.
- Automatic cleanup recursively removes dependent objects (such as links attached to perf-events) when their parent handlers are deleted.
- Thread safety is achieved through per-handler spin-locks rather than global mutexes, enabling high-concurrency access across processes.
Frequently Asked Questions
What is the handler pattern used for in bpftime?
The handler pattern provides a unified abstraction for managing diverse BPF resources—such as maps, programs, links, and perf-events—within a single shared-memory address space. By representing every object as a variant type indexed by an integer file descriptor, bpftime enables multiple processes to access and modify BPF state with zero-copy semantics and consistent lifecycle management.
How does bpftime ensure thread safety when accessing handlers?
Thread safety is implemented through fine-grained locking rather than global mutexes. Each bpf_map_handler instance contains its own pthread_spinlock_t that guards internal data operations. The handler_manager itself relies on the process-safe guarantees of Boost.Interprocess containers and assumes that each file descriptor is accessed exclusively by its owning process, eliminating the need for a global registry lock.
What happens when a BPF map handler is deleted in bpftime?
When handler_manager::clear_id_at(fd) is invoked, the manager first triggers the handler's destructor to free internal resources—such as calling map_free for map data structures. It then scans for dependent handlers (for example, links attached to a perf-event being removed) and recursively deletes them to prevent dangling references. Finally, the slot is marked as unused_handler, making the file descriptor available for reuse.
How does the handler pattern enable cross-process sharing?
The handler pattern enables cross-process sharing by storing all handler variants in a POSIX shared memory segment named bpftime_maps_shm using Boost.Interprocess vectors. Because the shared memory is mapped into each process's address space, the integer file descriptor returned by set_handler_at_empty_slot refers to the same physical memory location in every attached process. This design allows one process to create a map and another to look it up and access its data without kernel syscalls or data copying.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →