# How bpftime Implements Event Attachment Mechanisms: A Modular Plugin Architecture

> Discover how bpftime implements event attachment via a modular plugin architecture. Explore its flexible design for Frida uprobes, syscall tracing, and custom triggers.

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

---

**bpftime implements event attachment through a modular plugin architecture centered around abstract base classes, polymorphic private data containers, and a central manager that maps attach types to concrete implementations like Frida uprobes, syscall tracing, and custom triggers.**

The bpftime project (`eunomia-bpf/bpftime`) is a userspace eBPF runtime designed to execute eBPF programs outside the kernel context. Its **bpftime event attachment mechanisms** employ a highly extensible plugin system that enables uniform handling of diverse event sources—from uprobes and syscall traces to CUDA GPU kernels—without requiring modifications to the core runtime libraries.

## Core Architecture Components

### The Base Attach Implementation Interface

At the heart of the system lies `attach::base_attach_impl`, defined in [`attach/base_attach_impl/base_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/base_attach_impl/base_attach_impl.hpp). This abstract base class establishes the strict contract that every attach implementation must fulfill, including pure virtual methods for creation, detachment, and custom helper registration. By inheriting from this interface, disparate event sources such as kernel probes and user-space instrumentation can be managed through a unified API.

### Polymorphic Private Data Containers

The `attach::attach_private_data` class, found in [`attach/base_attach_impl/attach_private_data.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/base_attach_impl/attach_private_data.hpp), serves as a type-erased container for attach-specific initialization data. Each concrete implementation provides a factory lambda that constructs specialized private data from string descriptions, enabling runtime configuration of parameters like target function addresses or syscall numbers. This polymorphism allows `bpf_attach_ctx` to handle type-specific data without knowing the underlying implementation details.

### The Central Attachment Context Manager

The `bpf_attach_ctx` class in [`runtime/include/bpf_attach_ctx.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/runtime/include/bpf_attach_ctx.hpp) orchestrates all attachment activity. It maintains registries mapping attach-type IDs (such as `ATTACH_UPROBE` or `ATTACH_SYSCALL_TRACE`) to implementation objects, and it stores instantiated links between compiled eBPF programs and their event sources. The `register_attach_impl` method enables dynamic plugin registration at startup, while `create_attach_with_ebpf_callback` establishes the connection between an eBPF program's execution callback and the event source.

## The Event Attachment Lifecycle

### Registration

During system initialization, `bpf_attach_ctx::register_attach_impl` is invoked for each available implementation. This method binds a list of attach-type identifiers to a `std::unique_ptr` owning the concrete implementation, and associates a factory function for building `attach_private_data` objects from configuration strings. This registration phase populates the internal maps that the context manager uses to route attachment requests.

### Handler Instantiation

When creating a new attachment, the runtime calls `get_attach_impl_by_attach_type` to retrieve the appropriate implementation. It then constructs the private data using the registered factory and invokes `impl->create_attach_with_ebpf_callback`, passing the compiled eBPF program's `ebpf_run_callback`. This callback encapsulates the JIT-compiled or interpreted eBPF bytecode ready for execution.

### Event Triggering

Concrete implementations receive events from their respective sources—whether Frida's `GumInterceptor` for uprobes, a syscall trampoline for system call tracing, or manual calls to `simple_attach_impl::trigger`. They forward event arguments to the stored `ebpf_run_callback`, which executes the eBPF program. The callback's return value can be consumed by the attach implementation to perform actions such as `bpftime_set_retval` or `bpftime_override_return`.

### Detachment

Links are destroyed through `bpf_attach_ctx::destroy_instantiated_attach_link`, which delegates to the implementation's `detach_by_id` method. For Frida-based attaches, this removes the interceptor and restores original function prologues; for syscall tracing, it unregisters the callback from the per-syscall dispatch table. This mechanism ensures clean removal without affecting other active instrumentation.

## Built-in Implementation Types

### Simple Attach for Custom Triggers

The simple attach implementation in [`attach/simple_attach_impl/simple_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/simple_attach_impl/simple_attach_impl.hpp) provides a lightweight wrapper for user-defined triggers. It accepts string-based trigger arguments and callbacks, making it ideal for runtime-generated events, custom performance monitoring, or "hand-crafted" events inside the runtime that do not require external instrumentation frameworks.

### Syscall-Trace Attach

Implemented in [`attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp), this module hooks process syscall entry and exit points by replacing the original syscall dispatcher with a trampoline. It maintains per-syscall callback sets via structures like `syscall_trace_attach_impl` and forwards CPU register states and arguments to attached eBPF programs through its `dispatch_syscall` method.

### Frida Uprobe Attach

The Frida-based implementation in [`attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp) leverages Frida's `GumInterceptor` to instrument userspace functions. It supports four distinct attach types: **uprobe**, **uretprobe**, **override**, and **replace**. The implementation can invoke plain C++ lambdas or forward calls to eBPF programs via `attach_at_with_ebpf_callback` and `call_attach_specific_function`.

### CUDA GPU Attach

For heterogeneous computing environments, the optional CUDA attach implementation in [`attach/nv_attach_impl/nv_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/nv_attach_impl/nv_attach_impl.hpp) enables eBPF programs to trigger from GPU kernels. It establishes a shared-memory IPC channel between the CUDA device code and the bpftime runtime, extending observability into GPU-accelerated workloads without kernel driver modifications.

## Code Examples

### Registering a Simple Attach Implementation

```cpp
#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/simple_attach_impl/simple_attach_impl.hpp"

using namespace bpftime;

// Create the global attach context
bpf_attach_ctx ctx;

// Register a simple attach that reacts to a string trigger
ctx.register_attach_impl(
    { ATTACH_SIMPLE },                                   // ATTACH_SIMPLE is a user‑defined integer
    std::make_unique<simple_attach::simple_attach_impl>(
        // callback receives (attach_arg, trigger_arg, ebpf_cb)
        [](const std::string& attach_arg,
           const std::string& trigger_arg,
           const attach::ebpf_run_callback& ebpf_cb) -> int {
            // Forward the trigger argument to the eBPF program
            uint64_t ret = 0;
            int rc = ebpf_cb(nullptr, 0, &ret);
            // Do something with ret …
            return rc;
        },
        ATTACH_SIMPLE),
    // Factory that builds private data from a string (here just stores the string)
    [](const std::string_view& sv, int& err) -> std::unique_ptr<attach::attach_private_data> {
        struct simple_attach::simple_attach_private_data : attach::attach_private_data {
            std::string data;
            int initialize_from_string(const std::string_view& sv) override {
                data = std::string(sv);
                return 0;
            }
        };
        err = 0;
        return std::make_unique<simple_attach_private_data>();
    });

```

*Source reference*: This registration pattern is defined in `bpf_attach_ctx::register_attach_impl` within [`runtime/include/bpf_attach_ctx.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/runtime/include/bpf_attach_ctx.hpp).

### Attaching a Uprobe with Frida

```cpp
#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp"

bpftime::bpf_attach_ctx ctx;

// Assume the eBPF program has already been compiled and loaded,
// producing an `ebpf_run_callback` named `prog_cb`.
bpftime::attach::ebpf_run_callback prog_cb = /* ... */;

// Register the Frida implementation (done once at start‑up)
ctx.register_attach_impl(
    { bpftime::attach::ATTACH_UPROBE, bpftime::attach::ATTACH_URETPROBE },
    std::make_unique<bpftime::attach::frida_attach_impl>(),
    [](const std::string_view&, int& err) -> std::unique_ptr<bpftime::attach::attach_private_data> {
        // Frida doesn’t need extra data for the generic case
        err = 0;
        return nullptr;
    });

// Later, when creating the actual link:
int link_id = ctx.create_attach_with_ebpf_callback(
    prog_cb,
    /* private data for the specific function */ 
    []() -> std::unique_ptr<bpftime::attach::attach_private_data> {
        struct frida_uprobe_private_data : bpftime::attach::attach_private_data {
            void* func_addr = nullptr;
            int initialize_from_string(const std::string_view& sv) override {
                std::stringstream ss(std::string(sv));
                ss >> func_addr;          // simple example: "0x7f1234"
                return 0;
            }
        };
        return std::make_unique<frida_uprobe_private_data>();
    }(),
    bpftime::attach::ATTACH_UPROBE);

```

*Source reference*: The Frida implementation details are found in [`attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp), specifically within `attach_at_with_ebpf_callback` and `call_attach_specific_function`.

### Tracing System Calls

```cpp
#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp"

bpftime::bpf_attach_ctx ctx;

// Register the syscall‑trace impl (usually done by the runtime)
ctx.register_attach_impl(
    { bpftime::attach::ATTACH_SYSCALL_TRACE },
    std::make_unique<bpftime::attach::syscall_trace_attach_impl>(),
    [](const std::string_view& sv, int& err) -> std::unique_ptr<bpftime::attach::attach_private_data> {
        struct syscall_private_data : bpftime::attach::attach_private_data {
            int sys_nr = -1;
            bool is_enter = true;
            int initialize_from_string(const std::string_view& sv) override {
                // format: "enter:60" or "exit:60"
                std::string s(sv);
                is_enter = s.rfind("enter:", 0) == 0;
                sys_nr = std::stoi(s.substr(is_enter ? 6 : 5));
                return 0;
            }
        };
        err = 0;
        return std::make_unique<syscall_private_data>();
    });

// Create a link that prints arguments of `openat` (syscall 257) on entry
bpftime::attach::ebpf_run_callback ebpf_cb = /* compiled ebpf program */;
int link_id = ctx.create_attach_with_ebpf_callback(
    ebpf_cb,
    /* private data built from "enter:257" */
    [](){ 
        auto d = std::make_unique<bpftime::attach::syscall_private_data>();
        d->initialize_from_string("enter:257");
        return d;
    }(),
    bpftime::attach::ATTACH_SYSCALL_TRACE);

```

*Source reference*: The syscall dispatch and attachment logic resides in [`attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp).

## Extending bpftime with Custom Attach Types

Adding new event sources to bpftime requires only three steps without modifying the core runtime. First, create a subclass of `base_attach_impl` that overrides the pure virtual methods for creation and detachment. Second, derive from `attach_private_data` to store your attachment-specific configuration. Third, register your implementation with `bpf_attach_ctx::register_attach_impl` during startup. This design enables third-party developers to integrate proprietary instrumentation methods—such as hardware PMU counters or custom hypercall interfaces—while leveraging bpftime's existing eBPF program loading and memory management infrastructure.

## Summary

- bpftime uses a **plugin architecture** centered on `base_attach_impl`, `attach_private_data`, and `bpf_attach_ctx` to support multiple event types uniformly.
- The attachment lifecycle involves **registration** via `register_attach_impl`, **instantiation** through `create_attach_with_ebpf_callback`, **event forwarding** to `ebpf_run_callback`, and **cleanup** via `detach_by_id`.
- Built-in implementations cover user-space function tracing via **Frida** ([`frida_uprobe_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/frida_uprobe_attach_impl.hpp)), **syscall interception** via trampolines ([`syscall_trace_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/syscall_trace_attach_impl.hpp)), lightweight **string triggers** ([`simple_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/simple_attach_impl.hpp)), and optional **CUDA GPU** events ([`nv_attach_impl.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/nv_attach_impl.hpp)).
- Custom attach types can be added without rebuilding the core runtime by implementing the abstract base classes and registering them with the central context manager.

## Frequently Asked Questions

### What is the role of `bpf_attach_ctx` in bpftime?

`bpf_attach_ctx` acts as the central coordinator for all event attachments in the bpftime runtime. It maintains registries that map attach-type identifiers to concrete implementation objects, manages the lifecycle of instantiated links between eBPF programs and event sources, and provides the `register_attach_impl` API that enables modular extension of the system.

### How does bpftime support different attachment types like uprobes and syscalls?

bpftime uses a polymorphic plugin system where each attachment type implements the `base_attach_impl` interface and provides a corresponding `attach_private_data` factory. The runtime registers these implementations at startup with `register_attach_impl`, allowing the same `bpf_attach_ctx` API to handle Frida-based uprobe instrumentation, syscall tracing via trampolines, and custom triggers uniformly through virtual dispatch.

### Can I implement custom event sources in bpftime?

Yes. You can create custom attach types by subclassing `attach::base_attach_impl` to implement attachment logic and `attach::attach_private_data` for configuration storage. After implementing the required methods such as `create_attach_with_ebpf_callback` and `detach_by_id`, register your module with `bpf_attach_ctx::register_attach_impl`. This requires no changes to the core bpftime runtime, making the system highly extensible for proprietary instrumentation needs.

### How does bpftime handle the eBPF program callback during event triggering?

When an event fires, the concrete attach implementation invokes the `ebpf_run_callback` function pointer stored during link creation. This callback executes the compiled eBPF program with the event context and arguments, and returns a status code that the attach implementation can use to perform actions like modifying return values via `bpftime_override_return` or logging data to maps.