How to Add New Event Sources to bpftime: A Complete Developer's Guide

To add new event sources to bpftime, you must implement a concrete subclass of base_attach_impl, define a unique attach-type identifier, create a private-data class for per-instance configuration, and register the implementation with bpf_attach_ctx.

The bpftime runtime provides an extensible attach framework that allows developers to integrate custom event sources—ranging from kernel tracepoints to hardware-specific signals—while maintaining the project's zero-copy shared-memory architecture. This guide walks through the core architecture, implementation steps, and registration process required to extend bpftime with new event sources.

Understanding the bpftime Attach Architecture

The attach system in bpftime is organized into three distinct layers that separate event capture mechanisms from program lifecycle management.

Layer 1: Attach Implementations

Concrete event sources inherit from bpftime::attach::base_attach_impl defined in attach/base_attach_impl/base_attach_impl.hpp. This abstract base class mandates three core operations: creating an attach link, detaching by ID, and optionally registering custom BPF helpers.

Layer 2: Attach Manager (bpf_attach_ctx)

The manager class defined in runtime/include/bpf_attach_ctx.hpp maintains a registry of all available attach implementations. It handles instantiation of links between eBPF programs and event sources through methods like register_attach_impl and instantiate_attach.

Layer 3: Private Data Configuration

Each event source requires per-instance configuration stored in classes deriving from bpftime::attach::attach_private_data. These objects parse user-provided strings (such as PID numbers or memory addresses) through the initialize_from_string method.

Step-by-Step Guide to Adding a New Event Source

Follow this checklist to implement and register a custom event source in the bpftime runtime.

1. Define the Attach Type ID

Select a unique integer identifier not used by existing implementations. Standard bpftime sources use values like ATTACH_SYSCALL_TRACE = 2 or ATTACH_FRIDA_UPROBE. Define your constant in a public header:

// attach/my_source_impl/my_source_impl.hpp
constexpr size_t ATTACH_MY_SOURCE = 1000;

2. Create the Private Data Class

Derive from attach_private_data to store configuration specific to your event source. Implement the string parsing constructor:

// attach/my_source_impl/my_source_impl.hpp
#include "attach_private_data.hpp"
#include <string>

struct my_source_private_data : public bpftime::attach::attach_private_data {
    int target_pid = -1;
    std::string custom_param;
    
    int initialize_from_string(const std::string_view &sv) override {
        // Parse configuration string (e.g., "1234:debug")
        auto sep = sv.find(':');
        target_pid = std::stoi(std::string(sv.substr(0, sep)));
        if (sep != std::string_view::npos) {
            custom_param = sv.substr(sep + 1);
        }
        return 0;
    }
};

3. Implement the Base Attach Class

Create a concrete implementation of base_attach_impl in attach/my_source_impl/my_source_attach_impl.hpp. You must override the three pure virtual methods:

#include "base_attach_impl.hpp"
#include "my_source_impl.hpp"
#include <unordered_map>
#include <memory>

class my_source_attach_impl final : public bpftime::attach::base_attach_impl {
public:
    int create_attach_with_ebpf_callback(
        ebpf_run_callback &&cb,
        const bpftime::attach::attach_private_data &private_data,
        int attach_type) override
    {
        const auto &pd = static_cast<const my_source_private_data&>(private_data);
        int id = allocate_id();
        
        auto entry = std::make_unique<link_entry>();
        entry->id = id;
        entry->cb = std::move(cb);
        entry->pid = pd.target_pid;
        
        // Hook into your event source here (e.g., register callback with library)
        register_native_callback(pd.target_pid, id);
        
        links.emplace(id, std::move(entry));
        return id;
    }

    int detach_by_id(int id) override {
        auto it = links.find(id);
        if (it == links.end()) return -1;
        
        // Clean up OS resources
        unregister_native_callback(it->second->pid);
        links.erase(it);
        return 0;
    }

    void register_custom_helpers(ebpf_helper_register_callback reg) override {
        // Optional: register custom BPF helpers specific to this source
        reg(5000, "my_source_get_pid", (void*)+[](unsigned long long *ctx){
            // Implementation here
            return 0;
        });
    }

private:
    struct link_entry {
        int id;
        ebpf_run_callback cb;
        int pid;
    };
    
    std::unordered_map<int, std::unique_ptr<link_entry>> links;
    
    void register_native_callback(int pid, int link_id) {
        // Implementation-specific: hook into kernel, library, or hardware
    }
    
    void unregister_native_callback(int pid) {
        // Cleanup implementation
    }
    
    int allocate_id() { return next_id++; }
    int next_id = 1;
};

4. Register with the Attach Manager

During runtime initialization, register your implementation with bpf_attach_ctx. This typically occurs in main.cpp or a dedicated initialization function:

#include "bpf_attach_ctx.hpp"
#include "attach/my_source_impl/my_source_attach_impl.hpp"

void initialize_custom_source(bpftime::bpf_attach_ctx &ctx) {
    auto impl = std::make_unique<my_source_attach_impl>();
    
    ctx.register_attach_impl(
        { ATTACH_MY_SOURCE },  // Attach type IDs this implementation handles
        std::move(impl),
        [](const std::string_view &sv, int &err) 
            -> std::unique_ptr<bpftime::attach::attach_private_data> {
            auto pd = std::make_unique<my_source_private_data>();
            err = pd->initialize_from_string(sv);
            if (err != 0) return nullptr;
            return pd;
        }
    );
}

5. Integrate with the Event Source

The final step involves connecting your implementation to the actual event generation mechanism. Depending on your source type, this may involve:

  • Opening a perf_event_open fd for kernel tracepoints
  • Using ptrace or process_vm_writev for user-space probes
  • Registering callbacks with third-party libraries (similar to frida_uprobe_attach_impl)
  • Polling hardware performance counters

When the native event fires, your implementation must retrieve the stored callback using the link ID and invoke it:

void on_native_event_fires(int link_id, void *ctx) {
    auto it = links.find(link_id);
    if (it != links.end()) {
        // Prepare memory buffer with event context
        it->second->cb(memory_buffer, buffer_size, &return_value);
    }
}

Reference Implementations in the Source Tree

Study these existing implementations to understand specific patterns for different event types:

Summary

  • bpftime extends its event capabilities through a pluggable attach framework centered on base_attach_impl.
  • To add new event sources to bpftime, implement three core components: a unique attach-type constant, a private-data class for configuration, and a concrete attach implementation.
  • The bpf_attach_ctx manager handles registration through register_attach_impl, which accepts the implementation instance, supported attach types, and a factory function for parsing private data.
  • Event sources must implement create_attach_with_ebpf_callback to store the eBPF runner and detach_by_id to clean up resources.
  • Optional methods register_custom_helpers and call_attach_specific_function enable extended functionality for specialized event sources.

Frequently Asked Questions

What is the minimum code required to add a new event source?

At minimum, you need a class inheriting from bpftime::attach::base_attach_impl that overrides create_attach_with_ebpf_callback and detach_by_id, plus a private-data class derived from attach_private_data. You must also call bpf_attach_ctx::register_attach_impl during initialization to register your attach type ID and factory function.

How do I handle custom BPF helpers for my event source?

Override the register_custom_helpers method in your base_attach_impl subclass. This method receives a callback function that you invoke with the helper index, name, and function pointer. For example, the CUDA implementation in nv_attach_impl.hpp registers multiple GPU-specific helpers using this mechanism to expose hardware metrics to eBPF programs.

Can I add multiple attach types in a single implementation?

Yes. The register_attach_impl method accepts an std::initializer_list<int> for attach types, allowing one implementation class to handle multiple related event types. This is useful when different event types share the same underlying mechanism but require different configuration parameters or trigger conditions.

How do I test my new event source implementation?

Place unit tests in runtime/test/ or attach/test/. Verify that create_attach_with_ebpf_callback correctly stores the callback and returns a valid ID, that the native event trigger invokes the stored callback with proper memory context, and that detach_by_id successfully removes the link and releases OS resources. Integration tests should validate the full lifecycle through the bpf_attach_ctx API.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →