# How the Handler Pattern Works in bpftime: Shared-Memory Object Management

> Discover how the handler pattern in bpftime uses type-safe shared memory to manage BPF maps, programs, and links across processes. Learn about zero-copy access and automatic lifecycle management.

- Repository: [eunomia-bpf/bpftime](https://github.com/eunomia-bpf/bpftime)
- Tags: internals
- Published: 2026-03-01

---

**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`](https://github.com/eunomia-bpf/bpftime/blob/main/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`](https://github.com/eunomia-bpf/bpftime/blob/main/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`](https://github.com/eunomia-bpf/bpftime/blob/main/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`](https://github.com/eunomia-bpf/bpftime/blob/main/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`](https://github.com/eunomia-bpf/bpftime/blob/main/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`](https://github.com/eunomia-bpf/bpftime/blob/main/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

```cpp
// 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

```cpp
// 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

```cpp
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

```cpp
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::variant` container stored in POSIX shared memory.
- **`handler_manager`** acts 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.