libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
e3_interface.cpp
Go to the documentation of this file.
1/**
2 * @file e3_interface.cpp
3 * @brief E3Interface implementation
4 *
5 * Ported from the original C implementation's e3_agent.c
6 *
7 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
8 * SPDX-License-Identifier: Apache-2.0
9 */
10
12#include "libe3/logger.hpp"
13#include "libe3/latrec.h"
14#include <cctype>
15#include <cerrno>
16#include <chrono>
17#include <cstdio>
18#include <cstdlib>
19#include <cstring>
20#include <signal.h>
21#include <string>
22
23#ifdef __linux__
24#include <pthread.h>
25#include <sched.h>
26#include <sys/resource.h>
27#include <sys/syscall.h>
28#include <unistd.h>
29#endif
30
31namespace libe3 {
32
33namespace {
34constexpr const char* LOG_TAG = "E3Iface";
35
36/**
37 * @brief Build the default log-file path for a given role/identifier.
38 *
39 * Produces `/tmp/e3_<role>[_<sanitized-id>]_<euid>.log`. The id is sanitized
40 * to `[A-Za-z0-9._-]` (other bytes become '_') and omitted when empty. The
41 * effective uid is appended so the file is always owned by the opener,
42 * sidestepping `fs.protected_regular` denials in sticky /tmp.
43 */
44std::string default_log_path(E3Role role, const std::string& id) {
45 std::string san;
46 san.reserve(id.size());
47 for (char c : id) {
48 san.push_back((std::isalnum(static_cast<unsigned char>(c)) ||
49 c == '.' || c == '_' || c == '-')
50 ? c
51 : '_');
52 }
53 std::string path = "/tmp/e3_";
54 path += (role == E3Role::DAPP) ? "dapp" : "agent";
55 if (!san.empty()) {
56 path += '_';
57 path += san;
58 }
59#ifdef __linux__
60 path += '_';
61 path += std::to_string(static_cast<unsigned long>(geteuid()));
62#endif
63 path += ".log";
64 return path;
65}
66
67/**
68 * @brief Apply CPU-affinity and niceness to the calling thread, then open its
69 * latrec ring.
70 *
71 * Invoked at the very start of each I/O thread so that the settings take
72 * effect before any real work begins.
73 *
74 * @param affinity Logical CPU core to pin to, or -1 to skip pinning.
75 * @param niceness Nice value in [-20, 19], or 0 to skip.
76 * @param role latrec ring name for this thread (see LATREC_ROLE_* below).
77 */
78void apply_thread_config(int affinity, int niceness, const char* role) noexcept {
79#ifdef __linux__
80 if (affinity >= 0) {
81 cpu_set_t cpuset;
82 CPU_ZERO(&cpuset);
83 CPU_SET(static_cast<size_t>(affinity), &cpuset);
84 // Non-fatal, but say so: a silent miss looks identical to a working pin.
85 int rc = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
86 if (rc != 0) {
87 E3_LOG_WARN(LOG_TAG) << "Failed to pin " << role << " thread to core "
88 << affinity << ": " << std::strerror(rc)
89 << " (thread left unpinned)";
90 } else {
91 E3_LOG_INFO(LOG_TAG) << "Pinned " << role << " thread to core " << affinity;
92 }
93 }
94 if (niceness != 0) {
95 // Use the kernel TID so only this thread's scheduling priority changes.
96 // syscall(SYS_gettid) returns the kernel thread ID as a long; cast it
97 // to pid_t first, then to the id_t expected by setpriority(PRIO_PROCESS).
98 auto tid = static_cast<id_t>(static_cast<pid_t>(syscall(SYS_gettid)));
99 // Non-fatal: negative values require CAP_SYS_NICE.
100 if (setpriority(PRIO_PROCESS, tid, niceness) == -1) {
101 E3_LOG_WARN(LOG_TAG) << "Failed to set niceness " << niceness << " on "
102 << role << " thread: " << std::strerror(errno);
103 }
104 }
105#else
106 (void)affinity;
107 (void)niceness;
108#endif
109 // Open this thread's latrec ring up front: the stamp path never opens one,
110 // and doing it after pinning faults the mapping on the local NUMA node.
111 latrec_tls_open_as(role);
112}
113
114/* latrec ring names for the threads libe3 starts; an unnamed ring falls back to
115 * the program name, which is the same for all of them. They also select the
116 * per-role LATREC_ENTRIES_LOG2_<ROLE> sizing override. The RAN and dApp roles
117 * run in separate processes, so the two sides reuse one name per function. */
118constexpr const char* LATREC_ROLE_OUTBOUND = "libe3.outbound";
119constexpr const char* LATREC_ROLE_INBOUND = "libe3.inbound";
120constexpr const char* LATREC_ROLE_REPORT = "libe3.report";
121constexpr const char* LATREC_ROLE_SETUP = "libe3.setup";
122#ifdef LIBE3_ENABLE_LATREC
123constexpr const char* LATREC_ROLE_CONTEXT = "libe3.context";
124#endif
125} // anonymous namespace
126
128 // Monotonic over the ASN.1 E3-MessageID (1..4294967295) range, skipping 0
129 // (the grammar's lower bound is 1). A 32-bit counter does not wrap within
130 // any realistic run, so an id identifies a request uniquely rather than
131 // only across the in-flight window.
132 const uint64_t n = next_message_id_.fetch_add(1, std::memory_order_relaxed);
133 return static_cast<uint32_t>(n % 0xFFFFFFFFull) + 1;
134}
135
136void E3Interface::remember_subscription_op(uint32_t request_id,
137 uint32_t ran_function_id,
138 bool is_delete) {
139 std::lock_guard<std::mutex> lk(pending_subscriptions_mutex_);
140 pending_subscriptions_[request_id] = PendingSubscriptionOp{ran_function_id, is_delete};
141}
142
143void E3Interface::forget_subscription_op(uint32_t request_id) {
144 std::lock_guard<std::mutex> lk(pending_subscriptions_mutex_);
145 pending_subscriptions_.erase(request_id);
146}
147
148uint32_t E3Interface::sanitize_request_message_id(uint32_t request_id) {
149 // E3-MessageID is 1..4294967295: every non-zero id can be echoed, a peer
150 // that counts across message types (Aerial) passes 1000 within seconds.
151 if (request_id != 0) {
152 return request_id;
153 }
154 return generate_message_id();
155}
156
158 : config_(config)
159{
160 // Suppress shared log-file writes when logging is disabled. This lets
161 // multiple E3Interface instances (e.g. a RAN-role and a dApp-role one in
162 // the same process — integration tests, the latency benchmark, or
163 // production multi-peer dApps) coexist without racing on the same file.
164 if (config.log_level > 0) {
165 std::string log_path = config.log_path;
166 if (log_path.empty()) {
167 const std::string& id = (config.role == E3Role::DAPP)
168 ? config.dapp_name
169 : config.ran_identifier;
170 log_path = default_log_path(config.role, id);
171 }
172 Logger::instance().set_log_file(log_path);
173 }
174 Logger::instance().set_level(config.log_level);
175 E3_LOG_INFO(LOG_TAG) << "E3Interface created (role=" << role_to_string(config.role) << ")";
176}
177
179 stop();
180 E3_LOG_INFO(LOG_TAG) << "E3Interface destroyed";
181}
182
184 if (state_.load() != AgentState::UNINITIALIZED) {
185 E3_LOG_WARN(LOG_TAG) << "Interface already initialized";
186 return ErrorCode::ALREADY_INITIALIZED;
187 }
188
189 E3_LOG_INFO(LOG_TAG) << "Initializing E3Interface";
190
191 // Create encoder
192 encoder_ = create_encoder(config_.encoding);
193 if (!encoder_) {
194 E3_LOG_ERROR(LOG_TAG) << "Failed to create encoder";
195 return ErrorCode::INTERNAL_ERROR;
196 }
197
198 // Allocate role-specific state. Exactly one is non-null.
199 if (config_.role == E3Role::RAN) {
200 subscription_manager_ = std::make_unique<SubscriptionManager>();
201 subscription_manager_->set_sm_lifecycle_callback(
202 [this](uint32_t ran_function_id, bool should_start) {
203 on_sm_lifecycle_change(ran_function_id, should_start);
204 }
205 );
206 } else {
207 dapp_state_ = std::make_unique<DAppSubscriptionState>();
208 }
209
210 // Create response queue
211 response_queue_ = std::make_unique<LockFreeQueue<Pdu>>();
212
213 // Create dApp-report queue (RAN side drains it via the report worker)
214 report_queue_ = std::make_unique<LockFreeQueue<DAppReport>>(1024);
215
216 // Create connector
217 connector_ = create_connector(
218 config_.link_layer,
219 config_.transport_layer,
220 config_.setup_endpoint,
221 config_.subscriber_endpoint,
222 config_.publisher_endpoint,
223 config_.setup_port,
224 config_.subscriber_port,
225 config_.publisher_port,
226 config_.io_threads,
227 config_.role
228 );
229
230 if (!connector_) {
231 E3_LOG_ERROR(LOG_TAG) << "Failed to create connector";
232 return ErrorCode::INTERNAL_ERROR;
233 }
234
235 state_.store(AgentState::INITIALIZED);
236 E3_LOG_INFO(LOG_TAG) << "E3Interface initialized successfully";
237
238 return ErrorCode::SUCCESS;
239}
240
243 if (!state_.compare_exchange_strong(expected, AgentState::CONNECTING)) {
244 E3_LOG_ERROR(LOG_TAG) << "Cannot start from state: "
245 << agent_state_to_string(expected);
246 return ErrorCode::STATE_ERROR;
247 }
248
249 E3_LOG_INFO(LOG_TAG) << "Starting E3Interface (role=" << role_to_string(config_.role) << ")";
250 should_stop_.store(false);
251
252 // Set up the setup channel. RAN binds; dApp connects.
253 ErrorCode conn_result = (config_.role == E3Role::RAN)
254 ? connector_->setup_initial_connection()
255 : connector_->setup_initial_connection_client();
256 if (conn_result != ErrorCode::SUCCESS) {
257 E3_LOG_ERROR(LOG_TAG) << "Failed to setup initial connection: "
258 << error_code_to_string(conn_result);
259 state_.store(AgentState::ERROR);
260 return conn_result;
261 }
262
263 state_.store(AgentState::CONNECTED);
264
265 // Start threads. RAN runs setup+inbound+outbound plus a report worker; dApp
266 // runs setup+inbound+outbound (dApps host no SMs and receive no reports).
267 if (config_.role == E3Role::RAN) {
268 setup_thread_ = std::make_unique<std::thread>(&E3Interface::setup_loop_ran, this);
269 inbound_thread_ = std::make_unique<std::thread>(&E3Interface::inbound_loop_ran, this);
270 outbound_thread_ = std::make_unique<std::thread>(&E3Interface::outbound_loop_ran, this);
271 // RAN receives dApp reports; drain them off the inbound thread.
272 report_worker_thread_ = std::make_unique<std::thread>(&E3Interface::report_worker_loop, this);
273 } else {
274 setup_thread_ = std::make_unique<std::thread>(&E3Interface::setup_loop_dapp, this);
275 inbound_thread_ = std::make_unique<std::thread>(&E3Interface::inbound_loop_dapp, this);
276 outbound_thread_ = std::make_unique<std::thread>(&E3Interface::outbound_loop_dapp, this);
277 }
278
279#ifdef LIBE3_ENABLE_LATREC
280 context_thread_ = std::make_unique<std::thread>(&E3Interface::context_monitor_loop, this);
281#endif
282
283 state_.store(AgentState::RUNNING);
284 E3_LOG_INFO(LOG_TAG) << "E3Interface started successfully";
285
286 return ErrorCode::SUCCESS;
287}
288
290 if (state_.load() == AgentState::UNINITIALIZED ||
291 state_.load() == AgentState::STOPPING) {
292 return;
293 }
294
295 E3_LOG_INFO(LOG_TAG) << "Stopping E3Interface";
296 state_.store(AgentState::STOPPING);
297 should_stop_.store(true);
298
299 // Wake up response queue
300 if (response_queue_) {
301 response_queue_->shutdown();
302 }
303
304 // Wake up the report queue so the report worker's blocking pop returns.
305 if (report_queue_) {
306 report_queue_->shutdown();
307 }
308
309 // Interrupt blocking socket operations
310 if (connector_) {
311 connector_->shutdown();
312 }
313
314 // Join threads
315 if (setup_thread_ && setup_thread_->joinable()) {
316 setup_thread_->join();
317 }
318 if (inbound_thread_ && inbound_thread_->joinable()) {
319 inbound_thread_->join();
320 }
321 if (outbound_thread_ && outbound_thread_->joinable()) {
322 outbound_thread_->join();
323 }
324 if (report_worker_thread_ && report_worker_thread_->joinable()) {
325 report_worker_thread_->join();
326 }
327#ifdef LIBE3_ENABLE_LATREC
328 if (context_thread_ && context_thread_->joinable()) {
329 context_thread_->join();
330 }
331#endif
332
333 // Wake up anyone blocked in wait_for_setup so they don't hang.
334 {
335 std::lock_guard<std::mutex> lk(setup_complete_mu_);
336 setup_complete_ = true;
337 }
338 setup_complete_cv_.notify_all();
339
340 // Clean up SM registry — only the RAN role owns SMs. Doing this
341 // unconditionally would wipe a sibling RAN-role E3Interface's registered
342 // SMs in a two-roles-in-one-process scenario (integration tests, the
343 // latency benchmark, multi-peer dApps colocated with a RAN). The dApp
344 // role never registers an SM, so there's nothing to clear.
345 if (config_.role == E3Role::RAN) {
346 SmRegistry::instance().clear();
347 }
348
349 // Dispose connector
350 if (connector_) {
351 connector_->dispose();
352 }
353
354 state_.store(AgentState::INITIALIZED);
355 E3_LOG_INFO(LOG_TAG) << "E3Interface stopped";
356}
357
359 if (!response_queue_) {
360 return ErrorCode::NOT_INITIALIZED;
361 }
362 // Per-thread ring: open the producer thread's outbound ring so the L0 stamp
363 // below is recorded (idempotent; no-op with tracing off).
364 latrec_tls_open_as(LATREC_ROLE_OUTBOUND);
365 // enqueue_seq identifies this PDU across the latrec stage records. An emit
366 // path that already stamped LATREC_EMIT_ENTER allocated it there, so
367 // that record and these share one key; everything else allocates here.
368 if (pdu.enqueue_seq == 0)
370 const uint64_t seq = pdu.enqueue_seq;
371 latrec_tstamp(seq, LATREC_ENQUEUE, 0, static_cast<uint64_t>(pdu.type));
372 const ErrorCode rc = response_queue_->push(std::move(pdu));
373 if (rc != ErrorCode::SUCCESS) {
374 // NOT_INITIALIZED means shutdown() had already been called on this
375 // queue (e.g. a registered SM's own producer thread, not one of
376 // E3Interface's own, still emitting in the window between
377 // response_queue_->shutdown() and that thread being joined via
378 // SmRegistry::clear() in stop()) -- distinct from the queue actually
379 // being full.
380 const uint64_t reason = (rc == ErrorCode::NOT_INITIALIZED)
382 latrec_tstamp(seq, LATREC_DROP, 0, reason);
383 }
384 return rc;
385}
386
387std::vector<uint32_t> E3Interface::get_available_ran_functions() const {
388 return SmRegistry::instance().get_available_ran_functions();
389}
390
391ErrorCode E3Interface::register_sm(std::unique_ptr<ServiceModel> sm) {
392 if (!sm) {
393 return ErrorCode::INVALID_PARAM;
394 }
395 const uint32_t ran_function_id = sm->ran_function_id();
396 sm->set_subscribers_provider([this, ran_function_id]() {
397 if (!subscription_manager_) {
398 return std::vector<uint32_t>{};
399 }
400 return subscription_manager_->get_subscribed_dapps(ran_function_id);
401 });
402 sm->set_outbound_emitter([this, ran_function_id](Pdu&& pdu) {
403 if (pdu.message_id == 0) {
404 pdu.message_id = generate_message_id();
405 }
406 // Mirrors E3Agent::send_indication / e3_service_model_emit_indication:
407 // this is an emit path, so it stamps EMIT_ENTER itself rather than
408 // leaving it to queue_outbound, which only allocates enqueue_seq for a
409 // Pdu that arrives with none (its own comment says as much -- this was
410 // "everything else", the one emit path that hadn't followed it). A
411 // Service Model that has opened its own ring and called
412 // latrec_ctx_set() before this point -- the documented pattern every
413 // producer follows -- gets a real join to the outbound leg; one that
414 // hasn't gets an EMIT_ENTER with aux 0, same as an unset context
415 // anywhere else.
416 pdu.enqueue_seq = latrec_seq_next();
417 latrec_tstamp(pdu.enqueue_seq, LATREC_EMIT_ENTER, latrec_ctx(), ran_function_id);
418 return queue_outbound(std::move(pdu));
419 });
420 return SmRegistry::instance().register_sm(std::move(sm));
421}
422
424 if (dapp_status_changed_handler_) {
425 dapp_status_changed_handler_();
426 }
427}
428
429// =========================================================================
430// Thread Entry Points
431// =========================================================================
432
433void E3Interface::setup_loop_ran() {
434 // This thread takes no affinity/niceness config, so open its ring here.
435 latrec_tls_open_as(LATREC_ROLE_SETUP);
436 E3_LOG_INFO(LOG_TAG) << "Setup loop (RAN) started";
437
438 auto available_ran_functions = SmRegistry::instance().get_available_ran_functions();
439
440 while (!should_stop_.load()) {
441 std::vector<uint8_t> buffer;
442 int ret = connector_->recv_setup_request(buffer);
443
444 if (ret <= 0) {
445 if (should_stop_.load()) break;
446 continue;
447 }
448
449 E3_LOG_INFO(LOG_TAG) << "Setup request received: " << ret << " bytes";
450
451 // Decode the setup request. On any failure the REQ/REP exchange
452 // must still be completed (see send_negative_setup_reply) or the
453 // setup channel wedges for every subsequent dApp.
454 auto decode_result = encoder_->decode(buffer.data(), static_cast<size_t>(ret));
455 if (!decode_result) {
456 E3_LOG_WARN(LOG_TAG) << "Undecodable setup message (" << ret
457 << " bytes, wrong encoding or garbage); rejecting";
458 send_negative_setup_reply(0);
459 continue;
460 }
461
462 Pdu& pdu = *decode_result;
463 if (pdu.type != PduType::SETUP_REQUEST) {
464 E3_LOG_WARN(LOG_TAG) << "Unexpected PDU type in setup: "
465 << pdu_type_to_string(pdu.type) << "; rejecting";
466 send_negative_setup_reply(pdu.message_id);
467 continue;
468 }
469
470 auto* request = std::get_if<SetupRequest>(&pdu.choice);
471 if (!request) {
472 E3_LOG_WARN(LOG_TAG) << "Failed to get SetupRequest from PDU; rejecting";
473 send_negative_setup_reply(pdu.message_id);
474 continue;
475 }
476
477 handle_setup_request(*request, pdu.message_id);
478 }
479
480 E3_LOG_INFO(LOG_TAG) << "Setup loop (RAN) stopped";
481}
482
483void E3Interface::inbound_loop_ran() {
484 apply_thread_config(config_.io_thread_affinity, config_.io_thread_niceness, LATREC_ROLE_INBOUND);
485 E3_LOG_INFO(LOG_TAG) << "Inbound loop (RAN) started";
486
487 ErrorCode result = connector_->setup_inbound_connection();
488 if (result != ErrorCode::SUCCESS) {
489 E3_LOG_ERROR(LOG_TAG) << "Failed to setup inbound connection";
490 return;
491 }
492
493 std::vector<uint8_t> buffer;
494
495 while (!should_stop_.load()) {
496 int ret = connector_->receive(buffer);
497
498 if (ret <= 0) {
499 if (should_stop_.load()) break;
500 continue;
501 }
502
503 // seq comes from the process-wide counter, so an inbound message never
504 // shares a key with an outbound one.
505 const uint64_t seq = latrec_seq_next();
506 latrec_tstamp(seq, LATREC_RECV, static_cast<uint64_t>(ret), 0);
507
508 auto decode_result = encoder_->decode(buffer.data(), static_cast<size_t>(ret));
509 if (!decode_result) {
510 E3_LOG_ERROR(LOG_TAG) << "Failed to decode PDU in subscriber";
512 continue;
513 }
514
515 Pdu& pdu = *decode_result;
516 latrec_tstamp(seq, LATREC_DECODE_E3AP_DONE, pdu.message_id, static_cast<uint64_t>(pdu.type));
517
518 switch (pdu.type) {
519 case PduType::SUBSCRIPTION_REQUEST: {
520 auto* request = std::get_if<SubscriptionRequest>(&pdu.choice);
521 if (request) {
522 handle_subscription_request(*request, pdu.message_id);
523 }
524 break;
525 }
526
527 case PduType::SUBSCRIPTION_DELETE: {
528 auto* del = std::get_if<SubscriptionDelete>(&pdu.choice);
529 if (del) {
530 handle_subscription_delete(*del, pdu.message_id);
531 }
532 break;
533 }
534
535 case PduType::DAPP_CONTROL_ACTION: {
536 auto* action = std::get_if<DAppControlAction>(&pdu.choice);
537 if (action) {
538 handle_control_action(*action, pdu.message_id, seq);
539 }
540 break;
541 }
542
543 case PduType::DAPP_REPORT: {
544 auto* report = std::get_if<DAppReport>(&pdu.choice);
545 if (report) {
546 if (report_queue_) {
547 // Hand off to the report worker so downstream work
548 // never blocks the inbound read path. The queue logs
549 // on overflow; surface it here as an error too.
550 report->recv_seq = seq;
551 // Timestamped before the push, not after: the worker
552 // is already blocked on pop(), so it can drain, run
553 // the handler and stamp REPORT_DONE inside the window
554 // between the push returning and this thread issuing
555 // its own stamp. Stamping afterwards puts the queue
556 // entry after the queue exit whenever that happens.
557 const uint64_t t_queued = latrec_tnow();
558 const ErrorCode report_rc = report_queue_->push(std::move(*report));
559 if (report_rc != ErrorCode::SUCCESS) {
560 E3_LOG_ERROR(LOG_TAG) << "Report queue full — dropping dApp report";
561 // See queue_outbound: NOT_INITIALIZED means
562 // shutdown() already ran, not that this queue was
563 // actually full.
564 const uint64_t reason = (report_rc == ErrorCode::NOT_INITIALIZED)
566 latrec_tstamp(seq, LATREC_DROP, 0, reason);
567 } else {
568 latrec_tstamp_at(seq, LATREC_REPORT_QUEUED, 0, 0, t_queued);
569 }
570 }
571 }
572 break;
573 }
574
575 case PduType::RELEASE_MESSAGE: {
576 auto* release = std::get_if<ReleaseMessage>(&pdu.choice);
577 if (release) {
578 handle_release_message(*release);
579 }
580 break;
581 }
582
583 default:
584 E3_LOG_WARN(LOG_TAG) << "Received unexpected PDU type: "
585 << pdu_type_to_string(pdu.type);
587 break;
588 }
589 }
590
591 E3_LOG_INFO(LOG_TAG) << "Inbound loop (RAN) stopped";
592}
593
594void E3Interface::outbound_loop_ran() {
595 apply_thread_config(config_.io_thread_affinity, config_.io_thread_niceness, LATREC_ROLE_OUTBOUND);
596 E3_LOG_INFO(LOG_TAG) << "Outbound loop (RAN) started";
597
598 // Ignore SIGPIPE to handle closed connections gracefully
599 signal(SIGPIPE, SIG_IGN);
600
601 // Set up the outbound (PUB) socket. Try immediately; retry with backoff
602 // only on failure. Putting an unconditional sleep before the first
603 // attempt introduces a multi-second startup latency that breaks
604 // integration tests and any colocated RAN/dApp pair.
605 ErrorCode result = connector_->setup_outbound_connection();
606 while (result != ErrorCode::SUCCESS && !should_stop_.load()) {
607 E3_LOG_WARN(LOG_TAG) << "Outbound connection setup failed ("
608 << error_code_to_string(result) << "), retrying in 1s";
609 std::this_thread::sleep_for(std::chrono::seconds(1));
610 if (should_stop_.load()) return;
611 result = connector_->setup_outbound_connection();
612 }
613
614 if (result != ErrorCode::SUCCESS) {
615 E3_LOG_ERROR(LOG_TAG) << "Failed to setup outbound connection";
616 return;
617 }
618 E3_LOG_INFO(LOG_TAG) << "Outbound connection established";
619
620 while (!should_stop_.load()) {
621 // Pop from queue (blocking)
622 auto pdu_opt = response_queue_->pop(std::chrono::milliseconds(10));
623
624 if (!pdu_opt) {
625 continue;
626 }
627 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DEQUEUE, 0, static_cast<uint64_t>(pdu_opt->type));
628
629 // Encode and send
630 auto encode_result = encoder_->encode(*pdu_opt);
631 if (!encode_result) {
632 E3_LOG_ERROR(LOG_TAG) << "Failed to encode PDU for sending";
633 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DROP, 0, LATREC_DROP_ENCODE);
634 continue;
635 }
636 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_ENCODE_E3AP_DONE, encode_result->buffer.size(), 0);
637
638 latrec_ctx_set(pdu_opt->enqueue_seq); // for the connector's LC stamps
639 ErrorCode send_result = connector_->send(encode_result->buffer);
640 if (send_result != ErrorCode::SUCCESS) {
641 E3_LOG_ERROR(LOG_TAG) << "Failed to send PDU";
642 // CANCELLED means the connector's shutdown() had already run,
643 // distinct from a genuine transport failure.
644 const uint64_t reason = (send_result == ErrorCode::CANCELLED)
646 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DROP, 0, reason);
647 } else {
648 E3_LOG_DEBUG(LOG_TAG) << "Sent PDU: " << pdu_type_to_string(pdu_opt->type);
649 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_SEND_DONE, pdu_opt->message_id, 0);
650 }
651 }
652
653 E3_LOG_INFO(LOG_TAG) << "Outbound loop (RAN) stopped";
654}
655
656void E3Interface::report_worker_loop() {
657 apply_thread_config(config_.io_thread_affinity, config_.io_thread_niceness, LATREC_ROLE_REPORT);
658 E3_LOG_INFO(LOG_TAG) << "Report worker loop started";
659
660 while (!should_stop_.load()) {
661 // Blocking pop with the queue's adaptive spin-wait, mirroring
662 // outbound_loop_ran's use of response_queue_.
663 auto report_opt = report_queue_->pop(std::chrono::milliseconds(10));
664 if (report_opt) {
665 handle_dapp_report(*report_opt);
666 latrec_tstamp(report_opt->recv_seq, LATREC_REPORT_DONE, 0, 0);
667 }
668 }
669
670 // Drain anything left after shutdown was signalled.
671 while (auto report_opt = report_queue_->try_pop()) {
672 handle_dapp_report(*report_opt);
673 }
674
675 E3_LOG_INFO(LOG_TAG) << "Report worker loop stopped";
676}
677
678#ifdef LIBE3_ENABLE_LATREC
679void E3Interface::context_monitor_loop() {
680 latrec_tls_open_as(LATREC_ROLE_CONTEXT);
681
682#ifdef __linux__
683 long prev_nivcsw = 0;
684 {
685 struct rusage ru {};
686 if (getrusage(RUSAGE_SELF, &ru) == 0) {
687 prev_nivcsw = ru.ru_nivcsw;
688 }
689 }
690#endif
691
692 while (!should_stop_.load()) {
693 std::this_thread::sleep_for(std::chrono::seconds(1));
694 if (should_stop_.load()) {
695 break;
696 }
697
698 uint64_t nivcsw_delta = 0;
699 uint64_t cur_freq_khz = 0;
700#ifdef __linux__
701 struct rusage ru {};
702 if (getrusage(RUSAGE_SELF, &ru) == 0) {
703 nivcsw_delta = static_cast<uint64_t>(ru.ru_nivcsw - prev_nivcsw);
704 prev_nivcsw = ru.ru_nivcsw;
705 }
706
707 // Best-effort: absent under most VMs/containers and some CI runners,
708 // in which case this stays 0 rather than failing the sample.
709 const int cpu = sched_getcpu();
710 if (cpu >= 0) {
711 char path[128];
712 std::snprintf(path, sizeof(path),
713 "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", cpu);
714 if (FILE* f = std::fopen(path, "r")) {
715 long khz = 0;
716 if (std::fscanf(f, "%ld", &khz) == 1) {
717 cur_freq_khz = static_cast<uint64_t>(khz);
718 }
719 std::fclose(f);
720 }
721 }
722#endif
723 // seq = 0: a 1 Hz slow-lane record carries no per-message key.
724 latrec_tstamp(0, LATREC_CONTEXT, nivcsw_delta, cur_freq_khz);
725 }
726}
727#endif // LIBE3_ENABLE_LATREC
728
729// =========================================================================
730// Message Handlers
731// =========================================================================
732
733void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t request_message_id) {
734 request_message_id = sanitize_request_message_id(request_message_id);
735 latrec_tstamp(request_message_id, LATREC_SETUP_BEGIN, 0, 0);
736 E3_LOG_INFO(LOG_TAG) << "Handling setup request from dApp '" << request.dapp_name
737 << "' (version=" << request.dapp_version
738 << ", vendor=" << request.vendor
739 << ", e3ap_version=" << request.e3ap_protocol_version << ")";
740
741 ResponseCode response_code = ResponseCode::NEGATIVE;
742 uint32_t assigned_dapp_id = 0;
743
744 // Register the dApp
745 auto [result, dapp_id] = subscription_manager_->register_dapp();
746 assigned_dapp_id = dapp_id;
747
748 if (result == ErrorCode::SUCCESS) {
749 response_code = ResponseCode::POSITIVE;
750 E3_LOG_INFO(LOG_TAG) << "dApp '" << request.dapp_name << "' registered with assigned ID " << assigned_dapp_id;
751 notify_dapp_status_changed();
752 } else {
753 E3_LOG_ERROR(LOG_TAG) << "Failed to register dApp '" << request.dapp_name << "': " << error_code_to_string(result);
754 }
755
756 // Create and send response
757 // Get available RAN functions and convert to RanFunctionDef list
758 auto available_ran_function_ids = SmRegistry::instance().get_available_ran_functions();
759 E3_LOG_DEBUG(LOG_TAG) << "Available RAN function ids count: " << available_ran_function_ids.size();
760 std::vector<RanFunctionDef> ran_function_list;
761 for (auto id : available_ran_function_ids) {
762 RanFunctionDef func;
763 func.ran_function_identifier = id;
764 ServiceModel* sm = SmRegistry::instance().get_by_ran_function(id);
765 if (sm) {
766 func.telemetry_identifier_list = sm->telemetry_ids();
767 func.control_identifier_list = sm->control_ids();
768 func.ran_function_data = sm->ran_function_data();
769 }
770 // ranFunctionData is mandatory SIZE(1..32768) per entry, and the value
771 // is recomputed on every SetupRequest, so registration cannot vouch for
772 // what we get here. An entry that cannot be encoded would fail the whole
773 // response -- every other RAN function with it -- so omit it instead:
774 // ranFunctionList itself is OPTIONAL, which is how E3AP spells "nothing
775 // to advertise".
776 if (func.ran_function_data.empty()
777 || func.ran_function_data.size() > MAX_PROTOCOL_DATA_SIZE) {
778 E3_LOG_ERROR(LOG_TAG) << "Omitting RAN function " << id
779 << " from the setup response: "
780 << func.ran_function_data.size()
781 << " bytes of ran_function_data (need 1.."
782 << MAX_PROTOCOL_DATA_SIZE << ")";
783 continue;
784 }
785 ran_function_list.push_back(func);
786 }
787 E3_LOG_DEBUG(LOG_TAG) << "Constructed ran_function_list size: " << ran_function_list.size();
788 for (const auto &rf : ran_function_list) {
789 E3_LOG_DEBUG(LOG_TAG) << " RAN function id=" << rf.ran_function_identifier
790 << ", telemetry_count=" << rf.telemetry_identifier_list.size()
791 << ", control_count=" << rf.control_identifier_list.size()
792 << ", ran_function_data_len=" << rf.ran_function_data.size();
793 }
794
795 auto encode_result = encoder_->encode_setup_response(
796 generate_message_id(),
797 request_message_id,
798 response_code,
799 config_.e3ap_version,
800 // A failed registration assigns no id; engaging the optional with 0
801 // would violate E3-DAppID (1..65535) at encode.
802 response_code == ResponseCode::POSITIVE
803 ? std::optional<uint32_t>(assigned_dapp_id)
804 : std::nullopt,
805 config_.ran_identifier, // ran_identifier
806 ran_function_list
807 );
808
809 if (!encode_result) {
810 // The peer-controlled inputs are sanitized above, so a failure here
811 // is an internal encoder or configuration bug.
812 E3_LOG_ERROR(LOG_TAG) << "Failed to encode setup response for request id "
813 << request_message_id << "; aborting";
814 std::abort();
815 }
816
817 ErrorCode send_result = connector_->send_response(encode_result->buffer);
818 if (send_result != ErrorCode::SUCCESS) {
819 E3_LOG_ERROR(LOG_TAG) << "Failed to send setup response for request id " << request_message_id
820 << "; error=" << error_code_to_string(send_result);
821 } else {
822 E3_LOG_INFO(LOG_TAG) << "Sent setup response for request id " << request_message_id;
823 }
824 latrec_tstamp(request_message_id, LATREC_SETUP_DONE, 0,
825 send_result == ErrorCode::SUCCESS ? 1 : 0);
826}
827
828void E3Interface::send_negative_setup_reply(uint32_t request_id) {
829 // A bare negative SetupResponse omits every OPTIONAL field
830 // (ranFunctionList included), so with the fixed, mutually known
831 // encoding it always encodes. The mandatory fields have to satisfy
832 // their schema constraints, though: requestId is E3-MessageID
833 // INTEGER (1..4294967295) — an id that cannot be echoed (0, from
834 // undecodable bytes) is replaced with a freshly generated one — and ranIdentifier is
835 // SIZE (1..64), hence the placeholder when the config left it empty.
836 uint32_t message_id = generate_message_id();
837 request_id = sanitize_request_message_id(request_id);
838 auto encode_result = encoder_->encode_setup_response(
839 message_id,
840 request_id,
841 ResponseCode::NEGATIVE,
842 config_.e3ap_version,
843 std::nullopt, // no dApp identifier assigned
844 config_.ran_identifier.empty() ? "unknown" : config_.ran_identifier,
845 std::nullopt // no RAN functions advertised
846 );
847
848 if (!encode_result) {
849 E3_LOG_ERROR(LOG_TAG) << "Failed to encode negative setup response for request id "
850 << request_id << "; setup channel may be left unusable";
851 return;
852 }
853
854 ErrorCode send_result = connector_->send_response(encode_result->buffer);
855 if (send_result != ErrorCode::SUCCESS) {
856 E3_LOG_WARN(LOG_TAG) << "Failed to send negative setup response for request id "
857 << request_id << "; error=" << error_code_to_string(send_result);
858 }
859 latrec_tstamp(request_id, LATREC_SETUP_DONE, 0, 0 /* rejected */);
860}
861
862void E3Interface::handle_subscription_request(const SubscriptionRequest& request, uint32_t request_message_id) {
863 latrec_tstamp(request_message_id, LATREC_SUB_BEGIN, 0, 0);
864 E3_LOG_INFO(LOG_TAG) << "Handling subscription request from dApp " << request.dapp_identifier
865 << " for RAN function " << request.ran_function_identifier;
866
867 ResponseCode response_code = ResponseCode::NEGATIVE;
868 uint32_t subscription_id = 1;
869
870 // Check if dApp is registered
871 if (!subscription_manager_->is_dapp_registered(request.dapp_identifier)) {
872 E3_LOG_ERROR(LOG_TAG) << "dApp " << request.dapp_identifier << " not registered";
873 } else {
874 uint32_t period = request.periodicity.value_or(0);
875 auto [result, sub_id] = subscription_manager_->add_subscription(
876 request.dapp_identifier,
877 request.ran_function_identifier,
878 request.telemetry_identifier_list,
879 request.control_identifier_list,
880 period
881 );
882 subscription_id = sub_id;
883
884 if (result == ErrorCode::SUCCESS || result == ErrorCode::SUBSCRIPTION_EXISTS) {
885 response_code = ResponseCode::POSITIVE;
886 E3_LOG_INFO(LOG_TAG) << "Subscription added: dApp " << request.dapp_identifier
887 << " -> RAN function " << request.ran_function_identifier
888 << " (subscription_id=" << subscription_id << ")";
889 if (result == ErrorCode::SUCCESS) {
890 notify_dapp_status_changed();
891 }
892 } else {
893 E3_LOG_ERROR(LOG_TAG) << "Failed to add subscription: "
894 << error_code_to_string(result);
895 }
896 }
897
898 // Create and queue response
899 Pdu response_pdu(PduType::SUBSCRIPTION_RESPONSE);
900 SubscriptionResponse resp;
901 response_pdu.message_id = generate_message_id();
902 resp.request_id = request_message_id;
903 resp.dapp_identifier = request.dapp_identifier;
904 resp.response_code = response_code;
905 if (response_code == ResponseCode::POSITIVE) {
906 resp.subscription_id = subscription_id;
907 }
908 response_pdu.choice = resp;
909
910 latrec_tstamp(request_message_id, LATREC_SUB_DONE, 0,
911 response_code == ResponseCode::POSITIVE ? 1 : 0);
912 queue_outbound(std::move(response_pdu));
913}
914
915void E3Interface::handle_subscription_delete(const SubscriptionDelete& del, uint32_t request_message_id) {
916 E3_LOG_INFO(LOG_TAG) << "Handling subscription delete from dApp " << del.dapp_identifier
917 << " for subscription " << del.subscription_id;
918
919 ResponseCode response_code = ResponseCode::NEGATIVE;
920
921 ErrorCode result = subscription_manager_->remove_subscription_by_id(
922 del.dapp_identifier,
923 del.subscription_id
924 );
925
926 if (result == ErrorCode::SUCCESS) {
927 response_code = ResponseCode::POSITIVE;
928 E3_LOG_INFO(LOG_TAG) << "Subscription " << del.subscription_id << " removed for dApp " << del.dapp_identifier;
929 notify_dapp_status_changed();
930 } else {
931 E3_LOG_ERROR(LOG_TAG) << "Failed to remove subscription: "
932 << error_code_to_string(result);
933 }
934
935 // Create and queue response
936 Pdu response_pdu(PduType::SUBSCRIPTION_RESPONSE);
937 SubscriptionResponse resp;
938 response_pdu.message_id = generate_message_id();
939 resp.request_id = request_message_id;
940 resp.dapp_identifier = del.dapp_identifier;
941 resp.response_code = response_code;
942 if (response_code == ResponseCode::POSITIVE) {
943 resp.subscription_id = del.subscription_id;
944 }
945 response_pdu.choice = resp;
946
947 queue_outbound(std::move(response_pdu));
948}
949
950void E3Interface::handle_control_action(const DAppControlAction& action, uint32_t request_message_id,
951 uint64_t latrec_seq) {
952 E3_LOG_INFO(LOG_TAG) << "Handling control action from dApp " << action.dapp_identifier
953 << " for RAN function " << action.ran_function_identifier
954 << " control " << action.control_identifier
955 << " (" << action.action_data.size() << " bytes)";
956
957 ServiceModel* sm = SmRegistry::instance().get_by_ran_function(action.ran_function_identifier);
958
959 if (sm && sm->is_running()) {
960 // The SM's own DECODE_E3SM_BEGIN/DONE already marks the library-to-SM
961 // boundary, keyed on the SM's own inflight counter -- that pair needs
962 // no bridge. What has none is whatever the SM emits in response
963 // (typically a MessageAck): its EMIT_ENTER has nothing to read unless
964 // this inbound leg's key is published first, the same bridge
965 // handle_indication and handle_xapp_control_action publish on the
966 // dApp role.
967 latrec_ctx_set(latrec_seq);
968 ErrorCode result = sm->handle_control_action(request_message_id, action);
969 if (result != ErrorCode::SUCCESS) {
970 E3_LOG_ERROR(LOG_TAG) << "SM failed to process control action: "
971 << error_code_to_string(result);
972 return;
973 }
974 E3_LOG_INFO(LOG_TAG) << "Control action processed by SM";
975 } else {
976 E3_LOG_ERROR(LOG_TAG) << "No running SM found for RAN function "
977 << action.ran_function_identifier;
978 return;
979 }
980}
981
982void E3Interface::handle_dapp_report(const DAppReport& report) {
983 E3_LOG_INFO(LOG_TAG) << "Handling dApp report from dApp " << report.dapp_identifier
984 << " for RAN function " << report.ran_function_identifier;
985
986 // Forward to callback if set
987 if (dapp_report_handler_) {
988 dapp_report_handler_(report);
989 }
990}
991
992void E3Interface::handle_release_message(const ReleaseMessage& release) {
993 E3_LOG_INFO(LOG_TAG) << "Handling release message from dApp " << release.dapp_identifier;
994
995 handle_dapp_disconnection(release.dapp_identifier);
996}
997
998void E3Interface::handle_dapp_disconnection(uint32_t dapp_id) {
999 E3_LOG_INFO(LOG_TAG) << "Handling dApp disconnection: " << dapp_id;
1000
1001 // Get subscriptions before unregistering
1002 auto subscriptions = subscription_manager_->get_dapp_subscriptions(dapp_id);
1003
1004 // Unregister dApp (this will also clean up subscriptions)
1005 ErrorCode result = subscription_manager_->unregister_dapp(dapp_id);
1006
1007 if (result == ErrorCode::SUCCESS) {
1008 E3_LOG_INFO(LOG_TAG) << "dApp " << dapp_id << " unregistered, "
1009 << subscriptions.size() << " subscriptions removed";
1010 notify_dapp_status_changed();
1011 } else {
1012 E3_LOG_ERROR(LOG_TAG) << "Failed to unregister dApp " << dapp_id << ": "
1013 << error_code_to_string(result);
1014 }
1015}
1016
1017// =========================================================================
1018// SM Lifecycle Management
1019// =========================================================================
1020
1021// ===========================================================================
1022// dApp-role thread bodies
1023// ===========================================================================
1024
1025void E3Interface::setup_loop_dapp() {
1026 // As in setup_loop_ran: this thread takes no affinity/niceness config, so
1027 // its ring is opened here. It enqueues the setup response, which the dApp
1028 // session ring picks up.
1029 latrec_tls_open_as(LATREC_ROLE_SETUP);
1030 E3_LOG_INFO(LOG_TAG) << "Setup loop (dApp) started";
1031
1032 // Encode the SetupRequest from config_'s dApp identification fields.
1033 auto enc = encoder_->encode_setup_request(
1034 generate_message_id(),
1035 config_.e3ap_version,
1036 config_.dapp_name,
1037 config_.dapp_version,
1038 config_.vendor
1039 );
1040 if (!enc) {
1041 E3_LOG_ERROR(LOG_TAG) << "Failed to encode SetupRequest";
1042 std::lock_guard<std::mutex> lk(setup_complete_mu_);
1043 setup_complete_ = true;
1044 setup_succeeded_ = false;
1045 setup_complete_cv_.notify_all();
1046 return;
1047 }
1048
1049 // Lazy-pirate REQ: send, wait for reply with timeout, retry on no reply.
1050 constexpr int MAX_RETRIES = 5;
1051 int retries = 0;
1052 bool got_response = false;
1053 while (!should_stop_.load() && retries < MAX_RETRIES) {
1054 ErrorCode rc = connector_->send_setup_request_client(enc->buffer);
1055 if (rc != ErrorCode::SUCCESS) {
1056 E3_LOG_ERROR(LOG_TAG) << "send_setup_request_client failed: "
1057 << error_code_to_string(rc);
1058 ++retries;
1059 std::this_thread::sleep_for(std::chrono::seconds(1));
1060 continue;
1061 }
1062
1063 // Try a few times to receive (connector recv has its own short timeout).
1064 for (int attempt = 0; attempt < 4 && !should_stop_.load(); ++attempt) {
1065 std::vector<uint8_t> resp_buf;
1066 int n = connector_->recv_setup_response_client(resp_buf);
1067 if (n > 0) {
1068 auto decoded = encoder_->decode(resp_buf.data(), static_cast<size_t>(n));
1069 if (!decoded) {
1070 E3_LOG_ERROR(LOG_TAG) << "Failed to decode SetupResponse";
1071 break;
1072 }
1073 if (decoded->type != PduType::SETUP_RESPONSE) {
1074 E3_LOG_ERROR(LOG_TAG) << "Unexpected PDU on setup channel: "
1075 << pdu_type_to_string(decoded->type);
1076 break;
1077 }
1078 auto* resp = std::get_if<SetupResponse>(&decoded->choice);
1079 if (resp) {
1080 handle_setup_response(*resp);
1081 got_response = true;
1082 }
1083 break;
1084 }
1085 if (n < 0) {
1086 E3_LOG_ERROR(LOG_TAG) << "recv_setup_response_client error";
1087 break;
1088 }
1089 }
1090 if (got_response) break;
1091 ++retries;
1092 }
1093
1094 {
1095 std::lock_guard<std::mutex> lk(setup_complete_mu_);
1096 setup_complete_ = true;
1097 setup_succeeded_ = got_response;
1098 }
1099 setup_complete_cv_.notify_all();
1100
1101 E3_LOG_INFO(LOG_TAG) << "Setup loop (dApp) finished, success="
1102 << (got_response ? "yes" : "no");
1103}
1104
1105void E3Interface::inbound_loop_dapp() {
1106 apply_thread_config(config_.io_thread_affinity, config_.io_thread_niceness, LATREC_ROLE_INBOUND);
1107 E3_LOG_INFO(LOG_TAG) << "Inbound loop (dApp) started";
1108
1109 ErrorCode result = connector_->setup_inbound_connection_client();
1110 if (result != ErrorCode::SUCCESS) {
1111 E3_LOG_ERROR(LOG_TAG) << "Failed to setup inbound (dApp) connection: "
1112 << error_code_to_string(result);
1113 return;
1114 }
1115
1116 std::vector<uint8_t> buffer;
1117 while (!should_stop_.load()) {
1118 int ret = connector_->receive(buffer);
1119 if (ret <= 0) {
1120 if (should_stop_.load()) break;
1121 continue;
1122 }
1123 const uint64_t seq = latrec_seq_next();
1124 latrec_tstamp(seq, LATREC_RECV, static_cast<uint64_t>(ret), 0);
1125 auto decoded = encoder_->decode(buffer.data(), static_cast<size_t>(ret));
1126 if (!decoded) {
1127 E3_LOG_ERROR(LOG_TAG) << "Failed to decode PDU in dApp inbound";
1129 continue;
1130 }
1131 Pdu& pdu = *decoded;
1132 latrec_tstamp(seq, LATREC_DECODE_E3AP_DONE, pdu.message_id, static_cast<uint64_t>(pdu.type));
1133 switch (pdu.type) {
1134 case PduType::SUBSCRIPTION_RESPONSE: {
1135 auto* r = std::get_if<SubscriptionResponse>(&pdu.choice);
1136 if (r) handle_subscription_response(*r);
1137 break;
1138 }
1139 case PduType::INDICATION_MESSAGE: {
1140 auto* m = std::get_if<IndicationMessage>(&pdu.choice);
1141 if (m) handle_indication(*m, seq);
1142 break;
1143 }
1144 case PduType::XAPP_CONTROL_ACTION: {
1145 auto* a = std::get_if<XAppControlAction>(&pdu.choice);
1146 if (a) {
1147 a->message_id = pdu.message_id;
1148 handle_xapp_control_action(*a, seq);
1149 }
1150 break;
1151 }
1152 case PduType::MESSAGE_ACK: {
1153 auto* a = std::get_if<MessageAck>(&pdu.choice);
1154 if (a) handle_message_ack(*a);
1155 break;
1156 }
1157 default:
1158 E3_LOG_WARN(LOG_TAG) << "dApp received unexpected PDU type: "
1159 << pdu_type_to_string(pdu.type);
1161 break;
1162 }
1163 }
1164
1165 E3_LOG_INFO(LOG_TAG) << "Inbound loop (dApp) stopped";
1166}
1167
1168void E3Interface::outbound_loop_dapp() {
1169 apply_thread_config(config_.io_thread_affinity, config_.io_thread_niceness, LATREC_ROLE_OUTBOUND);
1170 E3_LOG_INFO(LOG_TAG) << "Outbound loop (dApp) started";
1171
1172 signal(SIGPIPE, SIG_IGN);
1173
1174 ErrorCode result = connector_->setup_outbound_connection_client();
1175 if (result != ErrorCode::SUCCESS) {
1176 E3_LOG_ERROR(LOG_TAG) << "Failed to setup outbound (dApp) connection: "
1177 << error_code_to_string(result);
1178 return;
1179 }
1180
1181 while (!should_stop_.load()) {
1182 auto pdu_opt = response_queue_->pop(std::chrono::milliseconds(10));
1183 if (!pdu_opt) continue;
1184 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DEQUEUE, 0, static_cast<uint64_t>(pdu_opt->type));
1185
1186 auto enc = encoder_->encode(*pdu_opt);
1187 if (!enc) {
1188 E3_LOG_ERROR(LOG_TAG) << "Failed to encode dApp outbound PDU";
1189 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DROP, 0, LATREC_DROP_ENCODE);
1190 continue;
1191 }
1192 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_ENCODE_E3AP_DONE, enc->buffer.size(), 0);
1193 latrec_ctx_set(pdu_opt->enqueue_seq); // for the connector's LC stamps
1194 ErrorCode rc = connector_->send(enc->buffer);
1195 if (rc != ErrorCode::SUCCESS) {
1196 E3_LOG_ERROR(LOG_TAG) << "Failed to send dApp outbound PDU";
1197 const uint64_t reason = (rc == ErrorCode::CANCELLED)
1199 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_DROP, 0, reason);
1200 } else {
1201 E3_LOG_DEBUG(LOG_TAG) << "Sent dApp outbound PDU: "
1202 << pdu_type_to_string(pdu_opt->type);
1203 latrec_tstamp(pdu_opt->enqueue_seq, LATREC_SEND_DONE, pdu_opt->message_id, 0);
1204 }
1205 }
1206
1207 E3_LOG_INFO(LOG_TAG) << "Outbound loop (dApp) stopped";
1208}
1209
1210// ===========================================================================
1211// dApp-role message handlers
1212// ===========================================================================
1213
1214void E3Interface::handle_setup_response(const SetupResponse& resp) {
1215 E3_LOG_INFO(LOG_TAG) << "Handling SetupResponse rc="
1216 << response_code_to_string(resp.response_code);
1217 if (dapp_state_ && resp.response_code == ResponseCode::POSITIVE) {
1218 uint32_t dapp_id = resp.dapp_identifier.value_or(0);
1219 dapp_state_->record_setup_response(dapp_id, resp.ran_function_list);
1220 }
1221 if (setup_response_handler_) {
1222 setup_response_handler_(resp);
1223 }
1224}
1225
1226void E3Interface::handle_subscription_response(const SubscriptionResponse& resp) {
1227 latrec_tstamp(resp.request_id, LATREC_SUB_BEGIN, 0, 0);
1228 if (dapp_state_) {
1229 std::lock_guard<std::mutex> lk(dapp_state_->mu);
1230 if (!dapp_state_->assigned_dapp_id.has_value() ||
1231 *dapp_state_->assigned_dapp_id != resp.dapp_identifier) {
1232 return; // not for us
1233 }
1234 }
1235 E3_LOG_INFO(LOG_TAG) << "Handling SubscriptionResponse for request "
1236 << resp.request_id << " rc="
1237 << response_code_to_string(resp.response_code);
1238 // Attribute the response to the request that caused it. The RAN function id
1239 // is not on the wire (E3-SubscriptionResponse has no ranFunctionIdentifier),
1240 // so it comes from the pending-op record keyed by the id we sent.
1241 std::optional<PendingSubscriptionOp> pending;
1242 {
1243 std::lock_guard<std::mutex> lk(pending_subscriptions_mutex_);
1244 auto it = pending_subscriptions_.find(resp.request_id);
1245 if (it != pending_subscriptions_.end()) {
1246 pending = it->second;
1247 pending_subscriptions_.erase(it);
1248 }
1249 }
1250 if (dapp_state_ && pending && resp.response_code == ResponseCode::POSITIVE) {
1251 // Not under dapp_state_->mu: these helpers take it themselves.
1252 if (pending->is_delete) {
1253 dapp_state_->remove_subscription_by_rf(pending->ran_function_id);
1254 E3_LOG_INFO(LOG_TAG) << "Subscription to RAN function "
1255 << pending->ran_function_id << " released";
1256 } else if (resp.subscription_id.has_value()) {
1257 dapp_state_->record_subscription(pending->ran_function_id,
1258 *resp.subscription_id);
1259 E3_LOG_INFO(LOG_TAG) << "Subscription " << *resp.subscription_id
1260 << " granted for RAN function "
1261 << pending->ran_function_id;
1262 }
1263 }
1264 if (subscription_response_handler_) {
1265 subscription_response_handler_(resp);
1266 }
1267}
1268
1269void E3Interface::handle_indication(const IndicationMessage& msg, uint64_t latrec_seq) {
1270 // ZMQ PUB broadcasts to every connected SUB, so a dApp instance will
1271 // receive indications addressed to other dApps connected to the same RAN.
1272 // Filter: an indication is only for us if its dApp identifier matches
1273 // the one the RAN assigned us during setup.
1274 if (dapp_state_) {
1275 std::lock_guard<std::mutex> lk(dapp_state_->mu);
1276 if (!dapp_state_->assigned_dapp_id.has_value() ||
1277 *dapp_state_->assigned_dapp_id != msg.dapp_identifier) {
1279 return; // not for us
1280 }
1281 }
1282 latrec_tstamp(latrec_seq, LATREC_DELIVER_BEGIN, 0, 0);
1283 // The application's own stages key on this: it is handed the message, not
1284 // the seq, so publish it the way the outbound direction already does for
1285 // EMIT_ENTER. Without this a dApp's A9..A12 can only be paired with this
1286 // leg positionally, on the strength of sharing a thread.
1287 latrec_ctx_set(latrec_seq);
1288 if (indication_handler_) {
1289 indication_handler_(msg);
1290 }
1291 latrec_tstamp(latrec_seq, LATREC_DELIVER_DONE, 0, 0);
1292}
1293
1294void E3Interface::handle_xapp_control_action(const XAppControlAction& action,
1295 uint64_t latrec_seq) {
1296 if (dapp_state_) {
1297 std::lock_guard<std::mutex> lk(dapp_state_->mu);
1298 if (!dapp_state_->assigned_dapp_id.has_value() ||
1299 *dapp_state_->assigned_dapp_id != action.dapp_identifier) {
1301 return;
1302 }
1303 }
1304 // aux carries the loop's sequenceId. The ring's own seq stays the inbound
1305 // counter (the ctx_set bridge below depends on it), so this is what lets a
1306 // reader join this dApp-side leg to the RAN's BRIDGE_IN/OUT records for the
1307 // same procedure -- the two rings share no counter, only this id.
1308 latrec_tstamp(latrec_seq, LATREC_DELIVER_BEGIN, action.sequence_id, 0);
1309 latrec_ctx_set(latrec_seq);
1310 if (xapp_control_handler_) {
1311 xapp_control_handler_(action);
1312 }
1313 latrec_tstamp(latrec_seq, LATREC_DELIVER_DONE, action.sequence_id, 0);
1314}
1315
1316void E3Interface::handle_message_ack(const MessageAck& ack) {
1317 // Closes the acknowledgment tail. Keyed on the acknowledged message's id,
1318 // matching the LATREC_ACK_SENT the RAN-side Service Model stamps, so the
1319 // leg joins on a libe3 build the way it already did on backends that
1320 // stamped this themselves.
1321 latrec_tstamp(ack.request_id, LATREC_ACK_RECV,
1322 static_cast<uint64_t>(ack.response_code), 0);
1323 if (message_ack_handler_) {
1324 message_ack_handler_(ack);
1325 }
1326}
1327
1328// ===========================================================================
1329// dApp-role accessors and outbound helpers
1330// ===========================================================================
1331
1332std::optional<uint32_t> E3Interface::dapp_id() const noexcept {
1333 if (!dapp_state_) return std::nullopt;
1334 std::lock_guard<std::mutex> lk(dapp_state_->mu);
1335 return dapp_state_->assigned_dapp_id;
1336}
1337
1338std::vector<uint32_t> E3Interface::active_subscription_ids() const {
1339 if (!dapp_state_) return {};
1340 return dapp_state_->active_subscriptions();
1341}
1342
1343std::vector<uint32_t> E3Interface::subscribed_ran_functions() const {
1344 if (!dapp_state_) return {};
1345 return dapp_state_->subscribed_ran_functions();
1346}
1347
1348std::vector<RanFunctionDef> E3Interface::remote_ran_functions() const {
1349 if (!dapp_state_) return {};
1350 std::lock_guard<std::mutex> lk(dapp_state_->mu);
1351 return dapp_state_->remote_ran_functions;
1352}
1353
1355 uint32_t ran_function_id,
1356 std::vector<uint32_t> telemetry_ids,
1357 std::vector<uint32_t> control_ids,
1358 std::optional<uint32_t> sub_time,
1359 std::optional<uint32_t> periodicity,
1360 uint32_t* out_request_id
1361) {
1362 if (!dapp_state_) return ErrorCode::STATE_ERROR;
1363 auto id = dapp_id();
1364 if (!id) return ErrorCode::NOT_INITIALIZED;
1365
1368 req.dapp_identifier = *id;
1369 req.ran_function_identifier = ran_function_id;
1370 req.telemetry_identifier_list = std::move(telemetry_ids);
1371 req.control_identifier_list = std::move(control_ids);
1372 req.subscription_time = sub_time;
1373 req.periodicity = periodicity;
1374 pdu.choice = std::move(req);
1375 const uint32_t mid = generate_message_id();
1376 pdu.message_id = mid;
1377 // Recorded before the send, not after: the response can only be attributed
1378 // to a RAN function through this entry, and the inbound thread may see it
1379 // before this call returns.
1380 remember_subscription_op(mid, ran_function_id, /*is_delete=*/false);
1381 ErrorCode rc = queue_outbound(std::move(pdu));
1382 if (rc != ErrorCode::SUCCESS) {
1383 forget_subscription_op(mid);
1384 return rc;
1385 }
1386 if (out_request_id) {
1387 *out_request_id = mid;
1388 }
1389 return rc;
1390}
1391
1393 if (!dapp_state_) return ErrorCode::STATE_ERROR;
1394 auto id = dapp_id();
1395 if (!id) return ErrorCode::NOT_INITIALIZED;
1396 auto sub_id = dapp_state_->subscription_id_for(ran_function_id);
1397 if (!sub_id) return ErrorCode::SUBSCRIPTION_NOT_FOUND;
1398
1401 del.dapp_identifier = *id;
1402 del.subscription_id = *sub_id;
1403 pdu.choice = del;
1404 const uint32_t mid = generate_message_id();
1405 pdu.message_id = mid;
1406 // The RAN answers a delete with a SubscriptionResponse too, so both
1407 // directions land in handle_subscription_response and are told apart here.
1408 remember_subscription_op(mid, ran_function_id, /*is_delete=*/true);
1409 ErrorCode rc = queue_outbound(std::move(pdu));
1410 if (rc != ErrorCode::SUCCESS) {
1411 forget_subscription_op(mid);
1412 }
1413 return rc;
1414}
1415
1417 uint32_t ran_function_id,
1418 uint32_t control_id,
1419 std::vector<uint8_t> action_data,
1420 uint32_t sequence_id,
1421 uint32_t* out_message_id
1422) {
1423 if (!dapp_state_) return ErrorCode::STATE_ERROR;
1424 auto id = dapp_id();
1425 if (!id) return ErrorCode::NOT_INITIALIZED;
1426
1429 a.dapp_identifier = *id;
1430 a.ran_function_identifier = ran_function_id;
1431 a.control_identifier = control_id;
1432 a.sequence_id = sequence_id;
1433 a.action_data = std::move(action_data);
1434 pdu.choice = std::move(a);
1435 const uint32_t mid = generate_message_id();
1436 pdu.message_id = mid;
1437 // Mirrors E3Agent::send_indication: this is an emit path, so it stamps
1438 // EMIT_ENTER itself rather than leaving it to queue_outbound, which only
1439 // allocates enqueue_seq for a Pdu that arrives with none.
1441 latrec_tstamp(pdu.enqueue_seq, LATREC_EMIT_ENTER, latrec_ctx(), ran_function_id);
1442 ErrorCode rc = queue_outbound(std::move(pdu));
1443 if (rc == ErrorCode::SUCCESS && out_message_id) {
1444 *out_message_id = mid;
1445 }
1446 return rc;
1447}
1448
1450 uint32_t ran_function_id,
1451 std::vector<uint8_t> report_data,
1452 uint32_t sequence_id,
1453 uint32_t* out_message_id
1454) {
1455 // E3-SequenceID is 1..4294967295 and mandatory on a report: with 0 every
1456 // encode fails, so refuse it here where the caller sees it.
1457 if (sequence_id == 0) {
1458 E3_LOG_ERROR(LOG_TAG) << "dApp report refused: sequence_id must be non-zero (E3-SequenceID is 1..4294967295)";
1459 return ErrorCode::INVALID_PARAM;
1460 }
1461
1462 if (!dapp_state_) return ErrorCode::STATE_ERROR;
1463 auto id = dapp_id();
1464 if (!id) return ErrorCode::NOT_INITIALIZED;
1465
1467 DAppReport r;
1468 r.dapp_identifier = *id;
1469 r.ran_function_identifier = ran_function_id;
1470 r.sequence_id = sequence_id;
1471 r.report_data = std::move(report_data);
1472 pdu.choice = std::move(r);
1473 const uint32_t mid = generate_message_id();
1474 pdu.message_id = mid;
1475 ErrorCode rc = queue_outbound(std::move(pdu));
1476 if (rc == ErrorCode::SUCCESS && out_message_id) {
1477 *out_message_id = mid;
1478 }
1479 return rc;
1480}
1481
1483 if (!dapp_state_) return ErrorCode::STATE_ERROR;
1484 auto id = dapp_id();
1485 if (!id) return ErrorCode::NOT_INITIALIZED;
1486
1488 ReleaseMessage rel;
1489 rel.dapp_identifier = *id;
1490 pdu.choice = rel;
1492 return queue_outbound(std::move(pdu));
1493}
1494
1495ErrorCode E3Interface::wait_for_setup(std::chrono::milliseconds timeout) {
1496 if (config_.role != E3Role::DAPP) return ErrorCode::INVALID_PARAM;
1497 std::unique_lock<std::mutex> lk(setup_complete_mu_);
1498 if (!setup_complete_cv_.wait_for(lk, timeout, [this]() { return setup_complete_; })) {
1499 return ErrorCode::TIMEOUT;
1500 }
1501 return setup_succeeded_ ? ErrorCode::SUCCESS : ErrorCode::CONNECTION_FAILED;
1502}
1503
1504void E3Interface::on_sm_lifecycle_change(uint32_t ran_function_id, bool should_start) {
1505 if (should_start) {
1506 ErrorCode result = SmRegistry::instance().start_sm(ran_function_id);
1507 if (result != ErrorCode::SUCCESS) {
1508 E3_LOG_ERROR(LOG_TAG) << "Failed to start SM for RAN function "
1509 << ran_function_id << ": " << error_code_to_string(result);
1510 }
1511 } else {
1512 ErrorCode result = SmRegistry::instance().stop_sm(ran_function_id);
1513 if (result != ErrorCode::SUCCESS) {
1514 E3_LOG_WARN(LOG_TAG) << "Failed to stop SM for RAN function "
1515 << ran_function_id << ": " << error_code_to_string(result);
1516 }
1517 }
1518}
1519
1520} // namespace libe3
ErrorCode queue_dapp_report(uint32_t ran_function_id, std::vector< uint8_t > report_data, uint32_t sequence_id, uint32_t *out_message_id=nullptr)
void notify_dapp_status_changed()
ErrorCode queue_dapp_control_action(uint32_t ran_function_id, uint32_t control_id, std::vector< uint8_t > action_data, uint32_t sequence_id=0, uint32_t *out_message_id=nullptr)
ErrorCode init()
Initialize the interface.
std::vector< uint32_t > subscribed_ran_functions() const
ErrorCode queue_outbound(Pdu pdu)
Queue a PDU for outbound transmission.
ErrorCode queue_subscription_delete(uint32_t ran_function_id)
~E3Interface()
Destructor - ensures clean shutdown.
ErrorCode queue_subscription_request(uint32_t ran_function_id, std::vector< uint32_t > telemetry_ids, std::vector< uint32_t > control_ids, std::optional< uint32_t > sub_time=std::nullopt, std::optional< uint32_t > periodicity=std::nullopt, uint32_t *out_request_id=nullptr)
std::vector< uint32_t > get_available_ran_functions() const
Get available RAN functions.
uint32_t generate_message_id()
Generate the next message ID, monotonic within 1..4294967295.
ErrorCode start()
Start the interface processing loops.
ErrorCode wait_for_setup(std::chrono::milliseconds timeout)
ErrorCode queue_release_message()
E3Interface(const E3Config &config)
Construct with configuration.
void stop()
Stop the interface.
std::vector< uint32_t > active_subscription_ids() const
std::vector< RanFunctionDef > remote_ran_functions() const
std::optional< uint32_t > dapp_id() const noexcept
uint32_t sanitize_request_message_id(uint32_t request_id)
Return a request id that can be echoed into a response.
ErrorCode register_sm(std::unique_ptr< ServiceModel > sm)
Register a Service Model.
static Logger & instance()
Get the singleton logger instance.
Definition logger.hpp:60
void set_level(LogLevel level) noexcept
Set the log level.
Definition logger.hpp:68
static SmRegistry & instance()
Get the singleton instance.
E3Interface - Internal protocol coordination layer.
Lock-free per-thread latency stamp recorder (header-only, C99).
static void latrec_tstamp(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2)
Definition latrec.h:685
static uint64_t latrec_tnow(void)
Definition latrec.h:695
static void latrec_ctx_set(uint64_t seq)
Definition latrec.h:696
static uint64_t latrec_seq_next(void)
Definition latrec.h:684
static void latrec_tstamp_at(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2, uint64_t t_ns)
Definition latrec.h:689
@ LATREC_SUB_BEGIN
Definition latrec.h:228
@ LATREC_EMIT_ENTER
Definition latrec.h:174
@ LATREC_DEQUEUE
Definition latrec.h:183
@ LATREC_ACK_RECV
Definition latrec.h:276
@ LATREC_SETUP_BEGIN
Definition latrec.h:221
@ LATREC_CONTEXT
Definition latrec.h:322
@ LATREC_ENCODE_E3AP_DONE
Definition latrec.h:185
@ LATREC_RECV
Definition latrec.h:193
@ LATREC_DECODE_E3AP_DONE
Definition latrec.h:194
@ LATREC_REPORT_QUEUED
Definition latrec.h:204
@ LATREC_DELIVER_BEGIN
Definition latrec.h:196
@ LATREC_SETUP_DONE
Definition latrec.h:223
@ LATREC_SEND_DONE
Definition latrec.h:186
@ LATREC_SUB_DONE
Definition latrec.h:229
@ LATREC_REPORT_DONE
Definition latrec.h:206
@ LATREC_ENQUEUE
Definition latrec.h:181
@ LATREC_DELIVER_DONE
Definition latrec.h:202
@ LATREC_DROP
Definition latrec.h:212
static uint64_t latrec_ctx(void)
Definition latrec.h:697
static latrec_t * latrec_tls_open_as(const char *role)
Definition latrec.h:682
@ LATREC_DROP_SHUTDOWN
Definition latrec.h:368
@ LATREC_DROP_FILTERED
Definition latrec.h:360
@ LATREC_DROP_DECODE
Definition latrec.h:356
@ LATREC_DROP_ENCODE
Definition latrec.h:354
@ LATREC_DROP_REPORT_QUEUE
Definition latrec.h:357
@ LATREC_DROP_QUEUE_PUSH
Definition latrec.h:353
@ LATREC_DROP_SEND
Definition latrec.h:355
@ LATREC_DROP_NO_HANDLER
Definition latrec.h:358
Logging facility for libe3.
#define E3_LOG_WARN(component)
Definition logger.hpp:226
#define E3_LOG_DEBUG(component)
Definition logger.hpp:228
#define E3_LOG_INFO(component)
Definition logger.hpp:227
#define E3_LOG_ERROR(component)
Definition logger.hpp:225
ErrorCode
Error codes returned by libe3 operations Use shared error-code list (see libe3/error_codes....
Definition types.hpp:141
AgentState
Agent state machine states.
Definition types.hpp:126
@ RUNNING
Main processing loop active.
Definition types.hpp:131
@ CONNECTING
Connection in progress.
Definition types.hpp:129
@ CONNECTED
Connected and ready for operations.
Definition types.hpp:130
@ ERROR
Error state.
Definition types.hpp:134
@ UNINITIALIZED
Agent not yet initialized.
Definition types.hpp:127
@ INITIALIZED
Agent initialized but not connected.
Definition types.hpp:128
@ STOPPING
Shutdown in progress.
Definition types.hpp:132
std::unique_ptr< E3Encoder > create_encoder(EncodingFormat format)
Factory function to create appropriate encoder.
E3Role
Role this E3 endpoint plays in the protocol.
Definition types.hpp:75
@ RAN
RAN function (binds sockets; today's only behaviour).
Definition types.hpp:76
@ DAPP
dApp (connects to a RAN agent)
Definition types.hpp:77
ResponseCode
E3AP response codes.
Definition types.hpp:101
constexpr size_t MAX_PROTOCOL_DATA_SIZE
Definition types.hpp:160
const char * pdu_type_to_string(PduType type) noexcept
Convert PduType to string representation.
Definition types.hpp:515
@ SUBSCRIPTION_DELETE
Definition types.hpp:113
@ SUBSCRIPTION_REQUEST
Definition types.hpp:112
@ DAPP_CONTROL_ACTION
Definition types.hpp:116
const char * agent_state_to_string(AgentState state) noexcept
Convert AgentState to string representation.
Definition types.hpp:588
const char * role_to_string(E3Role role) noexcept
Convert E3Role to string representation.
Definition types.hpp:605
const char * response_code_to_string(ResponseCode code) noexcept
Convert ResponseCode to string representation.
Definition types.hpp:547
std::unique_ptr< E3Connector > create_connector(E3LinkLayer link_layer, E3TransportLayer transport_layer, const std::string &setup_endpoint, const std::string &inbound_endpoint, const std::string &outbound_endpoint, uint16_t setup_port, uint16_t inbound_port, uint16_t outbound_port, size_t io_threads=2, E3Role role=E3Role::RAN)
Factory function to create appropriate connector.
const char * error_code_to_string(ErrorCode code) noexcept
Convert ErrorCode to string representation.
Definition types.hpp:558
E3AP dApp Control Action structure.
Definition types.hpp:272
uint32_t ran_function_identifier
Definition types.hpp:274
std::vector< uint8_t > action_data
Definition types.hpp:276
uint32_t control_identifier
Control identifier.
Definition types.hpp:275
E3AP dApp Report structure.
Definition types.hpp:293
uint32_t dapp_identifier
Definition types.hpp:294
uint32_t sequence_id
Definition types.hpp:299
std::vector< uint8_t > report_data
Definition types.hpp:296
uint32_t ran_function_identifier
Definition types.hpp:295
Configuration for E3Agent.
Definition types.hpp:390
int log_level
Logging verbosity: 0=none, 1=error, 2=warn, 3=info, 4=debug, 5=trace.
Definition types.hpp:473
Generic E3AP PDU structure.
Definition types.hpp:345
uint64_t enqueue_seq
Set by queue_outbound; keys the [latrec] L-stage records.
Definition types.hpp:349
PduType type
Definition types.hpp:346
PduChoice choice
Definition types.hpp:347
uint32_t message_id
Unique message identifier.
Definition types.hpp:348
E3AP Release Message structure.
Definition types.hpp:321
uint32_t dapp_identifier
dApp identifier
Definition types.hpp:322
E3AP Subscription Delete structure.
Definition types.hpp:245
uint32_t dapp_identifier
dApp identifier
Definition types.hpp:246
uint32_t subscription_id
Subscription ID to delete.
Definition types.hpp:247
E3AP Subscription Request structure.
Definition types.hpp:233
std::optional< uint32_t > periodicity
Periodicity of data delivery (0-10000 microseconds).
Definition types.hpp:239
std::optional< uint32_t > subscription_time
How long to keep the subscription (0-3600 sec).
Definition types.hpp:238
uint32_t ran_function_identifier
RAN function to subscribe to.
Definition types.hpp:235
std::vector< uint32_t > control_identifier_list
List of control identifiers.
Definition types.hpp:237
std::vector< uint32_t > telemetry_identifier_list
List of telemetry identifiers.
Definition types.hpp:236
uint32_t dapp_identifier
dApp identifier
Definition types.hpp:234