libe3 is a standalone, vendor-neutral C++ library for implementing the E3AP (E3 Application Protocol) on both sides of the protocol — RAN functions (DU, CU-CP, CU-UP) and dApp clients. It provides a clean object-oriented API for both roles while hiding transport, encoding, and protocol complexity.
- Vendor-Neutral API: Clean E3Agent facade that hides implementation details
- Both Roles: Single
E3Agentclass acts as either RAN (server, binds sockets) or dApp (client, connects to a remote RAN), switched byE3Config.role - Multi-Peer dApps: One dApp process can hold N
E3Agentinstances connecting to N different RAN agents (e.g. a DU-HIGH and a DU-LOW) - Multiple Transports: Support for ZeroMQ and POSIX sockets (TCP, SCTP, Unix Domain)
- Multiple Encodings: ASN.1 APER (primary), JSON, and Protocol Buffers encoders
- Service Model Extensions: Easy-to-implement SM interface for custom functionality
- Python Bindings: Optional SWIG bindings (
LIBE3_ENABLE_SWIG=ON) so the same C++ library can back Python dApp clients - Thread-Safe: Proper synchronization for concurrent dApp operations
- Modern C++17: Uses std::variant, std::optional, RAII patterns
- Lightweight: Minimal external dependencies
E3Agent serves both sides of the E3 interface — the same class, switched by one config field:
RAN Application dApp Application
(DU · CU-CP/UP · OAI · OCUDU · Aerial) (C++ · Python)
│ │
│ role = RAN role = DAPP │
▼ ▼
┌───────────────────┐ ◄─── E3AP PDUs ───► ┌──────────────────────┐
│ E3Agent │◄──────────────────────────►│ E3Agent │
│ │ ZMQ / POSIX │ │
│ register_sm(...) │ IPC · TCP · SCTP │ set_indication_ │
│ send_indication │ ASN.1 · JSON · Protobuf │ handler(fn) │
│ send_xapp_ctrl │ │ subscribe(rfid, …) │
└───────────────────┘ │ send_control(…) │
│ send_report(…) │
│ wait_for_setup() │
│ release() │
└──────────────────────┘
One dApp process can connect to N RANs simultaneously by instantiating
N E3Agent(role=DAPP) objects — each with independent threads and state.
┌────────────────────────────────────────┐
│ E3Agent │ ◄── Public API
└──────────────────┬─────────────────────┘
│
┌────────────────────────────┼─────────────────────────┐
│ │ │
┌─────────▼──────────┐ ┌───────────▼──────────┐ ┌─────────▼────────┐
│ E3Connector │ │ E3Encoder │ │ ServiceModel │
│ (Transport) │ │ (Encoding) │ │ (RAN-side SM │
├────────────────────┤ ├──────────────────────┤ │ extension │
│ ZmqConnector │ │ Asn1Encoder │ │ point) │
│ PosixConnector │ │ JsonEncoder │ ├──────────────────┤
└────────────────────┘ └──────────────────────┘ │ SimpleSM │
│ custom SMs … │
└──────────────────┘
- C++17 compiler (GCC 7+, Clang 5+, MSVC 2017+)
- CMake 3.16+
- pthreads
asn1c— ASN.1 APER encoder, required bymessages/nlohmann-json3-dev— Header-only JSON library; CMake expects thenlohmann_jsontargetprotobuf-compiler+libprotobuf-dev—protocand the Protocol Buffers runtime (only whenLIBE3_ENABLE_PROTOBUF=ON)libzmq3-devfor ZMQ transportlibsctp-dev— SCTP development headers/libraries for POSIX/SCTP transport
Install all required packages using the project's installer (recommended) or manually:
# Recommended
./build_libe3 -I
# Manual (Debian/Ubuntu)
sudo apt update
sudo apt install -y build-essential cmake pkg-config libzmq3-dev ninja-build git asn1c nlohmann-json3-dev protobuf-compiler libprotobuf-dev libsctp-dev dpkg-dev debhelper fakerootThe packaging tools (dpkg-dev, debhelper, fakeroot) are only needed by scripts/create_deb.sh.
The easiest way to build libe3 is using the provided build script:
cd libe3
# Basic release build
./build_libe3
# Debug build with tests
./build_libe3 -g -t
# Clean build with Ninja
./build_libe3 -c --ninja
# Documentation
./build_libe3 --docs
# Install dependencies first (Ubuntu/Fedora/Arch/macOS)
./build_libe3 -I
# See all options
./build_libe3 --helpAlternatively, you can use CMake directly:
cd libe3
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)| Option | Default | Description |
|---|---|---|
LIBE3_BUILD_TESTS |
ON | Build unit tests |
LIBE3_BUILD_EXAMPLES |
ON | Build examples |
LIBE3_BUILD_INTEGRATION_TESTS |
OFF | Build the integration test suite (multi-role end-to-end tests + full-loop benchmark) |
LIBE3_ENABLE_ZMQ |
ON | Enable ZeroMQ transport |
LIBE3_ENABLE_ASN1 |
ON | Enable ASN.1 encoding |
LIBE3_ENABLE_JSON |
OFF | Enable JSON encoding support |
LIBE3_ENABLE_PROTOBUF |
OFF | Enable Protocol Buffers encoding support |
LIBE3_ENABLE_ASAN |
OFF | Enable AddressSanitizer |
LIBE3_ENABLE_TSAN |
OFF | Enable ThreadSanitizer |
LIBE3_ENABLE_SWIG |
OFF | Build the SWIG-generated Python bindings (_libe3py.so + libe3py.py) |
Note on encoding selection:
LIBE3_ENABLE_ASN1,LIBE3_ENABLE_JSON, andLIBE3_ENABLE_PROTOBUFare independent compile-time inclusion flags — any combination can beONsimultaneously to build a library that supports several encodings. The active encoding is selected at runtime via theencodingfield ofe3_config_t(0 = ASN1, 1 = JSON, 2 = PROTOBUF) when callinge3_agent_create_with_config().
# With build script
./build_libe3 -t
# Or manually
cd build && ctest --output-on-failure#include <libe3/libe3.hpp>
int main() {
// Configure the agent
libe3::E3Config config;
config.ran_identifier = "my-ran-001";
config.link_layer = libe3::E3LinkLayer::POSIX;
config.transport_layer = libe3::E3TransportLayer::IPC;
config.encoding = libe3::EncodingFormat::JSON;
// Create the agent
libe3::E3Agent agent(std::move(config));
// Initialize and start (start() will call init() if needed)
if (agent.init() != libe3::ErrorCode::SUCCESS) {
// handle initialization error
}
if (agent.start() != libe3::ErrorCode::SUCCESS) {
// handle start error
}
// ... run your application ...
agent.stop();
return 0;
}The same E3Agent class can act as a dApp client. Set E3Config.role to E3Role::DAPP and register handlers for the messages flowing back from the RAN:
#include <libe3/libe3.hpp>
int main() {
libe3::E3Config config;
config.role = libe3::E3Role::DAPP;
config.dapp_name = "MyDApp";
config.dapp_version = "1.0.0";
config.vendor = "WinesLab";
config.link_layer = libe3::E3LinkLayer::ZMQ;
config.transport_layer = libe3::E3TransportLayer::IPC;
libe3::E3Agent agent(std::move(config));
agent.set_indication_handler([](const libe3::IndicationMessage& msg) {
// Decode msg.protocol_data with your SM-specific encoder.
});
agent.set_subscription_response_handler([](const libe3::SubscriptionResponse& r) {
// Inspect r.subscription_id, r.response_code.
});
if (agent.start() != libe3::ErrorCode::SUCCESS) {
return 1;
}
if (agent.wait_for_setup(std::chrono::seconds(5)) != libe3::ErrorCode::SUCCESS) {
return 1;
}
agent.subscribe(/*ran_function_id=*/1, /*telemetry_ids=*/{1}, /*control_ids=*/{1});
// ... do work; control loop on the application side ...
agent.release();
agent.stop();
return 0;
}See examples/simple_dapp.cpp for a complete reference dApp using the Simple service model. It pairs with examples/simple_agent.cpp and is wire-compatible with Python dApp clients that implement the same Simple SM encoding.
A single dApp process can connect to multiple RAN agents simultaneously by instantiating multiple E3Agent objects, each with its own configuration:
// Connect one dApp process to a DU-HIGH and a DU-LOW
libe3::E3Config cfg_high; cfg_high.role = libe3::E3Role::DAPP;
cfg_high.setup_endpoint = "ipc:///tmp/dapps_high/setup";
cfg_high.subscriber_endpoint = "ipc:///tmp/dapps_high/dapp_socket";
cfg_high.publisher_endpoint = "ipc:///tmp/dapps_high/e3_socket";
libe3::E3Config cfg_low = cfg_high;
cfg_low.setup_endpoint = "ipc:///tmp/dapps_low/setup";
cfg_low.subscriber_endpoint = "ipc:///tmp/dapps_low/dapp_socket";
cfg_low.publisher_endpoint = "ipc:///tmp/dapps_low/e3_socket";
libe3::E3Agent dapp_to_high(std::move(cfg_high));
libe3::E3Agent dapp_to_low (std::move(cfg_low));
dapp_to_high.set_indication_handler([&](const libe3::IndicationMessage& m){ /* DU-HIGH */ });
dapp_to_low .set_indication_handler([&](const libe3::IndicationMessage& m){ /* DU-LOW */ });
dapp_to_high.start();
dapp_to_low.start();Each instance has its own threads, connector, encoder, and per-peer DAppSubscriptionState. The library guarantees that N coexisting E3Agent instances in one process do not share mutable state.
Service Models implement the libe3::ServiceModel interface. Control action
handlers are registered from the SM implementation via register_control_callback
and the E3Agent routes incoming control actions to the appropriate SM.
#include <libe3/libe3.hpp>
class MyKpmServiceModel : public libe3::ServiceModel {
public:
std::string name() const override { return "KPM"; }
uint32_t version() const override { return 2; }
uint32_t ran_function_id() const override { return 100; }
std::vector<uint32_t> telemetry_ids() const override { return {1}; }
std::vector<uint32_t> control_ids() const override { return {1}; }
ErrorCode init() override {
register_control_callback(1, [](const std::vector<uint8_t>& data) {
// decode and apply control action
return ErrorCode::SUCCESS;
});
return ErrorCode::SUCCESS;
}
void destroy() override { stop(); }
ErrorCode start() override { running_ = true; return ErrorCode::SUCCESS; }
void stop() override { running_ = false; }
bool is_running() const override { return running_; }
std::vector<uint8_t> ran_function_data() const override { return {}; }
private:
std::atomic<bool> running_{false};
};
// Register with agent
agent.register_sm(std::make_unique<MyKpmServiceModel>());The library exposes IPC endpoints by default (see E3Config defaults) so
local testing with dApps on the same host is supported using the IPC transport.
There is no dedicated simulation_mode field in E3Config; instead adjust
endpoints and transport selection to run in isolated/local environments.
The single entry-point class for both roles. Configure E3Config.role = E3Role::RAN to bind as a RAN function, or E3Role::DAPP to connect as a dApp client. Methods are annotated (RAN) or (dApp) where they apply to only one role; the rest are common.
| Method | Description |
|---|---|
init() |
Initialize the agent |
start() |
Start processing threads |
stop() |
Stop the agent |
state() |
Get current state |
is_running() |
Check if agent is running |
register_sm(sm) |
Register a Service Model |
get_available_ran_functions() |
Get available RAN function IDs from registered SMs |
set_dapp_report_handler(handler) |
Set callback for incoming dApp reports (dApp → RAN) |
set_dapp_status_changed_handler(handler) |
Set callback for dApp status changes (connect, disconnect, subscribe, unsubscribe) |
send_indication(dapp_id, ran_function_id, data) |
(RAN) Send an indication to a specific dApp |
send_xapp_control(dapp_id, ran_function_id, data) |
(RAN) Forward an xApp control action to a specific dApp |
send_message_ack(request_id, response_code) |
(RAN) Send a message acknowledgment to a dApp |
get_registered_dapps() |
(RAN) Get list of registered dApp IDs |
get_dapp_subscriptions(dapp_id) |
(RAN) Get subscriptions for a dApp |
get_ran_function_subscribers(ran_function_id) |
(RAN) Get subscribers for a RAN function |
set_indication_handler(handler) |
(dApp) Callback for incoming IndicationMessage |
set_subscription_response_handler(handler) |
(dApp) Callback for incoming SubscriptionResponse |
set_setup_response_handler(handler) |
(dApp) Callback for incoming SetupResponse |
set_xapp_control_handler(handler) |
(dApp) Callback for incoming XAppControlAction |
set_message_ack_handler(handler) |
(dApp) Callback for incoming MessageAck |
wait_for_setup(timeout) |
(dApp) Block until setup handshake completes |
subscribe(ran_function_id, telemetry, control, ...) |
(dApp) Subscribe to a RAN function |
unsubscribe(ran_function_id) |
(dApp) Tear down a subscription |
send_control(ran_function_id, control_id, data) |
(dApp) Send a control action to the RAN |
send_report(ran_function_id, data) |
(dApp) Send a dApp report to the RAN |
release() |
(dApp) Send ReleaseMessage to the RAN |
dapp_id() |
(dApp) Identifier assigned by the RAN in SetupResponse |
subscribed_ran_functions() / active_subscription_ids() |
(dApp) Per-instance subscription state |
config() |
Access current E3Config |
dapp_count() |
(RAN) Number of registered dApps. (dApp) 0/1 based on assignment |
subscription_count() |
Number of active subscriptions on this side |
The E3Config structure contains agent configuration and defaults suitable
for local development (IPC) or production deployments.
| Field | Type | Description |
|---|---|---|
role |
E3Role | RAN (default — binds sockets) or DAPP (connects to a remote RAN) |
ran_identifier |
string | Unique RAN identifier (used when role=RAN) |
dapp_name |
string | dApp name advertised in SetupRequest (used when role=DAPP) |
dapp_version |
string | dApp version advertised in SetupRequest |
vendor |
string | Vendor name for SetupRequest |
e3ap_version |
string | E3AP protocol version string |
link_layer |
E3LinkLayer | ZMQ or POSIX |
transport_layer |
E3TransportLayer | SCTP, TCP, IPC |
setup_endpoint |
string | Setup connection endpoint (default: ipc:///tmp/dapps/setup) |
subscriber_endpoint |
string | Subscriber endpoint (default: ipc:///tmp/dapps/dapp_socket) |
publisher_endpoint |
string | Publisher endpoint (default: ipc:///tmp/dapps/e3_socket) |
encoding |
EncodingFormat | ASN1, JSON, or PROTOBUF |
connect_timeout_ms |
uint32_t | Connect timeout (ms) |
recv_timeout_ms |
uint32_t | Receive timeout (ms) |
send_timeout_ms |
uint32_t | Send timeout (ms) |
receive_buffer_size |
size_t | Receive buffer size (bytes) |
send_buffer_size |
size_t | Send buffer size (bytes) |
io_threads |
size_t | Number of I/O threads |
log_level |
int | Logging level (0=none .. 5=trace) |
Interface for implementing custom service models.
| Method | Description |
|---|---|
name() |
SM name (e.g., "KPM") |
version() |
SM version number |
ran_function_id() |
RAN function ID served by this SM |
telemetry_ids() |
Telemetry identifiers provided by the SM |
control_ids() |
Control identifiers accepted by the SM |
ran_function_data() |
Optional opaque bytes included in SetupResponse |
init() |
Initialize the SM (register callbacks) |
destroy() |
Destroy the SM and release resources |
start() |
Start SM processing (on first subscription) |
stop() |
Stop SM processing (on last unsubscription) |
is_running() |
Check if SM is currently running |
libe3/
├── CMakeLists.txt # Main build configuration
├── README.md # This file
├── LICENSE # Apache 2.0 license
├── VERSION # Version file (used by CMake)
├── build_libe3 # Build script
├── cmake/
│ ├── libe3Version.cmake # Version configuration
│ ├── libe3_version.hpp.in # Version header template
│ └── ... # Other CMake modules
├── include/
│ └── libe3/
│ ├── libe3.hpp # Umbrella header
│ ├── types.hpp # Type definitions
│ ├── e3_agent.hpp # Main facade
│ ├── e3_connector.hpp # Transport interface
│ ├── e3_encoder.hpp # Encoder interface
│ └── ...
├── messages/
│ ├── asn1/ # ASN.1 definitions
│ └── CMakeLists.txt # ASN.1 build config
├── src/
│ ├── core/ # Core implementations
│ ├── connector/ # Transport implementations
│ └── encoder/ # Encoder implementations
├── tests/ # Unit tests
└── examples/ # Example applications
The library version is managed via the VERSION file in the project root. During compilation, CMake reads this file and generates libe3/version.hpp with the version macros:
#include <libe3/libe3.hpp>
// Access version information
const char* ver = libe3::version(); // e.g., "0.0.1"
int major, minor, patch;
libe3::version(major, minor, patch); // e.g., 0, 0, 1Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
The full API reference is automatically generated with Doxygen and published to GitHub Pages on every release.
🔗 https://wineslab.github.io/libe3/
To build the documentation locally:
# Install Doxygen and Graphviz (Ubuntu)
sudo apt-get install doxygen graphviz
# Build docs (output will be in build/docs/html/)
./build_libe3 --docs
# Open in browser
xdg-open build/docs/html/index.htmllibe3 ships an optional SWIG-generated Python binding so the same C++ library can back Python dApp clients. It is off by default — enable it with -DLIBE3_ENABLE_SWIG=ON:
# Install SWIG and Python development headers (Ubuntu)
sudo apt-get install -y swig python3-dev
# Build + install into the active interpreter's site-packages (activate your venv first)
./build_libe3 --install --enable-swig \
--cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
python3 -c "import libe3py; print('libe3py OK')"
# Or, build only (no install) and run the smoke test (CTest label "swig")
cmake -S . -B build -DLIBE3_ENABLE_SWIG=ON
cmake --build build --target libe3py -j $(nproc)
PYTHONPATH=build/swig python3 tests/test_swig_smoke.pyThe module exposes two layers: a backwards-compatible minimal E3Agent view, and
the full DAppSession dApp seam — a complete lifecycle (start /
wait_for_setup / subscribe / send_control / release / stop) plus a
batched, lock-free inbound queue drained via poll_events(max_batch, timeout_ms), designed for the sub-millisecond / high-throughput E3AP path
(GIL released during blocking calls). SM payloads cross as native bytes;
per-SM encoding stays in Python so existing Python SM implementations work
unchanged. build_libe3 --install --enable-swig installs _libe3py.so +
libe3py.py into Python3_SITEARCH.
See swig/README.md for the architecture, rationale, and a
full Python usage example, and swig/e3_dapp_session.hpp
for the documented API.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Based on the original E3AP implementation in OpenAirInterface by Andrea Lacava - Northeastern University
- The E3AP is a protocol inspired by O-RAN E2AP design patterns with due extensions for dApp-xApp management and synch
- The code rationale and architecture is partially insipired by the e2sim and ns-O-RAN projects
This work has been partially supported by OUSD(R&E) through Army Research Laboratory Cooperative Agreement Number W911NF-24-2-0065.