# How bpftime Handles eBPF Virtual Machines: A Plugin Architecture Deep Dive

> Discover how bpftime handles eBPF virtual machines using a plugin architecture and a unified C API. Learn about its support for interpretation, JIT, and future AOT extensions.

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

---

**bpftime abstracts concrete eBPF virtual machine implementations behind a unified C API, delegating execution to pluggable backends like uBPF through an opaque compatibility layer that supports interpretation, JIT compilation, and future AOT extensions.**

The bpftime project provides a userspace runtime for eBPF programs that decouples the execution engine from calling code. By handling eBPF virtual machines through a modular architecture, bpftime allows developers to swap between interpreters and JIT compilers without modifying their application logic, maintaining compatibility with the standard libbpf interface.

## The Unified C API Facade

The public interface resides in [`vm/vm-core/include/ebpf-vm.h`](https://github.com/eunomia-bpf/bpftime/blob/main/vm/vm-core/include/ebpf-vm.h), which declares an opaque `struct ebpf_vm` and a set of lifecycle functions (`ebpf_create`, `ebpf_load`, `ebpf_exec`, `ebpf_compile`, etc.) that mirror the kernel's libbpf API. Internally, this struct wraps a polymorphic backend instance:

```cpp
struct ebpf_vm {
    std::string vm_name;                                   // backend identifier (e.g., "ubpf")
    std::unique_ptr<bpftime::vm::compat::bpftime_vm_impl> vm_instance;
};

```

When a client calls `ebpf_create("ubpf")`, the implementation in [`vm/vm-core/src/ebpf-vm.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/vm/vm-core/src/ebpf-vm.cpp) forwards the request to a factory that instantiates the concrete backend:

```cpp
extern "C" ebpf_vm *ebpf_create(const char *vm_name_str)
{
    auto vm = new ebpf_vm;
    vm->vm_instance = bpftime::vm::compat::create_vm_instance(std::string(vm_name_str));
    return vm;
}

```

All subsequent operations on the VM—loading bytecode, registering helpers, executing programs, or JIT-compiling—are delegated through the `vm_instance` pointer to the specific backend implementation.

## The Compatibility Layer Interface

The abstraction is defined in [`vm/compat/include/bpftime_vm_compat.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/vm/compat/include/bpftime_vm_compat.hpp) via the abstract base class `bpftime_vm_impl`. This interface captures the complete VM lifecycle through pure virtual methods that every backend must implement:

- **`load_code(const void *code, size_t len)`** – Ingest raw eBPF bytecode and prepare it for execution
- **`exec(void *mem, size_t mem_len, uint64_t &ret)`** – Interpret the loaded program against provided memory
- **`compile()`** – JIT-compile the program to native machine code and return a function pointer
- **`unload_code()`** – Release the current program and reset the VM state
- **`set_lddw_helpers(...)`** – Bind helper functions invoked by the `lddw` pseudo-instruction
- **`set_unwind_function_index(size_t)`** – Configure tail-call-like unwind semantics
- **`set_pointer_secret(uint64_t)`** – Set an optional ROP-hardening secret for JIT-compiled code

The header also maintains a factory map (`std::map<std::string, create_vm_instance_func>`) that associates string names with constructor functions. Backends register themselves using `register_vm_factory("name", create_function)`, enabling runtime selection via `ebpf_create("name")`.

## The uBPF Backend Implementation

The reference backend implementation lives in [`vm/compat/ubpf-vm/compat_ubpf.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/vm/compat/ubpf-vm/compat_ubpf.cpp). It bridges bpftime's generic interface to the upstream uBPF library through the `bpftime_ubpf_vm` class.

**Factory Registration** occurs via a constructor that executes before `main()`:

```cpp
register_vm_factory("ubpf", create_ubpf_vm_instance);

```

**Concrete Implementation Details:**

The `bpftime_ubpf_vm` class inherits from `bpftime_vm_impl` and owns a `ubpf_vm*` handle from the underlying library. When `ebpf_load` is called, the backend:

1. Translates the raw byte buffer into a vector of `ebpf_inst` structures
2. Patches CALL instructions to resolve helper indices and LDDW instructions to bind map/variable helpers
3. Invokes `ubpf_load` to ingest the prepared bytecode

Execution modes are thin wrappers around uBPF primitives:

- **`exec`** delegates to `ubpf_exec` for interpretation
- **`compile`** calls `ubpf_compile` and returns a `precompiled_ebpf_function` pointer
- **Helper registration** stores function pointers for the patching logic used during loading
- **Unwind and security** settings forward to `ubpf_set_unwind_function_index` and `ubpf_set_pointer_secret`

All errors encountered during these operations are captured in `error_string` and returned to the caller through the C API's error output parameters.

## Extending the Architecture with New Backends

While the current repository ships only the uBPF implementation, the architecture anticipates multiple execution engines. Adding a new backend (such as an LLVM-based JIT or AOT compiler) requires:

1. Implementing `bpftime_vm_impl` with backend-specific logic for loading and execution
2. Registering the factory with `register_vm_factory("backend_name", create_instance)`
3. Users instantiating it via `ebpf_create("backend_name")`

The base class also declares optional AOT extension points—`do_aot_compile`, `load_aot_object`, and `generate_ptx`—that future backends can override to support ahead-of-time compilation workflows without changing the public C API.

## Complete VM Lifecycle Example

The following demonstrates the full lifecycle of a bpftime eBPF virtual machine, from creation to cleanup:

```c
#include <bpftime.hpp>   // Re-exports the C API

int main()
{
    // 1. Create a uBPF VM instance
    struct ebpf_vm *vm = ebpf_create("ubpf");

    // 2. Register a helper function (e.g., bpf_trace_printk equivalent)
    ebpf_register(vm, 5, "bpf_trace_printk", (void *)my_printk);

    // 3. Load raw eBPF bytecode
    char *err = NULL;
    if (ebpf_load(vm, prog_bytes, prog_len, &err) < 0) {
        fprintf(stderr, "Load error: %s\n", err);
        free(err);
        return 1;
    }

    // 4. Execute in interpreter mode
    uint64_t ret;
    if (ebpf_exec(vm, mem_buf, mem_sz, &ret) != 0)
        fprintf(stderr, "Execution failed\n");

    // 5. JIT-compile and execute directly (optional)
    char *jit_err = NULL;
    ebpf_jit_fn fn = ebpf_compile(vm, &jit_err);
    if (fn) {
        uint64_t jit_ret = fn(mem_buf, mem_sz);
        printf("JIT return value: %llu\n", (unsigned long long)jit_ret);
    } else {
        fprintf(stderr, "JIT compile error: %s\n", jit_err);
        free(jit_err);
    }

    // 6. Release resources
    ebpf_destroy(vm);
    return 0;
}

```

## Summary

- **bpftime virtualizes eBPF VMs** through an opaque `struct ebpf_vm` that hides backend-specific details behind a stable C API.
- **The compatibility layer** (`bpftime_vm_impl`) defines pure virtual methods for loading, executing, and compiling bytecode, enabling plug-and-play execution engines.
- **Factory registration** allows runtime selection of backends via string identifiers passed to `ebpf_create()`.
- **The uBPF backend** in [`compat_ubpf.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/compat_ubpf.cpp) demonstrates the pattern: it patches bytecode for helpers during load, then delegates execution to the uBPF interpreter or JIT compiler.
- **Future extensions** such as LLVM-JIT or AOT compilers can integrate by implementing the same interface and registering with the factory map.

## Frequently Asked Questions

### How do I select a specific eBPF VM backend in bpftime?

Pass the backend name as a string to `ebpf_create()`. For example, `ebpf_create("ubpf")` instantiates the uBPF backend by looking up the registered factory in the compatibility layer's internal map. If you implement a custom LLVM-JIT backend and register it as `"llvm"`, you would instantiate it with `ebpf_create("llvm")`.

### Where does bpftime handle bytecode patching for eBPF helpers?

The patching occurs within the backend's `load_code` implementation. In the uBPF backend ([`vm/compat/ubpf-vm/compat_ubpf.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/vm/compat/ubpf-vm/compat_ubpf.cpp)), the concrete class translates the raw byte buffer into instruction structures, resolves helper indices for CALL instructions, and binds LDDW helpers before forwarding the processed code to the underlying uBPF loader.

### Can bpftime load pre-compiled AOT eBPF objects?

The architecture supports AOT through optional methods in `bpftime_vm_impl` (`do_aot_compile`, `load_aot_object`, `generate_ptx`), but the current uBPF backend does not implement them. Future backends can enable AOT by overriding these methods to serialize compiled artifacts or load them from disk, while maintaining the same `ebpf_create` and `ebpf_load` entry points.

### What happens if a VM backend fails to load a program?

If loading fails (for example, due to invalid bytecode or unresolved helpers), the backend stores an error description in its internal `error_string` member. The C API function `ebpf_load` then returns a negative value and sets the caller's error pointer to a newly allocated string containing the message, which the caller must free after handling the error.