diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index ed14803..ac87089 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -60,10 +60,12 @@ jobs: - name: Run integration tests run: cd build && ctest -L integration --output-on-failure - dual-encoding: - # Regression guard for issue #26: ASN.1 and JSON must be buildable together - # (the encoder is selected at runtime via E3Config.encoding). - name: Dual Encoding (ASN.1 + JSON) + all-encodings: + # Regression guard: ASN.1, JSON, and Protocol Buffers must all be buildable + # together in a single libe3 (the encoder is selected at runtime via + # E3Config.encoding). Builds one library with every encoder compiled in and + # runs the full test suite against it. + name: All Encodings (ASN.1 + JSON + Protobuf) runs-on: ubuntu-latest needs: test steps: @@ -76,18 +78,19 @@ jobs: chmod +x ./build_libe3 || true ./build_libe3 -I || true - - name: Configure with both encoders enabled + - name: Configure with all encoders enabled run: | - cmake -S . -B build-both \ + cmake -S . -B build-all \ -DCMAKE_BUILD_TYPE=Release \ -DLIBE3_ENABLE_ASN1=ON \ - -DLIBE3_ENABLE_JSON=ON + -DLIBE3_ENABLE_JSON=ON \ + -DLIBE3_ENABLE_PROTOBUF=ON - name: Build - run: cmake --build build-both -j $(nproc) + run: cmake --build build-all -j $(nproc) - name: Run tests - run: cd build-both && ctest --output-on-failure + run: cd build-all && ctest --output-on-failure swig: name: SWIG Python Bindings diff --git a/.github/workflows/tag-and-release.yml b/.github/workflows/tag-and-release.yml index 7b44d5e..90ff2fe 100644 --- a/.github/workflows/tag-and-release.yml +++ b/.github/workflows/tag-and-release.yml @@ -68,15 +68,37 @@ jobs: run: | chmod +x ./scripts/create_deb.sh || true - - name: Run packaging script for amd64 + - name: Build and package all encoding variants for amd64 run: | - ./scripts/create_deb.sh amd64 - - - name: Upload amd64 .deb artifact + set -e + build_variant() { + local name="$1" dir="$2"; shift 2 + cmake -S . -B "$dir" \ + -DCMAKE_BUILD_TYPE=Release \ + -DLIBE3_BUILD_TESTS=OFF \ + -DLIBE3_BUILD_EXAMPLES=OFF \ + "$@" + cmake --build "$dir" -j "$(nproc)" + if [ -n "$name" ]; then + ./scripts/create_deb.sh --use-existing-build --build-dir "$dir" --variant "$name" amd64 + else + ./scripts/create_deb.sh --use-existing-build --build-dir "$dir" amd64 + fi + } + # Default (all encodings) keeps the primary "libe3" package name. + build_variant "" build-all -DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON -DLIBE3_ENABLE_PROTOBUF=ON + build_variant "asn1" build-asn1 -DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=OFF -DLIBE3_ENABLE_PROTOBUF=OFF + build_variant "json" build-json -DLIBE3_ENABLE_ASN1=OFF -DLIBE3_ENABLE_JSON=ON -DLIBE3_ENABLE_PROTOBUF=OFF + build_variant "protobuf" build-protobuf -DLIBE3_ENABLE_ASN1=OFF -DLIBE3_ENABLE_JSON=OFF -DLIBE3_ENABLE_PROTOBUF=ON + mkdir -p dist + cp build-*/*.deb dist/ + ls -l dist/ + + - name: Upload amd64 .deb artifacts uses: actions/upload-artifact@v4 with: name: libe3-deb-amd64 - path: build/libe3_${{ needs.create-tag.outputs.version }}_amd64.deb + path: dist/*.deb build-deb-arm64: name: Build Debian package (arm64) @@ -98,15 +120,37 @@ jobs: run: | chmod +x ./scripts/create_deb.sh || true - - name: Run packaging script for arm64 + - name: Build and package all encoding variants for arm64 run: | - ./scripts/create_deb.sh arm64 - - - name: Upload arm64 .deb artifact + set -e + build_variant() { + local name="$1" dir="$2"; shift 2 + cmake -S . -B "$dir" \ + -DCMAKE_BUILD_TYPE=Release \ + -DLIBE3_BUILD_TESTS=OFF \ + -DLIBE3_BUILD_EXAMPLES=OFF \ + "$@" + cmake --build "$dir" -j "$(nproc)" + if [ -n "$name" ]; then + ./scripts/create_deb.sh --use-existing-build --build-dir "$dir" --variant "$name" arm64 + else + ./scripts/create_deb.sh --use-existing-build --build-dir "$dir" arm64 + fi + } + # Default (all encodings) keeps the primary "libe3" package name. + build_variant "" build-all -DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON -DLIBE3_ENABLE_PROTOBUF=ON + build_variant "asn1" build-asn1 -DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=OFF -DLIBE3_ENABLE_PROTOBUF=OFF + build_variant "json" build-json -DLIBE3_ENABLE_ASN1=OFF -DLIBE3_ENABLE_JSON=ON -DLIBE3_ENABLE_PROTOBUF=OFF + build_variant "protobuf" build-protobuf -DLIBE3_ENABLE_ASN1=OFF -DLIBE3_ENABLE_JSON=OFF -DLIBE3_ENABLE_PROTOBUF=ON + mkdir -p dist + cp build-*/*.deb dist/ + ls -l dist/ + + - name: Upload arm64 .deb artifacts uses: actions/upload-artifact@v4 with: name: libe3-deb-arm64 - path: build/libe3_${{ needs.create-tag.outputs.version }}_arm64.deb + path: dist/*.deb build-docs: name: Build API Documentation diff --git a/.gitignore b/.gitignore index 6c6b018..ab17eab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # Build directories build/ +build-*/ cmake-build-*/ out/ +dist/ # Compiled objects *.o diff --git a/CLAUDE.md b/CLAUDE.md index fbd65d0..8f0710f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ ctest -LE integration # only unit tests PYTHONPATH=build/swig python3 tests/test_swig_smoke.py # SWIG bindings smoke test ``` -**Build options** live in `cmake/libe3Options.cmake`: `LIBE3_BUILD_TESTS` (ON), `LIBE3_BUILD_EXAMPLES` (ON), `LIBE3_BUILD_INTEGRATION_TESTS` (OFF), `LIBE3_ENABLE_SWIG` (OFF), `LIBE3_ENABLE_ZMQ` (ON), `LIBE3_ENABLE_ASN1` (ON), `LIBE3_ENABLE_JSON` (OFF), `LIBE3_ENABLE_ASAN`/`LIBE3_ENABLE_TSAN` (OFF), `LIBE3_BUILD_DOCS` (OFF). **At least one of `LIBE3_ENABLE_ASN1` / `LIBE3_ENABLE_JSON` must be ON**; both can be built together and the encoder is chosen at runtime. +**Build options** live in `cmake/libe3Options.cmake`: `LIBE3_BUILD_TESTS` (ON), `LIBE3_BUILD_EXAMPLES` (ON), `LIBE3_BUILD_INTEGRATION_TESTS` (OFF), `LIBE3_ENABLE_SWIG` (OFF), `LIBE3_ENABLE_ZMQ` (ON), `LIBE3_ENABLE_ASN1` (ON), `LIBE3_ENABLE_JSON` (OFF), `LIBE3_ENABLE_PROTOBUF` (OFF), `LIBE3_ENABLE_ASAN`/`LIBE3_ENABLE_TSAN` (OFF), `LIBE3_BUILD_DOCS` (OFF). **At least one of `LIBE3_ENABLE_ASN1` / `LIBE3_ENABLE_JSON` / `LIBE3_ENABLE_PROTOBUF` must be ON**; any combination can be built together and the encoder is chosen at runtime. `build_libe3` has a dedicated `--enable-protobuf` flag; JSON and SWIG use `--cmake-opt`. ## Architecture (big picture) @@ -64,7 +64,7 @@ The design has one façade over three pluggable seams, plus a real-time data pat **Real-time data path.** Producers never touch the network directly. Lock-free MPMC ring buffers (`include/libe3/mpmc_queue.hpp`, wrapped by `lockfree_queue.hpp`) decouple work from dedicated I/O threads that `E3Interface` spawns: setup, inbound, and outbound, plus RAN-only threads that poll Service Models for telemetry and drain dApp reports off the receive path. Optional CPU affinity and niceness (Linux) are set from `E3Config` to cut scheduling jitter in sub-millisecond loops. -**State & types.** `SubscriptionManager` (RAN side) and `DAppSubscriptionState` (dApp side) track subscriptions and assigned IDs. The 11 E3AP message types are modeled as a `std::variant`-based `Pdu` in `include/libe3/types.hpp`. The wire protocol's source of truth is the ASN.1 grammar `messages/asn1/V1/e3ap-1.0.0.asn1`, from which `asn1c` generates C encode/decode code at build time (the `asn1_e3ap` target). +**State & types.** `SubscriptionManager` (RAN side) and `DAppSubscriptionState` (dApp side) track subscriptions and assigned IDs. The 11 E3AP message types are modeled as a `std::variant`-based `Pdu` in `include/libe3/types.hpp`. Each wire encoding has its own grammar under `messages/`, generated at build time into a dedicated library: ASN.1 in `messages/asn1/V1/e3ap-1.0.0.asn1` (`asn1c` -> `asn1_e3ap`, C) and Protocol Buffers in `messages/proto/V1/e3ap-1.0.0.proto` (`protoc` -> `pb_e3ap`, C++). Keep these grammars in sync with the `Pdu` structs when adding fields. **Bindings & examples.** A C API lives in `include/libe3/c_api.h`; Python bindings via SWIG (`swig/libe3.i` → the `libe3py` module). `examples/simple_agent.cpp` (RAN) and `examples/simple_dapp.cpp` (dApp), with the reference Simple SM in `examples/sm_simple/`, are the best runnable illustration of the two roles (they require ASN.1). diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f08c04..ddf58c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,24 +43,23 @@ include(libe3Version) # ============================================================================ # Encoder selection # ============================================================================ -# ASN.1 and JSON can be enabled together: the encoder factory -# (src/encoder/encoder_factory.cpp) creates the right encoder per -# EncodingFormat at runtime, so a single build can serve either encoding +# ASN.1, JSON, and Protocol Buffers can be enabled together: the encoder +# factory (src/encoder/encoder_factory.cpp) creates the right encoder per +# EncodingFormat at runtime, so a single build can serve any enabled encoding # (selected by E3Config.encoding). At least one must be enabled. -if(NOT LIBE3_ENABLE_ASN1 AND NOT LIBE3_ENABLE_JSON) - message(FATAL_ERROR "Neither LIBE3_ENABLE_ASN1 nor LIBE3_ENABLE_JSON is enabled. Enable at least one encoding.") -endif() -if(LIBE3_ENABLE_ASN1 AND LIBE3_ENABLE_JSON) - message(STATUS "Both ASN.1 and JSON encoders enabled; encoding is selected at runtime via E3Config.encoding.") +if(NOT LIBE3_ENABLE_ASN1 AND NOT LIBE3_ENABLE_JSON AND NOT LIBE3_ENABLE_PROTOBUF) + message(FATAL_ERROR "No encoding enabled. Enable at least one of LIBE3_ENABLE_ASN1, LIBE3_ENABLE_JSON, or LIBE3_ENABLE_PROTOBUF.") endif() # ============================================================================ -# ASN.1 Messages Library +# Generated Message Libraries (ASN.1 and/or Protocol Buffers) # ============================================================================ -if(LIBE3_ENABLE_ASN1) +# The messages/ subdirectory generates the asn1_e3ap and/or pb_e3ap libraries +# from the E3AP grammars, gated internally on the enabled encodings. +if(LIBE3_ENABLE_ASN1 OR LIBE3_ENABLE_PROTOBUF) add_subdirectory(messages) else() - message(STATUS "ASN.1 encoding support disabled by configuration") + message(STATUS "ASN.1 and Protobuf encoding support disabled by configuration") endif() # ============================================================================ diff --git a/README.md b/README.md index 254c2d4..73dd834 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - **Both Roles**: Single `E3Agent` class acts as either **RAN** (server, binds sockets) or **dApp** (client, connects to a remote RAN), switched by `E3Config.role` - **Multi-Peer dApps**: One dApp process can hold N `E3Agent` instances 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) and JSON encoders +- **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 @@ -36,7 +36,7 @@ │ E3Agent │◄──────────────────────────►│ E3Agent │ │ │ ZMQ / POSIX │ │ │ register_sm(...) │ IPC · TCP · SCTP │ set_indication_ │ - │ send_indication │ ASN.1 APER · JSON │ handler(fn) │ + │ send_indication │ ASN.1 · JSON · Protobuf │ handler(fn) │ │ send_xapp_ctrl │ │ subscribe(rfid, …) │ └───────────────────┘ │ send_control(…) │ │ send_report(…) │ @@ -77,6 +77,7 @@ - pthreads - `asn1c` — ASN.1 APER encoder, required by `messages/` - `nlohmann-json3-dev` — Header-only JSON library; CMake expects the `nlohmann_json` target +- `protobuf-compiler` + `libprotobuf-dev` — `protoc` and the Protocol Buffers runtime (only when `LIBE3_ENABLE_PROTOBUF=ON`) - `libzmq3-dev` for ZMQ transport - `libsctp-dev` — SCTP development headers/libraries for POSIX/SCTP transport @@ -90,7 +91,7 @@ Install all required packages using the project's installer (recommended) or man # Manual (Debian/Ubuntu) sudo apt update -sudo apt install -y build-essential cmake pkg-config libzmq3-dev ninja-build git asn1c nlohmann-json3-dev libsctp-dev dpkg-dev debhelper fakeroot +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 fakeroot ``` The packaging tools (`dpkg-dev`, `debhelper`, `fakeroot`) are only needed by `scripts/create_deb.sh`. @@ -140,14 +141,16 @@ make -j$(nproc) | `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` and `LIBE3_ENABLE_JSON` are independent -> compile-time inclusion flags — both can be `ON` simultaneously to build a library that supports -> both encodings. The active encoding is **selected at runtime** via the `encoding` field of -> `e3_config_t` (0 = ASN1, 1 = JSON) when calling `e3_agent_create_with_config()`. +> **Note on encoding selection:** `LIBE3_ENABLE_ASN1`, `LIBE3_ENABLE_JSON`, and +> `LIBE3_ENABLE_PROTOBUF` are independent compile-time inclusion flags — any combination can be +> `ON` simultaneously to build a library that supports several encodings. The active encoding is +> **selected at runtime** via the `encoding` field of `e3_config_t` (0 = ASN1, 1 = JSON, +> 2 = PROTOBUF) when calling `e3_agent_create_with_config()`. ### Running Tests @@ -371,7 +374,7 @@ for local development (IPC) or production deployments. | `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 or JSON | +| `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) | diff --git a/VERSION b/VERSION index 1750564..5a5831a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.6 +0.0.7 diff --git a/build_libe3 b/build_libe3 index 439497f..c70da37 100755 --- a/build_libe3 +++ b/build_libe3 @@ -32,6 +32,7 @@ BUILD_EXAMPLES=1 ENABLE_ZMQ=1 ENABLE_ASN1=1 ENABLE_JSON=0 +ENABLE_PROTOBUF=0 ENABLE_ASAN=0 ENABLE_TSAN=0 ENABLE_UBSAN=0 @@ -104,6 +105,8 @@ Features: --disable-zmq Disable ZeroMQ transport --enable-asn1 Enable ASN.1 encoding support (default: ON) --disable-asn1 Disable ASN.1 encoding + --enable-protobuf Enable Protocol Buffers encoding support (default: OFF) + --disable-protobuf Disable Protocol Buffers encoding --enable-tests Build unit tests (default: ON) --disable-tests Disable unit tests --enable-examples Build examples (default: ON) @@ -225,7 +228,9 @@ install_dependencies() { bison \ flex \ nlohmann-json3-dev \ - libsctp-dev + libsctp-dev \ + protobuf-compiler \ + libprotobuf-dev # Build and install asn1c from source (some distros ship old versions) INSTALLER="apt-get" install_asn1c_from_source @@ -240,7 +245,9 @@ install_dependencies() { git \ asn1c \ nlohmann-json-devel \ - lksctp-tools-devel + lksctp-tools-devel \ + protobuf-compiler \ + protobuf-devel elif check_command pacman; then echo_info "Detected Arch Linux system" sudo pacman -Sy --noconfirm \ @@ -252,10 +259,11 @@ install_dependencies() { git \ asn1c \ nlohmann-json \ - lksctp-tools + lksctp-tools \ + protobuf elif check_command brew; then echo_info "Detected macOS with Homebrew" - brew install cmake pkg-config zeromq ninja asn1c nlohmann-json + brew install cmake pkg-config zeromq ninja asn1c nlohmann-json protobuf else echo_error "Unsupported package manager. Please install dependencies manually:" echo " - cmake (>= 3.16)" @@ -300,6 +308,7 @@ configure_cmake() { "-DLIBE3_ENABLE_ZMQ=$([ $ENABLE_ZMQ -eq 1 ] && echo ON || echo OFF)" "-DLIBE3_ENABLE_ASN1=$([ $ENABLE_ASN1 -eq 1 ] && echo ON || echo OFF)" "-DLIBE3_ENABLE_JSON=$([ $ENABLE_JSON -eq 1 ] && echo ON || echo OFF)" + "-DLIBE3_ENABLE_PROTOBUF=$([ $ENABLE_PROTOBUF -eq 1 ] && echo ON || echo OFF)" "-DLIBE3_ENABLE_ASAN=$([ $ENABLE_ASAN -eq 1 ] && echo ON || echo OFF)" "-DLIBE3_ENABLE_TSAN=$([ $ENABLE_TSAN -eq 1 ] && echo ON || echo OFF)" "-DLIBE3_BUILD_DOCS=$([ $BUILD_DOCS -eq 1 ] && echo ON || echo OFF)" @@ -571,6 +580,14 @@ main() { ENABLE_ASN1=0 shift ;; + --enable-protobuf) + ENABLE_PROTOBUF=1 + shift + ;; + --disable-protobuf) + ENABLE_PROTOBUF=0 + shift + ;; --enable-tests) BUILD_TESTS=1 shift diff --git a/cmake/libe3Config.cmake.in b/cmake/libe3Config.cmake.in index 367148e..9f71879 100644 --- a/cmake/libe3Config.cmake.in +++ b/cmake/libe3Config.cmake.in @@ -16,6 +16,11 @@ if(@LIBE3_ENABLE_JSON@) find_dependency(nlohmann_json 3.11 REQUIRED) endif() +# If libe3 was built with Protobuf support, make downstream consumers find Protobuf +if(@LIBE3_ENABLE_PROTOBUF@) + find_dependency(Protobuf REQUIRED) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/libe3Targets.cmake") check_required_components(libe3) diff --git a/cmake/libe3Dependencies.cmake b/cmake/libe3Dependencies.cmake index 639a68c..21629a8 100644 --- a/cmake/libe3Dependencies.cmake +++ b/cmake/libe3Dependencies.cmake @@ -27,6 +27,12 @@ if(LIBE3_ENABLE_JSON) endif() endif() +# Required: Protocol Buffers (libprotobuf runtime + protoc compiler) for protobuf encoding +if(LIBE3_ENABLE_PROTOBUF) + find_package(Protobuf REQUIRED) + message(STATUS "Protobuf found: ${Protobuf_VERSION} (protoc: ${Protobuf_PROTOC_EXECUTABLE})") +endif() + # Required: tl::expected for C++17 std::expected-like functionality include(FetchContent) FetchContent_Declare( @@ -43,6 +49,28 @@ message(STATUS "tl::expected: Fetched from GitHub") # Optional Dependencies # ============================================================================ +# Optional: Google Benchmark for the integration micro-benchmarks +# (dev-only; never linked into the shipped library) +if(LIBE3_BUILD_INTEGRATION_TESTS) + find_package(benchmark 1.8 QUIET) + if(NOT benchmark_FOUND) + set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) + set(BENCHMARK_INSTALL_DOCS OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG v1.9.1 + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(googlebenchmark) + message(STATUS "Google Benchmark: Fetched from GitHub") + else() + message(STATUS "Google Benchmark: Found installed version") + endif() +endif() + # Optional: ZeroMQ if(LIBE3_ENABLE_ZMQ) find_package(PkgConfig QUIET) diff --git a/cmake/libe3Install.cmake b/cmake/libe3Install.cmake index 365f21e..c62be4e 100644 --- a/cmake/libe3Install.cmake +++ b/cmake/libe3Install.cmake @@ -25,6 +25,10 @@ if(LIBE3_ENABLE_ASN1) list(APPEND LIBE3_INSTALL_TARGETS asn1_e3ap) endif() +if(LIBE3_ENABLE_PROTOBUF) + list(APPEND LIBE3_INSTALL_TARGETS pb_e3ap) +endif() + install(TARGETS ${LIBE3_INSTALL_TARGETS} EXPORT libe3Targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -52,6 +56,14 @@ if(LIBE3_ENABLE_ASN1) ) endif() +# Install Protobuf generated headers +if(LIBE3_ENABLE_PROTOBUF) + install(DIRECTORY ${PROTOBUF_GENERATED_DIR}/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/libe3/proto + FILES_MATCHING PATTERN "*.pb.h" + ) +endif() + # Export targets install(EXPORT libe3Targets FILE libe3Targets.cmake diff --git a/cmake/libe3Options.cmake b/cmake/libe3Options.cmake index 82f68ef..72fe247 100644 --- a/cmake/libe3Options.cmake +++ b/cmake/libe3Options.cmake @@ -11,6 +11,7 @@ option(LIBE3_ENABLE_SWIG option(LIBE3_ENABLE_ZMQ "Enable ZeroMQ transport" ON) option(LIBE3_ENABLE_ASN1 "Enable ASN.1 encoding support" ON) option(LIBE3_ENABLE_JSON "Enable JSON encoding support" OFF) +option(LIBE3_ENABLE_PROTOBUF "Enable Protocol Buffers encoding support" OFF) option(LIBE3_ENABLE_ASAN "Enable AddressSanitizer" OFF) option(LIBE3_ENABLE_TSAN "Enable ThreadSanitizer" OFF) option(LIBE3_BUILD_DOCS "Build documentation" OFF) diff --git a/cmake/libe3Sources.cmake b/cmake/libe3Sources.cmake index ec37912..7bbd33b 100644 --- a/cmake/libe3Sources.cmake +++ b/cmake/libe3Sources.cmake @@ -48,3 +48,7 @@ endif() if(LIBE3_ENABLE_JSON) list(APPEND LIBE3_SOURCES src/encoder/json_encoder.cpp) endif() + +if(LIBE3_ENABLE_PROTOBUF) + list(APPEND LIBE3_SOURCES src/encoder/protobuf_encoder.cpp) +endif() diff --git a/cmake/libe3Targets.cmake b/cmake/libe3Targets.cmake index 7b3fa36..fbeaeb8 100644 --- a/cmake/libe3Targets.cmake +++ b/cmake/libe3Targets.cmake @@ -39,6 +39,11 @@ if(LIBE3_ENABLE_ASN1) target_compile_definitions(libe3 PUBLIC LIBE3_ENABLE_ASN1) endif() +if(LIBE3_ENABLE_PROTOBUF) + target_link_libraries(libe3 PUBLIC pb_e3ap protobuf::libprotobuf) + target_compile_definitions(libe3 PUBLIC LIBE3_ENABLE_PROTOBUF) +endif() + if(LIBE3_ENABLE_ZMQ) target_compile_definitions(libe3 PUBLIC LIBE3_HAS_ZMQ=1) target_link_libraries(libe3 PRIVATE PkgConfig::ZMQ) @@ -88,6 +93,11 @@ if(LIBE3_ENABLE_ASN1) target_compile_definitions(libe3_shared PUBLIC LIBE3_ENABLE_ASN1) endif() +if(LIBE3_ENABLE_PROTOBUF) + target_link_libraries(libe3_shared PUBLIC pb_e3ap protobuf::libprotobuf) + target_compile_definitions(libe3_shared PUBLIC LIBE3_ENABLE_PROTOBUF) +endif() + if(LIBE3_ENABLE_ZMQ) target_compile_definitions(libe3_shared PUBLIC LIBE3_HAS_ZMQ=1) target_link_libraries(libe3_shared PRIVATE PkgConfig::ZMQ) diff --git a/cmake/libe3Tests.cmake b/cmake/libe3Tests.cmake index 03e9d10..d8e9bb2 100644 --- a/cmake/libe3Tests.cmake +++ b/cmake/libe3Tests.cmake @@ -27,6 +27,10 @@ foreach(test_src IN LISTS LIBE3_TEST_SOURCES) message(STATUS "Skipping test_json_encoder: JSON support disabled") continue() endif() + if(NOT LIBE3_ENABLE_PROTOBUF AND simple_name STREQUAL "protobuf_encoder") + message(STATUS "Skipping test_protobuf_encoder: Protobuf support disabled") + continue() + endif() add_executable(${target_name} "${CMAKE_CURRENT_SOURCE_DIR}/${test_src}") target_link_libraries(${target_name} @@ -69,6 +73,9 @@ if(LIBE3_BUILD_INTEGRATION_TESTS AND LIBE3_ENABLE_ASN1) ) target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples) + if(simple_name STREQUAL "bench_encoding_size") + target_link_libraries(${target_name} PRIVATE benchmark::benchmark) + endif() add_test(NAME ${target_name} COMMAND ${target_name}) set_tests_properties(${target_name} PROPERTIES LABELS "integration") endforeach() diff --git a/examples/simple_agent.cpp b/examples/simple_agent.cpp index 0e70a52..e3fc549 100644 --- a/examples/simple_agent.cpp +++ b/examples/simple_agent.cpp @@ -33,8 +33,9 @@ class SimpleServiceModel : public libe3::ServiceModel { // period_us: microseconds between indication emissions. Sub-millisecond // values stress the conflate/queueing balance; the per-send log is silenced // below 1 ms so stdout I/O does not dominate the measurement. - explicit SimpleServiceModel(uint64_t period_us = 2'000'000) - : period_us_(period_us), quiet_(period_us < 1000) {} + explicit SimpleServiceModel(uint64_t period_us = 2'000'000, + libe3::EncodingFormat encoding = libe3::EncodingFormat::ASN1) + : period_us_(period_us), quiet_(period_us < 1000), encoding_(encoding) {} std::string name() const override { return "SIMPLE"; } uint32_t version() const override { return 1; } @@ -69,7 +70,7 @@ class SimpleServiceModel : public libe3::ServiceModel { std::vector ran_function_data() const override { const std::string name = "SIMPLE"; std::vector out; - if (libe3_examples::encode_ran_function_data(name, out)) { + if (libe3_examples::encode_ran_function_data(name, out, encoding_)) { return out; } return {}; @@ -93,7 +94,7 @@ class SimpleServiceModel : public libe3::ServiceModel { const libe3::DAppControlAction& action ) override { int sampling = 0; - bool decode_ok = libe3_examples::decode_simple_control(action.action_data, sampling); + bool decode_ok = libe3_examples::decode_simple_control(action.action_data, sampling, encoding_); if (decode_ok) { std::cout << "[SIMPLE] Control action " << action.control_identifier << ": samplingThreshold=" << sampling << "\n"; @@ -115,6 +116,7 @@ class SimpleServiceModel : public libe3::ServiceModel { uint32_t seq_{0}; uint64_t period_us_{2'000'000}; bool quiet_{false}; + libe3::EncodingFormat encoding_{libe3::EncodingFormat::ASN1}; void worker_loop() { while (running_) { @@ -141,7 +143,7 @@ class SimpleServiceModel : public libe3::ServiceModel { si.timestamp = static_cast(now_ms & 0x7FFFFFFF); std::vector encoded; - if (!libe3_examples::encode_simple_indication(si, encoded)) { + if (!libe3_examples::encode_simple_indication(si, encoded, encoding_)) { std::cerr << "Failed to encode Simple-Indication\n"; continue; } @@ -171,7 +173,7 @@ void print_usage(const char* program_name) { << "Options:\n" << " -l, --link Link layer: zmq, posix (default: zmq)\n" << " -t, --transport Transport layer: sctp, tcp, ipc (default: ipc)\n" - << " -e, --encoding Encoding format: asn1, json (default: asn1)\n" + << " -e, --encoding Encoding format: asn1, json, protobuf (default: asn1)\n" << " -r, --ran_id RAN identifier advertised in setup (default: example-ran-001)\n" << " -d, --socket-dir IPC socket directory (default: /tmp/dapps).\n" << " Lets multiple RANs coexist on one host over IPC.\n" @@ -202,8 +204,18 @@ libe3::E3TransportLayer parse_transport_layer(const char* str) { libe3::EncodingFormat parse_encoding(const char* str) { if (std::strcmp(str, "asn1") == 0) return libe3::EncodingFormat::ASN1; if (std::strcmp(str, "json") == 0) return libe3::EncodingFormat::JSON; + if (std::strcmp(str, "protobuf") == 0) return libe3::EncodingFormat::PROTOBUF; std::cerr << "Invalid encoding format: " << str << ". Using default (asn1).\n"; - return libe3::EncodingFormat::JSON; + return libe3::EncodingFormat::ASN1; +} + +static const char* encoding_to_cstr(libe3::EncodingFormat enc) { + switch (enc) { + case libe3::EncodingFormat::JSON: return "json"; + case libe3::EncodingFormat::PROTOBUF: return "protobuf"; + case libe3::EncodingFormat::ASN1: return "asn1"; + } + return "asn1"; } int main(int argc, char* argv[]) { @@ -295,7 +307,7 @@ int main(int argc, char* argv[]) { << " RAN ID: " << ran_id << "\n" << " Link layer: " << libe3::link_layer_to_string(link_layer) << "\n" << " Transport layer: " << libe3::transport_layer_to_string(transport_layer) << "\n" - << " Encoding: " << (encoding == libe3::EncodingFormat::JSON ? "json" : "asn1") << "\n" + << " Encoding: " << encoding_to_cstr(encoding) << "\n" << " Socket dir: " << (socket_dir.empty() ? "/tmp/dapps (default)" : socket_dir) << "\n" << " Port offset: " << port_offset << "\n" << " Period: " << period_us << " us\n\n"; @@ -313,7 +325,7 @@ int main(int argc, char* argv[]) { } // Register the simple service model (with the configured emission period) - auto sm_result = agent.register_sm(std::make_unique(period_us)); + auto sm_result = agent.register_sm(std::make_unique(period_us, encoding)); if (sm_result != libe3::ErrorCode::SUCCESS) { std::cerr << "Failed to register Simple SM: " << libe3::error_code_to_string(sm_result) << "\n"; @@ -328,10 +340,10 @@ int main(int argc, char* argv[]) { }); // Handle dApp reports for the RAN - agent.set_dapp_report_handler([](const libe3::DAppReport& report) { + agent.set_dapp_report_handler([encoding](const libe3::DAppReport& report) { // In the RAN report is sent to the xApp through E2, here we just decode it as en example libe3_examples::SimpleDAppReport decoded; - if (libe3_examples::decode_simple_dapp_report(report.report_data, decoded)) { + if (libe3_examples::decode_simple_dapp_report(report.report_data, decoded, encoding)) { std::cout << "[SIMPLE] dApp report from dApp " << report.dapp_identifier << " (RAN function " << report.ran_function_identifier << "): bin1=" << decoded.bin1 << "\n"; diff --git a/examples/simple_dapp.cpp b/examples/simple_dapp.cpp index 5c16072..d10fc99 100644 --- a/examples/simple_dapp.cpp +++ b/examples/simple_dapp.cpp @@ -48,7 +48,7 @@ static void print_usage(const char* program_name) { << "Options:\n" << " -l, --link Link layer: zmq, posix (default: zmq)\n" << " -t, --transport Transport: sctp, tcp, ipc (default: ipc)\n" - << " -e, --encoding Encoding: asn1, json (default: asn1)\n" + << " -e, --encoding Encoding: asn1, json, protobuf (default: asn1)\n" << " -c, --control Send a Simple-Control every 5th indication\n" << " -T, --timed Stop after this many seconds (0 = unlimited)\n" << " -d, --socket-dir IPC socket dir of a RAN to connect to.\n" @@ -113,10 +113,20 @@ static libe3::E3TransportLayer parse_transport_layer(const char* str) { static libe3::EncodingFormat parse_encoding(const char* str) { if (std::strcmp(str, "asn1") == 0) return libe3::EncodingFormat::ASN1; if (std::strcmp(str, "json") == 0) return libe3::EncodingFormat::JSON; + if (std::strcmp(str, "protobuf") == 0) return libe3::EncodingFormat::PROTOBUF; std::cerr << "Invalid encoding: " << str << " (using asn1)\n"; return libe3::EncodingFormat::ASN1; } +static const char* encoding_to_cstr(libe3::EncodingFormat enc) { + switch (enc) { + case libe3::EncodingFormat::JSON: return "json"; + case libe3::EncodingFormat::PROTOBUF: return "protobuf"; + case libe3::EncodingFormat::ASN1: return "asn1"; + } + return "asn1"; +} + int main(int argc, char* argv[]) { libe3::E3LinkLayer link_layer = libe3::E3LinkLayer::ZMQ; libe3::E3TransportLayer transport_layer = libe3::E3TransportLayer::IPC; @@ -191,7 +201,7 @@ int main(int argc, char* argv[]) { << " Role: dapp\n" << " Link: " << libe3::link_layer_to_string(link_layer) << "\n" << " Transport: " << libe3::transport_layer_to_string(transport_layer) << "\n" - << " Encoding: " << (encoding == libe3::EncodingFormat::JSON ? "json" : "asn1") << "\n" + << " Encoding: " << encoding_to_cstr(encoding) << "\n" << " Control: " << (control_enabled ? "on" : "off") << "\n" << " Timed (s): " << timed_seconds << "\n" << " RAN peers: " << peers.size() << "\n\n"; @@ -224,9 +234,9 @@ int main(int argc, char* argv[]) { p->agent = std::make_unique(std::move(config)); - p->agent->set_indication_handler([p, control_enabled, quiet](const libe3::IndicationMessage& msg) { + p->agent->set_indication_handler([p, control_enabled, quiet, encoding](const libe3::IndicationMessage& msg) { libe3_examples::SimpleIndication si; - if (!libe3_examples::decode_simple_indication(msg.protocol_data, si)) { + if (!libe3_examples::decode_simple_indication(msg.protocol_data, si, encoding)) { std::cerr << "[SIMPLE] peer=" << p->label << " failed to decode indication (" << msg.protocol_data.size() << " bytes)\n"; return; @@ -279,7 +289,7 @@ int main(int argc, char* argv[]) { if (control_enabled && seq % 5 == 0) { const int sampling = static_cast(seq % 101); std::vector encoded; - if (libe3_examples::encode_simple_control(sampling, encoded)) { + if (libe3_examples::encode_simple_control(sampling, encoded, encoding)) { auto rc = p->agent->send_control(/*ran_function_id=*/1, /*control_id=*/1, encoded); if (rc == libe3::ErrorCode::SUCCESS) { if (!quiet) { diff --git a/examples/sm_simple/e3sm_simple.proto b/examples/sm_simple/e3sm_simple.proto new file mode 100644 index 0000000..bed901b --- /dev/null +++ b/examples/sm_simple/e3sm_simple.proto @@ -0,0 +1,38 @@ +// Simple Service Model message definitions, Protocol Buffers form. +// +// Protocol Buffers counterpart of examples/sm_simple/e3sm_simple.asn. These +// messages are carried inside the opaque E3AP payload fields (protocol_data, +// action_data, xapp_control_data, report_data) when the Simple SM example runs +// with protobuf encoding. +// +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package libe3.sm.simple.v1; + +// RAN function data (advertised in the Setup Response ran_function_data). +message SimpleRanFunctionData { + string name = 1; // Human-readable Service Model name +} + +// Indication payload (RAN -> dApp). +message SimpleIndication { + uint32 data1 = 1; // Random data / monotonic sequence counter + optional uint32 timestamp = 2; // Unix timestamp when samples were captured +} + +// Control payload (dApp -> RAN). +message SimpleControl { + optional uint32 sampling_threshold = 1; // Sampling delivery ratio (0..100) +} + +// Configuration control payload (xApp -> dApp via RAN). +message SimpleConfigControl { + bool enable = 1; // Enable/disable data delivery +} + +// dApp report payload (dApp -> RAN). +message SimpleDAppReport { + uint32 bin1 = 1; +} diff --git a/examples/sm_simple/e3sm_simple_wrapper.cpp b/examples/sm_simple/e3sm_simple_wrapper.cpp index 6c3a219..251f1cf 100644 --- a/examples/sm_simple/e3sm_simple_wrapper.cpp +++ b/examples/sm_simple/e3sm_simple_wrapper.cpp @@ -1,6 +1,7 @@ -/* Example wrapper helpers that use ASN.1 generated types. - * These are compiled only into the examples binary and are not part - * of the main libe3 public API. +/* Example wrapper helpers that use ASN.1 generated types (and, when libe3 is + * built with JSON or protobuf support, JSON text payloads or the generated + * Simple SM protobuf types). These are compiled only into the examples binary + * and are not part of the main libe3 public API. */ #include "e3sm_simple_wrapper.hpp" @@ -21,12 +22,63 @@ extern "C" { } #endif +#ifdef LIBE3_ENABLE_PROTOBUF +#include "e3sm_simple.pb.h" +#endif + +#ifdef LIBE3_ENABLE_JSON +#include +#endif + #include #include namespace libe3_examples { -bool encode_simple_indication(const SimpleIndication& in, std::vector& out) { +#ifdef LIBE3_ENABLE_PROTOBUF +namespace smpb = libe3::sm::simple::v1; +#endif + +#ifdef LIBE3_ENABLE_JSON +namespace { +// JSON SM payloads are UTF-8 JSON text bytes with camelCase keys, matching +// the JSON envelope encoder convention (the envelope nests the indication +// payload as a JSON object on the wire). +inline void json_to_bytes(const nlohmann::json& j, std::vector& out) { + const std::string s = j.dump(); + out.assign(s.begin(), s.end()); +} + +inline bool bytes_to_json(const std::vector& in, nlohmann::json& j) { + j = nlohmann::json::parse(in.begin(), in.end(), nullptr, false); + return !j.is_discarded(); +} +} // namespace +#endif + +bool encode_simple_indication(const SimpleIndication& in, std::vector& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleIndication m; + m.set_data1(in.data1); + if (in.timestamp.has_value()) m.set_timestamp(*in.timestamp); + std::string s; + if (!m.SerializeToString(&s)) return false; + out.assign(s.begin(), s.end()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + j["data1"] = in.data1; + if (in.timestamp.has_value()) j["timestamp"] = *in.timestamp; + json_to_bytes(j, out); + return true; + } +#endif Simple_Indication_t si; memset(&si, 0, sizeof(si)); si.data1 = in.data1; @@ -49,7 +101,29 @@ bool encode_simple_indication(const SimpleIndication& in, std::vector& return true; } -bool decode_simple_indication(const std::vector& in, SimpleIndication& out) { +bool decode_simple_indication(const std::vector& in, SimpleIndication& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleIndication m; + if (!m.ParseFromArray(in.data(), static_cast(in.size()))) return false; + out.data1 = m.data1(); + if (m.has_timestamp()) out.timestamp = m.timestamp(); + else out.timestamp.reset(); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + if (!bytes_to_json(in, j) || !j.contains("data1")) return false; + out.data1 = j["data1"].get(); + if (j.contains("timestamp")) out.timestamp = j["timestamp"].get(); + else out.timestamp.reset(); + return true; + } +#endif Simple_Indication_t *si = nullptr; asn_dec_rval_t dr = aper_decode(NULL, &asn_DEF_Simple_Indication, (void **)&si, in.data(), in.size(), 0, 0); if (dr.code != RC_OK || !si) return false; @@ -62,7 +136,26 @@ bool decode_simple_indication(const std::vector& in, SimpleIndication& return true; } -bool decode_simple_control(const std::vector& in, int& samplingThreshold) { +bool decode_simple_control(const std::vector& in, int& samplingThreshold, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleControl m; + if (!m.ParseFromArray(in.data(), static_cast(in.size()))) return false; + if (!m.has_sampling_threshold()) return false; + samplingThreshold = static_cast(m.sampling_threshold()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + if (!bytes_to_json(in, j) || !j.contains("samplingThreshold")) return false; + samplingThreshold = j["samplingThreshold"].get(); + return true; + } +#endif Simple_Control_t *sc = nullptr; asn_dec_rval_t dr = aper_decode(NULL, &asn_DEF_Simple_Control, (void **)&sc, in.data(), in.size(), 0, 0); if (dr.code != RC_OK || !sc) return false; @@ -77,7 +170,27 @@ bool decode_simple_control(const std::vector& in, int& samplingThreshol return true; } -bool encode_simple_control(int samplingThreshold, std::vector& out) { +bool encode_simple_control(int samplingThreshold, std::vector& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleControl m; + m.set_sampling_threshold(static_cast(samplingThreshold)); + std::string s; + if (!m.SerializeToString(&s)) return false; + out.assign(s.begin(), s.end()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + j["samplingThreshold"] = samplingThreshold; + json_to_bytes(j, out); + return true; + } +#endif Simple_Control_t sc; memset(&sc, 0, sizeof(sc)); sc.samplingThreshold = (long *)malloc(sizeof(long)); @@ -94,7 +207,27 @@ bool encode_simple_control(int samplingThreshold, std::vector& out) { return true; } -bool encode_ran_function_data(const std::string name, std::vector& out) { +bool encode_ran_function_data(const std::string name, std::vector& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleRanFunctionData m; + m.set_name(name); + std::string s; + if (!m.SerializeToString(&s)) return false; + out.assign(s.begin(), s.end()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + j["name"] = name; + json_to_bytes(j, out); + return true; + } +#endif Simple_RanFunctionData_t srd; memset(&srd, 0, sizeof(srd)); @@ -116,7 +249,25 @@ bool encode_ran_function_data(const std::string name, std::vector& out) return true; } -bool decode_simple_dapp_report(const std::vector& in, SimpleDAppReport& out) { +bool decode_simple_dapp_report(const std::vector& in, SimpleDAppReport& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleDAppReport m; + if (!m.ParseFromArray(in.data(), static_cast(in.size()))) return false; + out.bin1 = static_cast(m.bin1()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + if (!bytes_to_json(in, j) || !j.contains("bin1")) return false; + out.bin1 = j["bin1"].get(); + return true; + } +#endif Simple_DAppReport_t* rep = nullptr; asn_dec_rval_t dr = aper_decode(NULL, &asn_DEF_Simple_DAppReport, (void**)&rep, in.data(), in.size(), 0, 0); if (dr.code != RC_OK || !rep) return false; @@ -125,7 +276,27 @@ bool decode_simple_dapp_report(const std::vector& in, SimpleDAppReport& return true; } -bool encode_simple_config_control(const SimpleConfigControl& in, std::vector& out) { +bool encode_simple_config_control(const SimpleConfigControl& in, std::vector& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleConfigControl m; + m.set_enable(in.enable); + std::string s; + if (!m.SerializeToString(&s)) return false; + out.assign(s.begin(), s.end()); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + j["enable"] = in.enable; + json_to_bytes(j, out); + return true; + } +#endif Simple_ConfigControl_t scc; memset(&scc, 0, sizeof(scc)); scc.enable = in.enable ? 1 : 0; @@ -140,7 +311,25 @@ bool encode_simple_config_control(const SimpleConfigControl& in, std::vector& in, SimpleConfigControl& out) { +bool decode_simple_config_control(const std::vector& in, SimpleConfigControl& out, + libe3::EncodingFormat enc) { + (void)enc; +#ifdef LIBE3_ENABLE_PROTOBUF + if (enc == libe3::EncodingFormat::PROTOBUF) { + smpb::SimpleConfigControl m; + if (!m.ParseFromArray(in.data(), static_cast(in.size()))) return false; + out.enable = m.enable(); + return true; + } +#endif +#ifdef LIBE3_ENABLE_JSON + if (enc == libe3::EncodingFormat::JSON) { + nlohmann::json j; + if (!bytes_to_json(in, j) || !j.contains("enable")) return false; + out.enable = j["enable"].get(); + return true; + } +#endif Simple_ConfigControl_t* scc = nullptr; asn_dec_rval_t dr = aper_decode(NULL, &asn_DEF_Simple_ConfigControl, (void**)&scc, in.data(), in.size(), 0, 0); if (dr.code != RC_OK || !scc) return false; diff --git a/examples/sm_simple/e3sm_simple_wrapper.hpp b/examples/sm_simple/e3sm_simple_wrapper.hpp index 2d682d7..a49b981 100644 --- a/examples/sm_simple/e3sm_simple_wrapper.hpp +++ b/examples/sm_simple/e3sm_simple_wrapper.hpp @@ -1,6 +1,13 @@ /* * Wrapper helpers for encoding/decoding the Simple Service Model * (placed in examples so they don't affect the main library) + * + * The Service Model payload encoding is independent of the E3AP transport + * encoding, but the example selects it from the same EncodingFormat so a run + * started with `-e json` or `-e protobuf` uses that encoding end-to-end + * (envelope and SM payload). The JSON and protobuf branches are only + * available when libe3 was built with LIBE3_ENABLE_JSON / + * LIBE3_ENABLE_PROTOBUF respectively; otherwise these helpers use ASN.1 APER. */ #pragma once @@ -9,6 +16,8 @@ #include #include +#include + namespace libe3_examples { struct SimpleIndication { @@ -24,28 +33,36 @@ struct SimpleConfigControl { bool enable; }; -// Encode Simple-Indication into PER/UPER bytes -bool encode_simple_indication(const SimpleIndication& in, std::vector& out); +// Encode Simple-Indication into SM payload bytes +bool encode_simple_indication(const SimpleIndication& in, std::vector& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); -// Decode Simple-Indication from PER/UPER bytes -bool decode_simple_indication(const std::vector& in, SimpleIndication& out); +// Decode Simple-Indication from SM payload bytes +bool decode_simple_indication(const std::vector& in, SimpleIndication& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); // Encode Simple-Control (samplingThreshold) -bool encode_simple_control(int samplingThreshold, std::vector& out); +bool encode_simple_control(int samplingThreshold, std::vector& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); // Decode Simple-Control -> samplingThreshold -bool decode_simple_control(const std::vector& in, int& samplingThreshold); +bool decode_simple_control(const std::vector& in, int& samplingThreshold, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); -// Encode Simple-RanFunctionData into APER bytes -bool encode_ran_function_data(const std::string name, std::vector& out); +// Encode Simple-RanFunctionData into SM payload bytes +bool encode_ran_function_data(const std::string name, std::vector& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); -// Decode Simple-DAppReport from APER bytes (dApp → RAN report) -bool decode_simple_dapp_report(const std::vector& in, SimpleDAppReport& out); +// Decode Simple-DAppReport from SM payload bytes (dApp -> RAN report) +bool decode_simple_dapp_report(const std::vector& in, SimpleDAppReport& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); -// Encode Simple-ConfigControl into APER bytes -bool encode_simple_config_control(const SimpleConfigControl& in, std::vector& out); +// Encode Simple-ConfigControl into SM payload bytes +bool encode_simple_config_control(const SimpleConfigControl& in, std::vector& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); -// Decode Simple-ConfigControl from APER bytes -bool decode_simple_config_control(const std::vector& in, SimpleConfigControl& out); +// Decode Simple-ConfigControl from SM payload bytes +bool decode_simple_config_control(const std::vector& in, SimpleConfigControl& out, + libe3::EncodingFormat enc = libe3::EncodingFormat::ASN1); } // namespace libe3_examples diff --git a/include/libe3/types.hpp b/include/libe3/types.hpp index defe9d3..3a65b1c 100644 --- a/include/libe3/types.hpp +++ b/include/libe3/types.hpp @@ -29,8 +29,9 @@ namespace libe3 { * @brief E3AP encoding formats supported by the library */ enum class EncodingFormat : uint8_t { - ASN1 = 0, ///< ASN.1 PER encoding (standard O-RAN format) - JSON = 1 ///< JSON encoding (for development/debugging) + ASN1 = 0, ///< ASN.1 PER encoding (standard O-RAN format) + JSON = 1, ///< JSON encoding (for development/debugging) + PROTOBUF = 2 ///< Protocol Buffers encoding }; /** diff --git a/messages/CMakeLists.txt b/messages/CMakeLists.txt index f3667f3..e548337 100644 --- a/messages/CMakeLists.txt +++ b/messages/CMakeLists.txt @@ -2,62 +2,130 @@ # # SPDX-License-Identifier: Apache-2.0 # -# Handles ASN.1 code generation and the asn1_e3ap library +# Generates the E3AP message code for the enabled encodings: +# - ASN.1 -> asn1_e3ap (C, via asn1c) +# - Protobuf -> pb_e3ap (C++, via protoc) set(E3AP_VERSION 1 0 0) string(REPLACE ";" "." E3AP_RELEASE "${E3AP_VERSION}") - -if(E3AP_RELEASE VERSION_EQUAL "1.0.0") - include(asn1/V1/e3ap-1.0.0.cmake) -else() - message(FATAL_ERROR "Unknown E3AP_RELEASE ${E3AP_RELEASE}") -endif() - message(STATUS "Selected E3AP_VERSION: ${E3AP_RELEASE}") -# Find asn1c compiler -find_program(ASN1C_EXEC asn1c HINTS /opt/asn1c/bin) -if(NOT ASN1C_EXEC) - message(FATAL_ERROR "asn1c not found! ASN.1 encoding is required.\n" - "Install with: sudo apt install asn1c\n" - "Or provide path: cmake -DASN1C_EXEC=/path/to/asn1c") -endif() -message(STATUS "asn1c found: ${ASN1C_EXEC}") - -# Generate ASN.1 C code using custom_command (on-demand generation) -add_custom_command(OUTPUT ${e3ap_source} ${e3ap_headers} - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR} - COMMAND ${ASN1C_EXEC} -pdu=all -gen-APER -gen-UPER -no-gen-JER -no-gen-BER -no-gen-OER -fno-include-deps -fcompound-names -findirect-choice -no-gen-example -D ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR} - COMMENT "Generating E3AP ASN.1 source files from ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR}" -) - -# Optionally generate sources for the simple service model (e3sm_simple.asn) -if(LIBE3_BUILD_EXAMPLES) - # Include example-only CMake snippet which generates the simple ASN grammar - include(${CMAKE_CURRENT_SOURCE_DIR}/../examples/sm_simple/CMakeLists.txt) - - # If the examples CMake exported generated lists, append them into the - # asn1 sources so they are compiled only when examples are enabled. - if(DEFINED E3SM_SIMPLE_GENERATED_SRCS) - list(APPEND e3ap_source ${E3SM_SIMPLE_GENERATED_SRCS}) +# ============================================================================ +# ASN.1 message library (asn1_e3ap) +# ============================================================================ +if(LIBE3_ENABLE_ASN1) + if(E3AP_RELEASE VERSION_EQUAL "1.0.0") + include(asn1/V1/e3ap-1.0.0.cmake) + else() + message(FATAL_ERROR "Unknown E3AP_RELEASE ${E3AP_RELEASE}") + endif() + + # Find asn1c compiler + find_program(ASN1C_EXEC asn1c HINTS /opt/asn1c/bin) + if(NOT ASN1C_EXEC) + message(FATAL_ERROR "asn1c not found! ASN.1 encoding is required.\n" + "Install with: sudo apt install asn1c\n" + "Or provide path: cmake -DASN1C_EXEC=/path/to/asn1c") endif() - if(DEFINED E3SM_SIMPLE_GENERATED_HDRS) - list(APPEND e3ap_headers ${E3SM_SIMPLE_GENERATED_HDRS}) + message(STATUS "asn1c found: ${ASN1C_EXEC}") + + # Generate ASN.1 C code using custom_command (on-demand generation) + add_custom_command(OUTPUT ${e3ap_source} ${e3ap_headers} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${ASN1C_EXEC} -pdu=all -gen-APER -gen-UPER -no-gen-JER -no-gen-BER -no-gen-OER -fno-include-deps -fcompound-names -findirect-choice -no-gen-example -D ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR} + COMMENT "Generating E3AP ASN.1 source files from ${CMAKE_CURRENT_SOURCE_DIR}/${E3AP_GRAMMAR}" + ) + + # Optionally generate sources for the simple service model (e3sm_simple.asn) + if(LIBE3_BUILD_EXAMPLES) + # Include example-only CMake snippet which generates the simple ASN grammar + include(${CMAKE_CURRENT_SOURCE_DIR}/../examples/sm_simple/CMakeLists.txt) + + # If the examples CMake exported generated lists, append them into the + # asn1 sources so they are compiled only when examples are enabled. + if(DEFINED E3SM_SIMPLE_GENERATED_SRCS) + list(APPEND e3ap_source ${E3SM_SIMPLE_GENERATED_SRCS}) + endif() + if(DEFINED E3SM_SIMPLE_GENERATED_HDRS) + list(APPEND e3ap_headers ${E3SM_SIMPLE_GENERATED_HDRS}) + endif() endif() + + # Create ASN.1 library + add_library(asn1_e3ap ${e3ap_source}) + set_target_properties(asn1_e3ap PROPERTIES LINKER_LANGUAGE C) + target_include_directories(asn1_e3ap + PUBLIC + $ + $ + ) + target_compile_options(asn1_e3ap PRIVATE -DASN_DISABLE_OER_SUPPORT -w) + + # Export the generated directory for parent to use + set(ASN1_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}" PARENT_SCOPE) + + message(STATUS "ASN.1 encoding support enabled") endif() -# Create ASN.1 library -add_library(asn1_e3ap ${e3ap_source}) -set_target_properties(asn1_e3ap PROPERTIES LINKER_LANGUAGE C) -target_include_directories(asn1_e3ap - PUBLIC - $ - $ -) -target_compile_options(asn1_e3ap PRIVATE -DASN_DISABLE_OER_SUPPORT -w) +# ============================================================================ +# Protocol Buffers message library (pb_e3ap) +# ============================================================================ +if(LIBE3_ENABLE_PROTOBUF) + set(PROTO_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/proto") + file(MAKE_DIRECTORY "${PROTO_OUT_DIR}") + + # E3AP protobuf grammar + set(E3AP_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/proto/V1") + set(E3AP_PROTO "${E3AP_PROTO_DIR}/e3ap-1.0.0.proto") + set(e3ap_pb_src "${PROTO_OUT_DIR}/e3ap-1.0.0.pb.cc") + set(e3ap_pb_hdr "${PROTO_OUT_DIR}/e3ap-1.0.0.pb.h") + + add_custom_command(OUTPUT ${e3ap_pb_src} ${e3ap_pb_hdr} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PROTO_OUT_DIR} + COMMAND ${Protobuf_PROTOC_EXECUTABLE} + --cpp_out=${PROTO_OUT_DIR} + --proto_path=${E3AP_PROTO_DIR} + ${E3AP_PROTO} + DEPENDS ${E3AP_PROTO} + COMMENT "Generating E3AP protobuf source files from ${E3AP_PROTO}" + ) + + set(pb_e3ap_sources ${e3ap_pb_src}) -# Export the generated directory for parent to use -set(ASN1_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}" PARENT_SCOPE) + # Optionally generate the simple service model proto (e3sm_simple.proto) + if(LIBE3_BUILD_EXAMPLES) + set(E3SM_SIMPLE_PROTO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../examples/sm_simple") + set(E3SM_SIMPLE_PROTO "${E3SM_SIMPLE_PROTO_DIR}/e3sm_simple.proto") + set(e3sm_pb_src "${PROTO_OUT_DIR}/e3sm_simple.pb.cc") + set(e3sm_pb_hdr "${PROTO_OUT_DIR}/e3sm_simple.pb.h") -message(STATUS "ASN.1 encoding support enabled") + add_custom_command(OUTPUT ${e3sm_pb_src} ${e3sm_pb_hdr} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PROTO_OUT_DIR} + COMMAND ${Protobuf_PROTOC_EXECUTABLE} + --cpp_out=${PROTO_OUT_DIR} + --proto_path=${E3SM_SIMPLE_PROTO_DIR} + ${E3SM_SIMPLE_PROTO} + DEPENDS ${E3SM_SIMPLE_PROTO} + COMMENT "Generating Simple SM protobuf source files from ${E3SM_SIMPLE_PROTO}" + ) + list(APPEND pb_e3ap_sources ${e3sm_pb_src}) + endif() + + # Create Protobuf library. Generated protobuf code is treated as a system + # include so it does not trip the project's strict warning set. + add_library(pb_e3ap ${pb_e3ap_sources}) + set_target_properties(pb_e3ap PROPERTIES LINKER_LANGUAGE CXX POSITION_INDEPENDENT_CODE ON) + target_include_directories(pb_e3ap SYSTEM + PUBLIC + $ + $ + ) + target_link_libraries(pb_e3ap PUBLIC protobuf::libprotobuf) + target_compile_options(pb_e3ap PRIVATE -w) + + # Export the generated directory for parent to use + set(PROTOBUF_GENERATED_DIR "${PROTO_OUT_DIR}" PARENT_SCOPE) + + message(STATUS "Protocol Buffers encoding support enabled") +endif() diff --git a/messages/proto/V1/e3ap-1.0.0.proto b/messages/proto/V1/e3ap-1.0.0.proto new file mode 100644 index 0000000..c57decb --- /dev/null +++ b/messages/proto/V1/e3ap-1.0.0.proto @@ -0,0 +1,129 @@ +// E3AP (E3 Application Protocol) message definitions, Protocol Buffers form. +// +// This schema is the Protocol Buffers counterpart of the ASN.1 grammar in +// messages/asn1/V1/e3ap-1.0.0.asn1. Field layout mirrors the C++ PDU structs +// in include/libe3/types.hpp so the encoder round-trips them losslessly. The +// oneof cases follow the E3AP PDU type ordering (0..10). +// +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package libe3.e3ap.v1; + +// Response code shared by setup, subscription, and acknowledgment messages. +enum ResponseCode { + RESPONSE_CODE_POSITIVE = 0; // Request accepted + RESPONSE_CODE_NEGATIVE = 1; // Request rejected +} + +// E3 Setup Request (dApp -> RAN). +message SetupRequest { + string e3ap_protocol_version = 1; + string dapp_name = 2; + string dapp_version = 3; + string vendor = 4; +} + +// RAN function advertised in a Setup Response. +message RanFunctionDef { + uint32 ran_function_identifier = 1; + repeated uint32 telemetry_identifier_list = 2; + repeated uint32 control_identifier_list = 3; + bytes ran_function_data = 4; +} + +// E3 Setup Response (RAN -> dApp). +message SetupResponse { + uint32 request_id = 1; + ResponseCode response_code = 2; + optional string e3ap_protocol_version = 3; + optional uint32 dapp_identifier = 4; + string ran_identifier = 5; + repeated RanFunctionDef ran_function_list = 6; +} + +// Subscription Request (dApp -> RAN). +message SubscriptionRequest { + uint32 dapp_identifier = 1; + uint32 ran_function_identifier = 2; + repeated uint32 telemetry_identifier_list = 3; + repeated uint32 control_identifier_list = 4; + optional uint32 subscription_time = 5; + optional uint32 periodicity = 6; +} + +// Subscription Delete (dApp -> RAN). +message SubscriptionDelete { + uint32 dapp_identifier = 1; + uint32 subscription_id = 2; +} + +// Subscription Response (RAN -> dApp). +message SubscriptionResponse { + uint32 request_id = 1; + uint32 dapp_identifier = 2; + ResponseCode response_code = 3; + optional uint32 subscription_id = 4; +} + +// Indication Message (RAN -> dApp). protocol_data carries the opaque +// Service Model payload. +message IndicationMessage { + uint32 dapp_identifier = 1; + uint32 ran_function_identifier = 2; + bytes protocol_data = 3; +} + +// dApp Control Action (dApp -> RAN). action_data is Service Model opaque. +message DAppControlAction { + uint32 dapp_identifier = 1; + uint32 ran_function_identifier = 2; + uint32 control_identifier = 3; + bytes action_data = 4; +} + +// dApp Report (dApp -> RAN). report_data is Service Model opaque. +message DAppReport { + uint32 dapp_identifier = 1; + uint32 ran_function_identifier = 2; + bytes report_data = 3; +} + +// xApp Control Action (xApp -> dApp via RAN). xapp_control_data is opaque. +message XAppControlAction { + uint32 dapp_identifier = 1; + uint32 ran_function_identifier = 2; + bytes xapp_control_data = 3; +} + +// Release Message (dApp -> RAN). +message ReleaseMessage { + uint32 dapp_identifier = 1; +} + +// Generic message acknowledgment. +message MessageAck { + uint32 request_id = 1; + ResponseCode response_code = 2; +} + +// Top-level E3AP PDU envelope. The oneof case identifies the PDU type; its +// ordering matches the E3AP PDU type enumeration (SETUP_REQUEST=0 .. MESSAGE_ACK=10). +message E3Pdu { + uint32 id = 1; // message identifier + uint64 timestamp = 2; // milliseconds since epoch + oneof msg { + SetupRequest setup_request = 3; + SetupResponse setup_response = 4; + SubscriptionRequest subscription_request = 5; + SubscriptionDelete subscription_delete = 6; + SubscriptionResponse subscription_response = 7; + IndicationMessage indication_message = 8; + DAppControlAction dapp_control_action = 9; + DAppReport dapp_report = 10; + XAppControlAction xapp_control_action = 11; + ReleaseMessage release_message = 12; + MessageAck message_ack = 13; + } +} diff --git a/scripts/create_deb.sh b/scripts/create_deb.sh index 4169f65..1857fd8 100755 --- a/scripts/create_deb.sh +++ b/scripts/create_deb.sh @@ -11,10 +11,13 @@ VERSION=$(cat VERSION | tr -d '[:space:]') BUILD_DIR="$ROOT_DIR/build" DEB_OUT_DIR="$BUILD_DIR" USE_EXISTING_BUILD=0 +VARIANT="" usage() { - echo "Usage: $0 [--use-existing-build] [--build-dir DIR] [amd64|arm64|all]" + echo "Usage: $0 [--use-existing-build] [--build-dir DIR] [--variant NAME] [amd64|arm64|all]" echo "If no architecture argument is given, host architecture (dpkg --print-architecture) is used." + echo "--variant NAME produces libe3-NAME__.deb (Package: libe3-NAME);" + echo "without it, the package is libe3__.deb (Package: libe3)." } ARG_ARCH="" @@ -33,6 +36,15 @@ while [ "$#" -gt 0 ]; do BUILD_DIR="$2" shift 2 ;; + --variant) + if [ -z "${2:-}" ]; then + echo "Missing value for --variant" + usage + exit 1 + fi + VARIANT="$2" + shift 2 + ;; amd64|arm64|all|x86_64|x64|aarch64) if [ -n "$ARG_ARCH" ]; then echo "Only one architecture argument is allowed." @@ -115,8 +127,17 @@ build_for() { DEBIAN_DIR="$PKG_ROOT/DEBIAN" mkdir -p "$DEBIAN_DIR" + # Variant packages carry a distinct Package name and filename suffix so the + # per-encoding builds can coexist alongside the default (all-encodings) one. + local pkg_name="libe3" + local deb_name="libe3_${VERSION}_${ARCH}.deb" + if [ -n "$VARIANT" ]; then + pkg_name="libe3-${VARIANT}" + deb_name="libe3-${VARIANT}_${VERSION}_${ARCH}.deb" + fi + cat >"$DEBIAN_DIR/control" < create_encoder(EncodingFormat format) { #if LIBE3_ENABLE_JSON case EncodingFormat::JSON: return std::make_unique(); +#endif +#if LIBE3_ENABLE_PROTOBUF + case EncodingFormat::PROTOBUF: + return std::make_unique(); #endif default: break; diff --git a/src/encoder/protobuf_encoder.cpp b/src/encoder/protobuf_encoder.cpp new file mode 100644 index 0000000..63a218a --- /dev/null +++ b/src/encoder/protobuf_encoder.cpp @@ -0,0 +1,326 @@ +/** + * @file protobuf_encoder.cpp + * @brief Protocol Buffers encoder for E3AP PDUs + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "protobuf_encoder.hpp" + +#include "e3ap-1.0.0.pb.h" + +#include +#include + +namespace libe3 { + +namespace pb = e3ap::v1; + +namespace { + +pb::ResponseCode to_pb_response_code(ResponseCode rc) { + return rc == ResponseCode::POSITIVE ? pb::RESPONSE_CODE_POSITIVE + : pb::RESPONSE_CODE_NEGATIVE; +} + +ResponseCode from_pb_response_code(pb::ResponseCode rc) { + return rc == pb::RESPONSE_CODE_NEGATIVE ? ResponseCode::NEGATIVE + : ResponseCode::POSITIVE; +} + +std::vector to_bytes(const std::string& s) { + return std::vector(s.begin(), s.end()); +} + +} // namespace + +bool ProtobufE3Encoder::pdu_to_proto(const Pdu& pdu, pb::E3Pdu& out) const { + out.set_id(pdu.message_id); + out.set_timestamp(pdu.timestamp); + + switch (pdu.type) { + case PduType::SETUP_REQUEST: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_setup_request(); + m->set_e3ap_protocol_version(s->e3ap_protocol_version); + m->set_dapp_name(s->dapp_name); + m->set_dapp_version(s->dapp_version); + m->set_vendor(s->vendor); + return true; + } + case PduType::SETUP_RESPONSE: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_setup_response(); + m->set_request_id(s->request_id); + m->set_response_code(to_pb_response_code(s->response_code)); + if (s->e3ap_protocol_version) m->set_e3ap_protocol_version(*s->e3ap_protocol_version); + if (s->dapp_identifier) m->set_dapp_identifier(*s->dapp_identifier); + m->set_ran_identifier(s->ran_identifier); + for (const auto& rf : s->ran_function_list) { + auto* pf = m->add_ran_function_list(); + pf->set_ran_function_identifier(rf.ran_function_identifier); + for (auto t : rf.telemetry_identifier_list) pf->add_telemetry_identifier_list(t); + for (auto c : rf.control_identifier_list) pf->add_control_identifier_list(c); + pf->set_ran_function_data(rf.ran_function_data.data(), rf.ran_function_data.size()); + } + return true; + } + case PduType::SUBSCRIPTION_REQUEST: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_subscription_request(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_ran_function_identifier(s->ran_function_identifier); + for (auto t : s->telemetry_identifier_list) m->add_telemetry_identifier_list(t); + for (auto c : s->control_identifier_list) m->add_control_identifier_list(c); + if (s->subscription_time) m->set_subscription_time(*s->subscription_time); + if (s->periodicity) m->set_periodicity(*s->periodicity); + return true; + } + case PduType::SUBSCRIPTION_DELETE: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_subscription_delete(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_subscription_id(s->subscription_id); + return true; + } + case PduType::SUBSCRIPTION_RESPONSE: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_subscription_response(); + m->set_request_id(s->request_id); + m->set_dapp_identifier(s->dapp_identifier); + m->set_response_code(to_pb_response_code(s->response_code)); + if (s->subscription_id) m->set_subscription_id(*s->subscription_id); + return true; + } + case PduType::INDICATION_MESSAGE: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_indication_message(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_ran_function_identifier(s->ran_function_identifier); + m->set_protocol_data(s->protocol_data.data(), s->protocol_data.size()); + return true; + } + case PduType::DAPP_CONTROL_ACTION: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_dapp_control_action(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_ran_function_identifier(s->ran_function_identifier); + m->set_control_identifier(s->control_identifier); + m->set_action_data(s->action_data.data(), s->action_data.size()); + return true; + } + case PduType::DAPP_REPORT: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_dapp_report(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_ran_function_identifier(s->ran_function_identifier); + m->set_report_data(s->report_data.data(), s->report_data.size()); + return true; + } + case PduType::XAPP_CONTROL_ACTION: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_xapp_control_action(); + m->set_dapp_identifier(s->dapp_identifier); + m->set_ran_function_identifier(s->ran_function_identifier); + m->set_xapp_control_data(s->xapp_control_data.data(), s->xapp_control_data.size()); + return true; + } + case PduType::RELEASE_MESSAGE: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_release_message(); + m->set_dapp_identifier(s->dapp_identifier); + return true; + } + case PduType::MESSAGE_ACK: { + const auto* s = pdu.get_if(); + if (!s) return false; + auto* m = out.mutable_message_ack(); + m->set_request_id(s->request_id); + m->set_response_code(to_pb_response_code(s->response_code)); + return true; + } + } + return false; +} + +Pdu ProtobufE3Encoder::proto_to_pdu(const pb::E3Pdu& proto) const { + Pdu pdu; + pdu.message_id = proto.id(); + pdu.timestamp = proto.timestamp(); + + switch (proto.msg_case()) { + case pb::E3Pdu::kSetupRequest: { + const auto& m = proto.setup_request(); + SetupRequest s; + s.e3ap_protocol_version = m.e3ap_protocol_version(); + s.dapp_name = m.dapp_name(); + s.dapp_version = m.dapp_version(); + s.vendor = m.vendor(); + pdu.type = PduType::SETUP_REQUEST; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kSetupResponse: { + const auto& m = proto.setup_response(); + SetupResponse s; + s.request_id = m.request_id(); + s.response_code = from_pb_response_code(m.response_code()); + if (m.has_e3ap_protocol_version()) s.e3ap_protocol_version = m.e3ap_protocol_version(); + if (m.has_dapp_identifier()) s.dapp_identifier = m.dapp_identifier(); + s.ran_identifier = m.ran_identifier(); + for (int i = 0; i < m.ran_function_list_size(); ++i) { + const auto& pf = m.ran_function_list(i); + RanFunctionDef rf; + rf.ran_function_identifier = pf.ran_function_identifier(); + for (int j = 0; j < pf.telemetry_identifier_list_size(); ++j) + rf.telemetry_identifier_list.push_back(pf.telemetry_identifier_list(j)); + for (int j = 0; j < pf.control_identifier_list_size(); ++j) + rf.control_identifier_list.push_back(pf.control_identifier_list(j)); + rf.ran_function_data = to_bytes(pf.ran_function_data()); + s.ran_function_list.push_back(std::move(rf)); + } + pdu.type = PduType::SETUP_RESPONSE; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kSubscriptionRequest: { + const auto& m = proto.subscription_request(); + SubscriptionRequest s; + s.dapp_identifier = m.dapp_identifier(); + s.ran_function_identifier = m.ran_function_identifier(); + for (int j = 0; j < m.telemetry_identifier_list_size(); ++j) + s.telemetry_identifier_list.push_back(m.telemetry_identifier_list(j)); + for (int j = 0; j < m.control_identifier_list_size(); ++j) + s.control_identifier_list.push_back(m.control_identifier_list(j)); + if (m.has_subscription_time()) s.subscription_time = m.subscription_time(); + if (m.has_periodicity()) s.periodicity = m.periodicity(); + pdu.type = PduType::SUBSCRIPTION_REQUEST; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kSubscriptionDelete: { + const auto& m = proto.subscription_delete(); + SubscriptionDelete s; + s.dapp_identifier = m.dapp_identifier(); + s.subscription_id = m.subscription_id(); + pdu.type = PduType::SUBSCRIPTION_DELETE; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kSubscriptionResponse: { + const auto& m = proto.subscription_response(); + SubscriptionResponse s; + s.request_id = m.request_id(); + s.dapp_identifier = m.dapp_identifier(); + s.response_code = from_pb_response_code(m.response_code()); + if (m.has_subscription_id()) s.subscription_id = m.subscription_id(); + pdu.type = PduType::SUBSCRIPTION_RESPONSE; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kIndicationMessage: { + const auto& m = proto.indication_message(); + IndicationMessage s; + s.dapp_identifier = m.dapp_identifier(); + s.ran_function_identifier = m.ran_function_identifier(); + s.protocol_data = to_bytes(m.protocol_data()); + pdu.type = PduType::INDICATION_MESSAGE; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kDappControlAction: { + const auto& m = proto.dapp_control_action(); + DAppControlAction s; + s.dapp_identifier = m.dapp_identifier(); + s.ran_function_identifier = m.ran_function_identifier(); + s.control_identifier = m.control_identifier(); + s.action_data = to_bytes(m.action_data()); + pdu.type = PduType::DAPP_CONTROL_ACTION; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kDappReport: { + const auto& m = proto.dapp_report(); + DAppReport s; + s.dapp_identifier = m.dapp_identifier(); + s.ran_function_identifier = m.ran_function_identifier(); + s.report_data = to_bytes(m.report_data()); + pdu.type = PduType::DAPP_REPORT; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kXappControlAction: { + const auto& m = proto.xapp_control_action(); + XAppControlAction s; + s.dapp_identifier = m.dapp_identifier(); + s.ran_function_identifier = m.ran_function_identifier(); + s.xapp_control_data = to_bytes(m.xapp_control_data()); + pdu.type = PduType::XAPP_CONTROL_ACTION; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kReleaseMessage: { + const auto& m = proto.release_message(); + ReleaseMessage s; + s.dapp_identifier = m.dapp_identifier(); + pdu.type = PduType::RELEASE_MESSAGE; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::kMessageAck: { + const auto& m = proto.message_ack(); + MessageAck s; + s.request_id = m.request_id(); + s.response_code = from_pb_response_code(m.response_code()); + pdu.type = PduType::MESSAGE_ACK; + pdu.choice = std::move(s); + break; + } + case pb::E3Pdu::MSG_NOT_SET: + break; + } + return pdu; +} + +EncodeResult ProtobufE3Encoder::encode(const Pdu& pdu) { + pb::E3Pdu proto; + if (!pdu_to_proto(pdu, proto)) { + return tl::unexpected(ErrorCode::ENCODE_FAILED); + } + std::string serialized; + if (!proto.SerializeToString(&serialized)) { + return tl::unexpected(ErrorCode::ENCODE_FAILED); + } + return EncodedMessage{std::vector(serialized.begin(), serialized.end()), + EncodingFormat::PROTOBUF}; +} + +EncodeResult ProtobufE3Encoder::decode(const EncodedMessage& encoded) { + return decode(encoded.buffer.data(), encoded.buffer.size()); +} + +EncodeResult ProtobufE3Encoder::decode(const uint8_t* data, size_t size) { + if (data == nullptr && size != 0) { + return tl::unexpected(ErrorCode::DECODE_FAILED); + } + pb::E3Pdu proto; + if (!proto.ParseFromArray(data, static_cast(size))) { + return tl::unexpected(ErrorCode::DECODE_FAILED); + } + if (proto.msg_case() == pb::E3Pdu::MSG_NOT_SET) { + return tl::unexpected(ErrorCode::DECODE_FAILED); + } + return proto_to_pdu(proto); +} + +} // namespace libe3 diff --git a/src/encoder/protobuf_encoder.hpp b/src/encoder/protobuf_encoder.hpp new file mode 100644 index 0000000..c545625 --- /dev/null +++ b/src/encoder/protobuf_encoder.hpp @@ -0,0 +1,49 @@ +/** + * @file protobuf_encoder.hpp + * @brief Protocol Buffers encoder for E3AP PDUs + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef LIBE3_PROTOBUF_ENCODER_HPP +#define LIBE3_PROTOBUF_ENCODER_HPP + +#include "libe3/e3_encoder.hpp" + +// Forward declaration of the generated protobuf message (defined in e3ap-1.0.0.pb.h). +namespace libe3 { +namespace e3ap { +namespace v1 { +class E3Pdu; +} +} +} + +namespace libe3 { + +/** + * @brief Protocol Buffers encoder implementation + * + * Uses protoc-generated C++ code to encode/decode E3AP PDUs with Protocol + * Buffers wire format. + */ +class ProtobufE3Encoder : public E3Encoder { +public: + ProtobufE3Encoder() = default; + ~ProtobufE3Encoder() override = default; + + // E3Encoder interface + EncodeResult encode(const Pdu& pdu) override; + EncodeResult decode(const EncodedMessage& encoded) override; + EncodeResult decode(const uint8_t* data, size_t size) override; + EncodingFormat format() const noexcept override { return EncodingFormat::PROTOBUF; } + +private: + // Conversion helpers between the C++ Pdu struct and the generated message. + bool pdu_to_proto(const Pdu& pdu, e3ap::v1::E3Pdu& out) const; + Pdu proto_to_pdu(const e3ap::v1::E3Pdu& proto) const; +}; + +} // namespace libe3 + +#endif // LIBE3_PROTOBUF_ENCODER_HPP diff --git a/tests/integration/bench_encoding_size.cpp b/tests/integration/bench_encoding_size.cpp index b6ab1e4..2e58cb1 100644 --- a/tests/integration/bench_encoding_size.cpp +++ b/tests/integration/bench_encoding_size.cpp @@ -1,14 +1,28 @@ /** * @file bench_encoding_size.cpp - * @brief Measure the wire size (bytes) of each E3AP PDU type under every - * enabled encoding (ASN.1 APER, JSON; protobuf when available). + * @brief Measure the wire size (bytes) and encode/decode CPU cost of each + * E3AP PDU type under every enabled encoding (ASN.1 APER, JSON; + * protobuf when available). + * + * Timing uses Google Benchmark: iteration counts are auto-calibrated so + * sub-microsecond encode/decode operations are batched (amortizing the + * clock-read overhead), results are guarded with benchmark::DoNotOptimize + * to defeat dead-code elimination, and p50/p99 are computed across + * repetitions. Pin the process (e.g. `taskset -c 2`) and use the + * `performance` CPU governor for stable numbers; Google Benchmark warns on + * stderr when CPU frequency scaling is active. * * Outputs a CSV to stdout with columns: - * encoding, message_type, encoded_bytes + * encoding, message_type, encoded_bytes, info_bytes, + * encode_ns_p50, encode_ns_p99, decode_ns_p50, decode_ns_p99 + * + * `info_bytes` is the intrinsic information content of the sample PDU (sum + * of the raw sizes of the fields it carries), so encoded_bytes/info_bytes + * reads as the framing overhead of each encoding. * * Usage: * ./bench_encoding_size - * ./bench_encoding_size | python scripts/collect_baseline.py \ + * taskset -c 2 ./bench_encoding_size | python scripts/collect_baseline.py \ * --type encoding --output data/baseline/encoding/encoding.csv * * Representative values are used for each field; opaque payload fields @@ -21,16 +35,27 @@ #include #include + +#include + +#include #include #include #include +#include +#include #include #include using namespace libe3; +namespace { + +constexpr int kRepetitions = 20; +constexpr double kMinTimeSec = 0.02; + // --------------------------------------------------------------------------- -// Helpers +// Sample PDUs // --------------------------------------------------------------------------- // 64-byte representative payload for opaque binary fields (ASN.1 path). @@ -42,48 +67,52 @@ std::vector small_payload() { // JSON-formatted payload for IndicationMessage.protocol_data when encoding==JSON. // The JSON encoder embeds protocol_data as a nested JSON object (not hex). -// This represents a realistic Simple SM indication payload. +// This is the canonical Simple SM indication payload produced by the example +// wrapper's JSON branch. std::vector json_sm_payload() { const char* s = R"({"data1":42,"timestamp":1234567890})"; return std::vector(s, s + std::strlen(s)); } -// Encode one PDU with the given encoder and print the CSV row. -void measure(E3Encoder& enc, const char* encoding_name, - const char* msg_type, const Pdu& pdu) { - auto result = enc.encode(pdu); - if (!result.has_value()) { - std::fprintf(stderr, "WARN: encode failed for %s/%s\n", - encoding_name, msg_type); - return; - } - std::printf("%s,%s,%zu\n", - encoding_name, msg_type, result->buffer.size()); -} +// One sample PDU plus its intrinsic information content in bytes. +struct Sample { + const char* name; + Pdu pdu; + size_t info_bytes; + Sample(const char* n, PduType t) : name(n), pdu(t), info_bytes(0) {} +}; + +constexpr size_t kU32 = sizeof(uint32_t); +constexpr size_t kEnum = 1; // ResponseCode fits one byte of information +constexpr size_t kMsgId = kU32; -// Run all 11 PDU types through one encoder instance. -// json_indication: when true, use a JSON-formatted indication payload instead of -// raw bytes (the JSON encoder embeds protocol_data as a nested JSON object). -void run_encoder(E3Encoder& enc, const char* encoding_name, bool json_indication = false) { +// Build the 11 sample PDUs. json_indication: when true, use a JSON-formatted +// indication payload instead of raw bytes (the JSON encoder embeds +// protocol_data as a nested JSON object). +std::vector make_samples(bool json_indication) { + std::vector samples; auto payload = small_payload(); auto ind_payload = json_indication ? json_sm_payload() : payload; // 1. SetupRequest { - Pdu pdu(PduType::SETUP_REQUEST); + Sample s("SetupRequest", PduType::SETUP_REQUEST); SetupRequest req; req.e3ap_protocol_version = "1.0.0"; req.dapp_name = "BenchDApp"; req.dapp_version = "1.0.0"; req.vendor = "WinesLab"; - pdu.choice = req; - pdu.message_id = 1; - measure(enc, encoding_name, "SetupRequest", pdu); + s.pdu.choice = req; + s.pdu.message_id = 1; + s.info_bytes = kMsgId + req.e3ap_protocol_version.size() + + req.dapp_name.size() + req.dapp_version.size() + + req.vendor.size(); + samples.push_back(std::move(s)); } // 2. SetupResponse { - Pdu pdu(PduType::SETUP_RESPONSE); + Sample s("SetupResponse", PduType::SETUP_RESPONSE); SetupResponse resp; resp.request_id = 1; resp.response_code = ResponseCode::POSITIVE; @@ -96,14 +125,19 @@ void run_encoder(E3Encoder& enc, const char* encoding_name, bool json_indication rfdef.control_identifier_list = {1}; rfdef.ran_function_data = payload; resp.ran_function_list.push_back(rfdef); - pdu.choice = resp; - pdu.message_id = 2; - measure(enc, encoding_name, "SetupResponse", pdu); + s.pdu.choice = resp; + s.pdu.message_id = 2; + s.info_bytes = kMsgId + kU32 + kEnum + resp.ran_identifier.size() + + (resp.e3ap_protocol_version ? resp.e3ap_protocol_version->size() : 0) + kU32 + + (kU32 + kU32 * rfdef.telemetry_identifier_list.size() + + kU32 * rfdef.control_identifier_list.size() + + rfdef.ran_function_data.size()); + samples.push_back(std::move(s)); } // 3. SubscriptionRequest { - Pdu pdu(PduType::SUBSCRIPTION_REQUEST); + Sample s("SubscriptionRequest", PduType::SUBSCRIPTION_REQUEST); SubscriptionRequest req; req.dapp_identifier = 1; req.ran_function_identifier = 1; @@ -111,124 +145,273 @@ void run_encoder(E3Encoder& enc, const char* encoding_name, bool json_indication req.control_identifier_list = {1}; req.subscription_time = 0; req.periodicity = 1000; - pdu.choice = req; - pdu.message_id = 3; - measure(enc, encoding_name, "SubscriptionRequest", pdu); + s.pdu.choice = req; + s.pdu.message_id = 3; + s.info_bytes = kMsgId + kU32 + kU32 + + kU32 * req.telemetry_identifier_list.size() + + kU32 * req.control_identifier_list.size() + + kU32 + kU32; + samples.push_back(std::move(s)); } // 4. SubscriptionDelete { - Pdu pdu(PduType::SUBSCRIPTION_DELETE); + Sample s("SubscriptionDelete", PduType::SUBSCRIPTION_DELETE); SubscriptionDelete del; del.dapp_identifier = 1; del.subscription_id = 1; - pdu.choice = del; - pdu.message_id = 4; - measure(enc, encoding_name, "SubscriptionDelete", pdu); + s.pdu.choice = del; + s.pdu.message_id = 4; + s.info_bytes = kMsgId + kU32 + kU32; + samples.push_back(std::move(s)); } // 5. SubscriptionResponse { - Pdu pdu(PduType::SUBSCRIPTION_RESPONSE); + Sample s("SubscriptionResponse", PduType::SUBSCRIPTION_RESPONSE); SubscriptionResponse resp; resp.request_id = 3; resp.dapp_identifier = 1; resp.response_code = ResponseCode::POSITIVE; resp.subscription_id = 1; - pdu.choice = resp; - pdu.message_id = 5; - measure(enc, encoding_name, "SubscriptionResponse", pdu); + s.pdu.choice = resp; + s.pdu.message_id = 5; + s.info_bytes = kMsgId + kU32 + kU32 + kEnum + kU32; + samples.push_back(std::move(s)); } // 6. IndicationMessage { - Pdu pdu(PduType::INDICATION_MESSAGE); + Sample s("IndicationMessage", PduType::INDICATION_MESSAGE); IndicationMessage msg; msg.dapp_identifier = 1; msg.ran_function_identifier = 1; msg.protocol_data = ind_payload; - pdu.choice = msg; - pdu.message_id = 6; - measure(enc, encoding_name, "IndicationMessage", pdu); + s.pdu.choice = msg; + s.pdu.message_id = 6; + s.info_bytes = kMsgId + kU32 + kU32 + msg.protocol_data.size(); + samples.push_back(std::move(s)); } // 7. DAppControlAction { - Pdu pdu(PduType::DAPP_CONTROL_ACTION); + Sample s("DAppControlAction", PduType::DAPP_CONTROL_ACTION); DAppControlAction action; action.dapp_identifier = 1; action.ran_function_identifier = 1; action.control_identifier = 1; action.action_data = payload; - pdu.choice = action; - pdu.message_id = 7; - measure(enc, encoding_name, "DAppControlAction", pdu); + s.pdu.choice = action; + s.pdu.message_id = 7; + s.info_bytes = kMsgId + kU32 + kU32 + kU32 + action.action_data.size(); + samples.push_back(std::move(s)); } // 8. DAppReport { - Pdu pdu(PduType::DAPP_REPORT); + Sample s("DAppReport", PduType::DAPP_REPORT); DAppReport rep; rep.dapp_identifier = 1; rep.ran_function_identifier = 1; rep.report_data = payload; - pdu.choice = rep; - pdu.message_id = 8; - measure(enc, encoding_name, "DAppReport", pdu); + s.pdu.choice = rep; + s.pdu.message_id = 8; + s.info_bytes = kMsgId + kU32 + kU32 + rep.report_data.size(); + samples.push_back(std::move(s)); } // 9. XAppControlAction { - Pdu pdu(PduType::XAPP_CONTROL_ACTION); + Sample s("XAppControlAction", PduType::XAPP_CONTROL_ACTION); XAppControlAction xaction; xaction.dapp_identifier = 1; xaction.ran_function_identifier = 1; xaction.xapp_control_data = payload; - pdu.choice = xaction; - pdu.message_id = 9; - measure(enc, encoding_name, "XAppControlAction", pdu); + s.pdu.choice = xaction; + s.pdu.message_id = 9; + s.info_bytes = kMsgId + kU32 + kU32 + xaction.xapp_control_data.size(); + samples.push_back(std::move(s)); } // 10. ReleaseMessage { - Pdu pdu(PduType::RELEASE_MESSAGE); + Sample s("ReleaseMessage", PduType::RELEASE_MESSAGE); ReleaseMessage rel; rel.dapp_identifier = 1; - pdu.choice = rel; - pdu.message_id = 10; - measure(enc, encoding_name, "ReleaseMessage", pdu); + s.pdu.choice = rel; + s.pdu.message_id = 10; + s.info_bytes = kMsgId + kU32; + samples.push_back(std::move(s)); } // 11. MessageAck { - Pdu pdu(PduType::MESSAGE_ACK); + Sample s("MessageAck", PduType::MESSAGE_ACK); MessageAck ack; ack.request_id = 1; ack.response_code = ResponseCode::POSITIVE; - pdu.choice = ack; - pdu.message_id = 11; - measure(enc, encoding_name, "MessageAck", pdu); + s.pdu.choice = ack; + s.pdu.message_id = 11; + s.info_bytes = kMsgId + kU32 + kEnum; + samples.push_back(std::move(s)); } + + return samples; } +// --------------------------------------------------------------------------- +// Google Benchmark plumbing // --------------------------------------------------------------------------- -int main() { - std::printf("encoding,message_type,encoded_bytes\n"); +double stat_percentile(std::vector v, double p) { + if (v.empty()) return 0.0; + std::sort(v.begin(), v.end()); + size_t idx = static_cast(p * static_cast(v.size() - 1)); + return v[idx]; +} -#ifdef LIBE3_ENABLE_ASN1 - { - auto enc = create_encoder(EncodingFormat::ASN1); - if (enc) run_encoder(*enc, "asn1"); +double stat_p50(const std::vector& v) { return stat_percentile(v, 0.50); } +double stat_p99(const std::vector& v) { return stat_percentile(v, 0.99); } + +// One (encoding, PDU) measurement cell: pre-encoded buffer for the decode +// direction plus the CSV size columns. +struct Cell { + std::string encoding; // CSV encoding name (asn1/json/protobuf) + std::string message; // PDU type name + size_t encoded_bytes = 0; + size_t info_bytes = 0; + E3Encoder* enc = nullptr; + Pdu pdu{PduType::MESSAGE_ACK}; + EncodedMessage encoded; +}; + +// Captures the p50/p99 aggregate rows emitted by Google Benchmark instead of +// printing a console report, so the process output stays pure CSV. +class CaptureReporter : public benchmark::BenchmarkReporter { +public: + bool ReportContext(const Context&) override { return true; } + void ReportRuns(const std::vector& runs) override { + for (const auto& run : runs) { + if (run.skipped) continue; + if (run.run_type != Run::RT_Aggregate) continue; + if (run.aggregate_name != "p50" && run.aggregate_name != "p99") continue; + results_[run.run_name.function_name][run.aggregate_name] = + run.GetAdjustedRealTime(); + } } -#endif -#ifdef LIBE3_ENABLE_JSON - { - auto enc = create_encoder(EncodingFormat::JSON); - if (enc) run_encoder(*enc, "json", /*json_indication=*/true); + double get(const std::string& bench, const std::string& stat) const { + auto it = results_.find(bench); + if (it == results_.end()) return 0.0; + auto jt = it->second.find(stat); + return jt == it->second.end() ? 0.0 : jt->second; + } + +private: + std::map> results_; +}; + +void register_cell(Cell& cell) { + E3Encoder* enc = cell.enc; + const Pdu* pdu = &cell.pdu; + const EncodedMessage* encoded = &cell.encoded; + + auto* b_enc = benchmark::RegisterBenchmark( + ("encode/" + cell.encoding + "/" + cell.message).c_str(), + [enc, pdu](benchmark::State& state) { + for (auto _ : state) { + auto result = enc->encode(*pdu); + benchmark::DoNotOptimize(result); + } + }); + auto* b_dec = benchmark::RegisterBenchmark( + ("decode/" + cell.encoding + "/" + cell.message).c_str(), + [enc, encoded](benchmark::State& state) { + for (auto _ : state) { + auto result = enc->decode(*encoded); + benchmark::DoNotOptimize(result); + } + }); + for (auto* b : {b_enc, b_dec}) { + b->Unit(benchmark::kNanosecond) + ->MinTime(kMinTimeSec) + ->Repetitions(kRepetitions) + ->ComputeStatistics("p50", stat_p50) + ->ComputeStatistics("p99", stat_p99) + ->ReportAggregatesOnly(true); } +} + +} // namespace + +// --------------------------------------------------------------------------- + +int main(int argc, char* argv[]) { + benchmark::Initialize(&argc, argv); + + struct EncoderCase { + EncodingFormat format; + const char* name; + bool json_indication; + }; + std::vector encoder_cases; +#ifdef LIBE3_ENABLE_ASN1 + encoder_cases.push_back({EncodingFormat::ASN1, "asn1", false}); #endif +#ifdef LIBE3_ENABLE_JSON + encoder_cases.push_back({EncodingFormat::JSON, "json", true}); +#endif +#ifdef LIBE3_ENABLE_PROTOBUF + encoder_cases.push_back({EncodingFormat::PROTOBUF, "protobuf", false}); +#endif + + std::vector> encoders; + std::vector cells; + for (const auto& ec : encoder_cases) { + auto enc = create_encoder(ec.format); + if (!enc) { + std::fprintf(stderr, "WARN: no encoder for %s\n", ec.name); + continue; + } + for (auto& sample : make_samples(ec.json_indication)) { + auto result = enc->encode(sample.pdu); + if (!result.has_value()) { + std::fprintf(stderr, "WARN: encode failed for %s/%s\n", + ec.name, sample.name); + continue; + } + Cell cell; + cell.encoding = ec.name; + cell.message = sample.name; + cell.encoded_bytes = result->buffer.size(); + cell.info_bytes = sample.info_bytes; + cell.enc = enc.get(); + cell.pdu = sample.pdu; + cell.encoded = *result; + cells.push_back(std::move(cell)); + } + encoders.push_back(std::move(enc)); + } + + // Register after `cells` is fully built: the benchmark lambdas capture + // pointers into the vector, which must not reallocate afterwards. + for (auto& cell : cells) register_cell(cell); + + CaptureReporter capture; + benchmark::RunSpecifiedBenchmarks(&capture); + + std::printf("encoding,message_type,encoded_bytes,info_bytes," + "encode_ns_p50,encode_ns_p99,decode_ns_p50,decode_ns_p99\n"); + for (const auto& cell : cells) { + const std::string enc_key = "encode/" + cell.encoding + "/" + cell.message; + const std::string dec_key = "decode/" + cell.encoding + "/" + cell.message; + std::printf("%s,%s,%zu,%zu,%.1f,%.1f,%.1f,%.1f\n", + cell.encoding.c_str(), cell.message.c_str(), + cell.encoded_bytes, cell.info_bytes, + capture.get(enc_key, "p50"), capture.get(enc_key, "p99"), + capture.get(dec_key, "p50"), capture.get(dec_key, "p99")); + } + benchmark::Shutdown(); return 0; } diff --git a/tests/integration/bench_full_loop_latency.cpp b/tests/integration/bench_full_loop_latency.cpp index f949b98..54bb4c5 100644 --- a/tests/integration/bench_full_loop_latency.cpp +++ b/tests/integration/bench_full_loop_latency.cpp @@ -89,7 +89,7 @@ struct SharedTraces { class BenchSM : public ServiceModel { public: - explicit BenchSM(SharedTraces& s) : shared_(s) {} + BenchSM(SharedTraces& s, EncodingFormat enc) : shared_(s), enc_(enc) {} std::string name() const override { return "BENCH"; } uint32_t version() const override { return 1; } uint32_t ran_function_id() const override { return kRanFunctionId; } @@ -101,7 +101,7 @@ class BenchSM : public ServiceModel { std::vector ran_function_data() const override { std::vector out; - if (libe3_examples::encode_ran_function_data("BENCH", out)) return out; + if (libe3_examples::encode_ran_function_data("BENCH", out, enc_)) return out; return {0x01}; } @@ -137,7 +137,7 @@ class BenchSM : public ServiceModel { // Phase 2: encode int64_t t2 = now_ns(); std::vector enc; - if (!libe3_examples::encode_simple_indication(si, enc)) continue; + if (!libe3_examples::encode_simple_indication(si, enc, enc_)) continue; int64_t t3 = now_ns(); { std::lock_guard lk(shared_.mu); @@ -180,7 +180,7 @@ class BenchSM : public ServiceModel { shared_.current.t8_recv_control = t8; } int sampling = 0; - (void)libe3_examples::decode_simple_control(a.action_data, sampling); + (void)libe3_examples::decode_simple_control(a.action_data, sampling, enc_); int64_t t9 = now_ns(); { std::lock_guard lk(shared_.mu); @@ -198,6 +198,7 @@ class BenchSM : public ServiceModel { private: SharedTraces& shared_; + EncodingFormat enc_; std::atomic running_{false}; std::thread worker_; std::mutex emit_mu_; @@ -229,17 +230,19 @@ E3TransportLayer parse_transport(const char* s) { } EncodingFormat parse_encoding(const char* s) { - if (std::strcmp(s, "json") == 0) return EncodingFormat::JSON; - if (std::strcmp(s, "asn1") == 0) return EncodingFormat::ASN1; + if (std::strcmp(s, "json") == 0) return EncodingFormat::JSON; + if (std::strcmp(s, "asn1") == 0) return EncodingFormat::ASN1; + if (std::strcmp(s, "protobuf") == 0) return EncodingFormat::PROTOBUF; std::fprintf(stderr, "Unknown encoding '%s'; using asn1\n", s); return EncodingFormat::ASN1; } const char* encoding_str(EncodingFormat e) { switch (e) { - case EncodingFormat::JSON: return "JSON"; - case EncodingFormat::ASN1: return "ASN.1 APER"; - default: return "unknown"; + case EncodingFormat::JSON: return "JSON"; + case EncodingFormat::ASN1: return "ASN.1 APER"; + case EncodingFormat::PROTOBUF: return "Protocol Buffers"; + default: return "unknown"; } } @@ -280,7 +283,7 @@ int main(int argc, char* argv[]) { case 'e': encoding = parse_encoding(optarg); break; case 'h': std::printf("Usage: %s [--link zmq|posix] [--transport ipc|tcp|sctp]" - " [--encoding asn1|json]\n", argv[0]); + " [--encoding asn1|json|protobuf]\n", argv[0]); return 0; default: std::fprintf(stderr, "Unknown option; use --help\n"); @@ -316,7 +319,7 @@ int main(int argc, char* argv[]) { SharedTraces shared; E3Agent ran(ran_cfg); - auto* sm = new BenchSM(shared); + auto* sm = new BenchSM(shared, encoding); if (ran.register_sm(std::unique_ptr(sm)) != ErrorCode::SUCCESS) { std::cerr << "register_sm failed\n"; return 1; @@ -330,12 +333,12 @@ int main(int argc, char* argv[]) { dapp.set_indication_handler([&](const IndicationMessage& msg) { int64_t t4 = now_ns(); libe3_examples::SimpleIndication si; - if (!libe3_examples::decode_simple_indication(msg.protocol_data, si)) return; + if (!libe3_examples::decode_simple_indication(msg.protocol_data, si, encoding)) return; int64_t t5 = now_ns(); int64_t t6 = now_ns(); std::vector ctrl; - if (!libe3_examples::encode_simple_control(static_cast(si.data1 % 101), ctrl)) return; + if (!libe3_examples::encode_simple_control(static_cast(si.data1 % 101), ctrl, encoding)) return; int64_t t7 = now_ns(); { diff --git a/tests/test_framework.hpp b/tests/test_framework.hpp index 5dba672..7b1763b 100644 --- a/tests/test_framework.hpp +++ b/tests/test_framework.hpp @@ -85,6 +85,7 @@ inline int run_all_tests() { try { test.func(); std::cout << "[PASS] " << test.name << "\n"; + report_pass(test.name); } catch (const std::exception& e) { std::cout << "[FAIL] " << test.name << "\n"; std::cout << " Exception: " << e.what() << "\n"; diff --git a/tests/test_protobuf_encoder.cpp b/tests/test_protobuf_encoder.cpp new file mode 100644 index 0000000..9eaf007 --- /dev/null +++ b/tests/test_protobuf_encoder.cpp @@ -0,0 +1,351 @@ +/** + * @file test_protobuf_encoder.cpp + * @brief Unit tests for the Protocol Buffers encoder + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "test_framework.hpp" +#include "libe3/e3_encoder.hpp" +#include "libe3/types.hpp" + +#include + +using namespace libe3; + +// Get a protobuf encoder instance. +static std::unique_ptr make_encoder() { + return create_encoder(EncodingFormat::PROTOBUF); +} + +// A byte pattern covering every value 0..255 (exercises binary integrity, +// including embedded NUL bytes that a hex/JSON path would have to escape). +static std::vector all_bytes() { + std::vector v(256); + for (size_t i = 0; i < v.size(); ++i) v[i] = static_cast(i); + return v; +} + +TEST(ProtobufEncoder_available) { + auto encoder = make_encoder(); + ASSERT_TRUE(encoder != nullptr); + ASSERT_TRUE(encoder->format() == EncodingFormat::PROTOBUF); +} + +TEST(ProtobufEncoder_setup_request_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::SETUP_REQUEST); + SetupRequest req; + req.e3ap_protocol_version = "1.0.0"; + req.dapp_name = "MyDApp"; + req.dapp_version = "1.2.3"; + req.vendor = "MyVendor"; + original.choice = req; + original.message_id = 12345; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + ASSERT_FALSE(encoded->buffer.empty()); + ASSERT_TRUE(encoded->format == EncodingFormat::PROTOBUF); + + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + ASSERT_TRUE(decoded->type == PduType::SETUP_REQUEST); + ASSERT_EQ(decoded->message_id, 12345u); + + auto& r = std::get(decoded->choice); + ASSERT_STREQ(r.e3ap_protocol_version.c_str(), "1.0.0"); + ASSERT_STREQ(r.dapp_name.c_str(), "MyDApp"); + ASSERT_STREQ(r.dapp_version.c_str(), "1.2.3"); + ASSERT_STREQ(r.vendor.c_str(), "MyVendor"); +} + +TEST(ProtobufEncoder_setup_response_roundtrip_full) { + auto encoder = make_encoder(); + Pdu original(PduType::SETUP_RESPONSE); + SetupResponse resp; + resp.request_id = 7; + resp.response_code = ResponseCode::POSITIVE; + resp.e3ap_protocol_version = "1.0.0"; + resp.dapp_identifier = 42; + resp.ran_identifier = "ran-001"; + RanFunctionDef rf; + rf.ran_function_identifier = 1; + rf.telemetry_identifier_list = {1, 2, 3}; + rf.control_identifier_list = {10, 20}; + rf.ran_function_data = all_bytes(); + resp.ran_function_list.push_back(rf); + original.choice = resp; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + ASSERT_TRUE(decoded->type == PduType::SETUP_RESPONSE); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.request_id, 7u); + ASSERT_TRUE(r.response_code == ResponseCode::POSITIVE); + ASSERT_TRUE(r.e3ap_protocol_version.has_value()); + ASSERT_STREQ(r.e3ap_protocol_version->c_str(), "1.0.0"); + ASSERT_TRUE(r.dapp_identifier.has_value()); + ASSERT_EQ(*r.dapp_identifier, 42u); + ASSERT_STREQ(r.ran_identifier.c_str(), "ran-001"); + ASSERT_EQ(r.ran_function_list.size(), 1u); + ASSERT_EQ(r.ran_function_list[0].ran_function_identifier, 1u); + ASSERT_EQ(r.ran_function_list[0].telemetry_identifier_list.size(), 3u); + ASSERT_EQ(r.ran_function_list[0].control_identifier_list.size(), 2u); + ASSERT_TRUE(r.ran_function_list[0].ran_function_data == all_bytes()); +} + +TEST(ProtobufEncoder_setup_response_roundtrip_optionals_absent) { + auto encoder = make_encoder(); + Pdu original(PduType::SETUP_RESPONSE); + SetupResponse resp; + resp.request_id = 9; + resp.response_code = ResponseCode::NEGATIVE; + resp.ran_identifier = "ran-x"; + // e3ap_protocol_version, dapp_identifier, ran_function_list all empty/absent + original.choice = resp; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_TRUE(r.response_code == ResponseCode::NEGATIVE); + ASSERT_FALSE(r.e3ap_protocol_version.has_value()); + ASSERT_FALSE(r.dapp_identifier.has_value()); + ASSERT_EQ(r.ran_function_list.size(), 0u); +} + +TEST(ProtobufEncoder_subscription_request_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::SUBSCRIPTION_REQUEST); + SubscriptionRequest req; + req.dapp_identifier = 42; + req.ran_function_identifier = 100; + req.telemetry_identifier_list = {1, 2, 3}; + req.control_identifier_list = {10, 20}; + req.subscription_time = 3600; + req.periodicity = 500; + original.choice = req; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 42u); + ASSERT_EQ(r.ran_function_identifier, 100u); + ASSERT_TRUE(r.telemetry_identifier_list == std::vector({1, 2, 3})); + ASSERT_TRUE(r.control_identifier_list == std::vector({10, 20})); + ASSERT_TRUE(r.subscription_time.has_value()); + ASSERT_EQ(*r.subscription_time, 3600u); + ASSERT_TRUE(r.periodicity.has_value()); + ASSERT_EQ(*r.periodicity, 500u); +} + +TEST(ProtobufEncoder_subscription_request_optionals_absent) { + auto encoder = make_encoder(); + Pdu original(PduType::SUBSCRIPTION_REQUEST); + SubscriptionRequest req; + req.dapp_identifier = 1; + req.ran_function_identifier = 1; + original.choice = req; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_FALSE(r.subscription_time.has_value()); + ASSERT_FALSE(r.periodicity.has_value()); +} + +TEST(ProtobufEncoder_subscription_delete_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::SUBSCRIPTION_DELETE); + SubscriptionDelete del; + del.dapp_identifier = 42; + del.subscription_id = 77; + original.choice = del; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 42u); + ASSERT_EQ(r.subscription_id, 77u); +} + +TEST(ProtobufEncoder_subscription_response_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::SUBSCRIPTION_RESPONSE); + SubscriptionResponse resp; + resp.request_id = 3; + resp.dapp_identifier = 42; + resp.response_code = ResponseCode::POSITIVE; + resp.subscription_id = 5; + original.choice = resp; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.request_id, 3u); + ASSERT_EQ(r.dapp_identifier, 42u); + ASSERT_TRUE(r.response_code == ResponseCode::POSITIVE); + ASSERT_TRUE(r.subscription_id.has_value()); + ASSERT_EQ(*r.subscription_id, 5u); +} + +TEST(ProtobufEncoder_indication_roundtrip_binary) { + auto encoder = make_encoder(); + Pdu original(PduType::INDICATION_MESSAGE); + IndicationMessage msg; + msg.dapp_identifier = 1; + msg.ran_function_identifier = 2; + msg.protocol_data = all_bytes(); + original.choice = msg; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 1u); + ASSERT_EQ(r.ran_function_identifier, 2u); + ASSERT_TRUE(r.protocol_data == all_bytes()); +} + +TEST(ProtobufEncoder_dapp_control_action_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::DAPP_CONTROL_ACTION); + DAppControlAction act; + act.dapp_identifier = 1; + act.ran_function_identifier = 2; + act.control_identifier = 3; + act.action_data = {0xDE, 0xAD, 0x00, 0xBE, 0xEF}; + original.choice = act; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.control_identifier, 3u); + ASSERT_TRUE(r.action_data == std::vector({0xDE, 0xAD, 0x00, 0xBE, 0xEF})); +} + +TEST(ProtobufEncoder_dapp_report_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::DAPP_REPORT); + DAppReport rep; + rep.dapp_identifier = 4; + rep.ran_function_identifier = 5; + rep.report_data = {1, 2, 3, 0, 4}; + original.choice = rep; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 4u); + ASSERT_EQ(r.ran_function_identifier, 5u); + ASSERT_TRUE(r.report_data == std::vector({1, 2, 3, 0, 4})); +} + +TEST(ProtobufEncoder_xapp_control_action_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::XAPP_CONTROL_ACTION); + XAppControlAction act; + act.dapp_identifier = 6; + act.ran_function_identifier = 7; + act.xapp_control_data = {9, 8, 7}; + original.choice = act; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 6u); + ASSERT_EQ(r.ran_function_identifier, 7u); + ASSERT_TRUE(r.xapp_control_data == std::vector({9, 8, 7})); +} + +TEST(ProtobufEncoder_release_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::RELEASE_MESSAGE); + ReleaseMessage rel; + rel.dapp_identifier = 55; + original.choice = rel; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.dapp_identifier, 55u); +} + +TEST(ProtobufEncoder_message_ack_roundtrip) { + auto encoder = make_encoder(); + Pdu original(PduType::MESSAGE_ACK); + MessageAck ack; + ack.request_id = 123; + ack.response_code = ResponseCode::NEGATIVE; + original.choice = ack; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + + auto& r = std::get(decoded->choice); + ASSERT_EQ(r.request_id, 123u); + ASSERT_TRUE(r.response_code == ResponseCode::NEGATIVE); +} + +TEST(ProtobufEncoder_envelope_preserved) { + auto encoder = make_encoder(); + Pdu original(PduType::RELEASE_MESSAGE); + ReleaseMessage rel; + rel.dapp_identifier = 1; + original.choice = rel; + original.message_id = 999; + original.timestamp = 1700000000123ULL; + + auto encoded = encoder->encode(original); + ASSERT_TRUE(encoded.has_value()); + auto decoded = encoder->decode(*encoded); + ASSERT_TRUE(decoded.has_value()); + ASSERT_EQ(decoded->message_id, 999u); + ASSERT_EQ(decoded->timestamp, 1700000000123ULL); +} + +TEST(ProtobufEncoder_reject_garbage) { + auto encoder = make_encoder(); + // Bytes that do not decode to a valid E3Pdu with a set oneof. + std::vector garbage = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + auto result = encoder->decode(garbage.data(), garbage.size()); + ASSERT_FALSE(result.has_value()); +} + +int main() { + return RUN_ALL_TESTS(); +}