How bpftime Handles eBPF Virtual Machines: A Plugin Architecture Deep Dive
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, 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:
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 forwards the request to a factory that instantiates the concrete backend:
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 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 executionexec(void *mem, size_t mem_len, uint64_t &ret)– Interpret the loaded program against provided memorycompile()– JIT-compile the program to native machine code and return a function pointerunload_code()– Release the current program and reset the VM stateset_lddw_helpers(...)– Bind helper functions invoked by thelddwpseudo-instructionset_unwind_function_index(size_t)– Configure tail-call-like unwind semanticsset_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. 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():
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:
- Translates the raw byte buffer into a vector of
ebpf_inststructures - Patches CALL instructions to resolve helper indices and LDDW instructions to bind map/variable helpers
- Invokes
ubpf_loadto ingest the prepared bytecode
Execution modes are thin wrappers around uBPF primitives:
execdelegates toubpf_execfor interpretationcompilecallsubpf_compileand returns aprecompiled_ebpf_functionpointer- Helper registration stores function pointers for the patching logic used during loading
- Unwind and security settings forward to
ubpf_set_unwind_function_indexandubpf_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:
- Implementing
bpftime_vm_implwith backend-specific logic for loading and execution - Registering the factory with
register_vm_factory("backend_name", create_instance) - 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:
#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_vmthat 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.cppdemonstrates 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), 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.
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 →