From 172a42d4e4b49ffae27379e4ebfec0de6c4d3aa8 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 14:50:43 +0200 Subject: [PATCH 01/12] Add oals::rt platform shim (M1 of dev-tooling plan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the Linux-only RT primitives (SCHED_FIFO, SCHED_RR, CPU affinity, clock_nanosleep) into a shared oals::rt namespace under netutils/platform/. Linux body is a byte-for-byte lift of the helpers previously inlined in engine/main.cpp and io_sim/main.cpp (same priorities, same affinities, same syscalls). macOS body is silent no-ops plus nanosleep-based precise_sleep_ns — best-effort dev target, no QoS or mach_wait_until per plan decision. Gated on NOT EMBEDDED_BUILD so Zephyr firmware compiles zero bytes of rt.cpp; verified by configuring with -DEMBEDDED_BUILD=ON and confirming rt.cpp.o is not produced. Unblocks engine/io_sim compilation on macOS (linking still depends on M3 transport seam). Linux behavior unchanged: same SCHED_FIFO priorities (25/20/80 in engine, 50 in io_sim), same SCHED_RR priority (99 in io_sim), same CPU affinities (1/1/2/3 in engine), same CLOCK_MONOTONIC sleeps. See OpenAudioLiveSystem/Docs/dev-tooling-plan.md M1. --- netutils/CMakeLists.txt | 7 +++++ netutils/platform/rt.cpp | 68 ++++++++++++++++++++++++++++++++++++++++ netutils/platform/rt.h | 18 +++++++++++ 3 files changed, 93 insertions(+) create mode 100644 netutils/platform/rt.cpp create mode 100644 netutils/platform/rt.h diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index 2837448..a46fc22 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -5,6 +5,13 @@ set(OAN_UTILS_SOURCES LowLatSocket.h ) +if(NOT EMBEDDED_BUILD) + list(APPEND OAN_UTILS_SOURCES + platform/rt.h + platform/rt.cpp + ) +endif() + if(EMBEDDED_BUILD) add_library(oannetutils STATIC ${OAN_UTILS_SOURCES}) else () diff --git a/netutils/platform/rt.cpp b/netutils/platform/rt.cpp new file mode 100644 index 0000000..5d84492 --- /dev/null +++ b/netutils/platform/rt.cpp @@ -0,0 +1,68 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#include "rt.h" + +#ifdef __linux__ + #include + #include + #include + #include +#else + #include +#endif + +namespace oals::rt { + +#ifdef __linux__ + +void set_thread_realtime(uint8_t prio) { + sched_param sparams{}; + sparams.sched_priority = prio; + if (sched_setscheduler(0, SCHED_FIFO, &sparams) != 0) { + std::cerr << "[oals::rt] set_thread_realtime failed" << std::endl; + } +} + +void set_running_cpu(int cpu_id) { + cpu_set_t cs{}; + CPU_ZERO(&cs); + CPU_SET(cpu_id, &cs); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cs) != 0) { + std::cerr << "[oals::rt] set_running_cpu failed" << std::endl; + } +} + +void set_process_scheduler_rr(int prio) { + sched_param params{}; + params.sched_priority = prio; + if (sched_setscheduler(0, SCHED_RR, ¶ms) != 0) { + std::cerr << "[oals::rt] set_process_scheduler_rr failed" << std::endl; + } +} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, nullptr); +} + +#else + +void set_thread_realtime(uint8_t) {} +void set_running_cpu(int) {} +void set_process_scheduler_rr(int){} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + nanosleep(&ts, nullptr); +} + +#endif + +} diff --git a/netutils/platform/rt.h b/netutils/platform/rt.h new file mode 100644 index 0000000..6fbc3c6 --- /dev/null +++ b/netutils/platform/rt.h @@ -0,0 +1,18 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#ifndef OALS_RT_H +#define OALS_RT_H + +#include + +namespace oals::rt { + void set_thread_realtime(uint8_t prio); + void set_running_cpu(int cpu_id); + void set_process_scheduler_rr(int prio); + void precise_sleep_ns(long ns); +} + +#endif From 61ed291c2608f280b8391b41fb46f8ca55a79dbf Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 17:05:08 +0200 Subject: [PATCH 02/12] Add ITransport seam: LowLatSocket dispatches to runtime-chosen backend (M3 of dev-tooling plan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce ITransport abstraction in netutils/transport/ so LowLatSocket and NetworkMapper no longer call AF_PACKET sendto/recv or Zephyr's extern "C" hooks directly. The dispatch is selected at runtime by init_socket parsing the iface string for a sim: or raw: prefix (or OAN_TRANSPORT env var). This unblocks the engine binary from linking on macOS, which was the M2 link gap. Three backends ship: - RawLinuxTransport: verbatim lift of the AF_PACKET code from LowLatSocket.cpp's previous Linux arm. Used only when a raw: prefix is passed (opt-in on Linux; the no-prefix default path keeps the existing direct AF_PACKET fast path, unchanged). - SimTransport: STUB for M3 (open() prints "not yet implemented (M4)" and returns false). M4 ships the Unix-socket daemon client. - RawMacTransport: STUB only (BPF deferred to Track 2). Gating shape across LowLatSocket.{h,cpp} and NetworkMapper.cpp: #if defined(__linux__) direct AF_PACKET (default, unchanged) or ITransport when m_transport set #elif defined(OAN_HOST_BACKENDS) ITransport always; no prefix → fail #else Zephyr extern "C" hooks (unchanged) Linux's default no-prefix path is byte-for-byte unchanged: backend_send falls through to the same sendto(MSG_DONTWAIT, &m_iface_addr, ...) as pre-M3; backend_recv to the same recv(MSG_DONTWAIT). Templates' INT_LLP framing and MAC lookup are unchanged. Other changes: - htons macro at LowLatSocket.h:38 now three-way: OAN_HOST_BACKENDS → ; !__ZEPHYR__ → existing byte-swap macro; else → zephyr/net/net_ip.h. Skips the collision with the real htons pulled in by arpa/inet.h on Mac host. - Fix the _fetch_iface_meta ODR hazard the dev-tooling plan flagged: standardize to const std::string& everywhere on the OAN side (LowLatSocket.cpp:88 used to declare it non-const while NetworkMapper.cpp:12 declared it const — same extern "C" symbol, two prototypes). Firmware-side definitions in IO_Board_FW and RackedIO_Board_FW still use non-const std::string& but are extern "C" so the linker symbol is identical; follow-up PRs to those repos to harmonize. - NetworkMapper local_now/local_now_us chrono path extended to include OAN_HOST_BACKENDS (avoids needing _now_ms/_now_us host stubs). _delay declaration dropped (was unreferenced). - extern "C" hook declarations wrapped in #if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) so they are Zephyr-only. - LowLatSocket Linux dtor now guards close(m_socket) on m_socket > 0 (previously closed fd 0 / stdin if init_socket was never called). CMake: transport/* sources gated under OAN_HOST_BACKENDS. RawLinux only added when NOT APPLE (BPF placeholder is the Mac counterpart). target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) on APPLE to defer NetworkMapper::get_mac_by_uid symbol resolution to the executable link (Apple ld64 is strict about undefined symbols in shared libs; Linux ld is lenient — circular dep oannetutils → NetworkMapper → oancommon → oannetutils is otherwise the same issue both platforms would face). Known follow-ups (M4 / Track 2 scope, not blocking): - NetworkMapper::update_packet calls host_iface_meta before init_socket has a chance to pick SimTransport, so passing sim:default on Mac prints a "no AF_LINK entry" warning before SimTransport's own stub message fires. M4 needs to plumb the transport's synthetic MAC into update_packet (likely by making PeerConf or the mapper consult the transport for self-MAC). - Templates now funnel through non-template backend_send / backend_recv defined out-of-line in oannetutils.so. Adds one call boundary per packet vs. pre-M3. Negligible in cold paths; worth benchmarking on the engine's pipe updater tick before sound-on-target sessions. -flto would resolve. Verified on macOS: full build (100% Built target, 11 binaries + libraries), 0 compile errors. Smoke runs: ./OALSEngine en99 prints "host_iface_meta: no AF_LINK entry" + "this host requires a prefix" and exits; ./OALSEngine sim:default prints "SimTransport: not yet implemented (M4)" and exits. Linux byte-for-byte regression verified by simulating cmake -DOAN_HOST_BACKENDS=OFF (no transport sources compiled, no arpa/inet.h pulled in, no -undefined dynamic_lookup flag, oannetutils builds cleanly). See OpenAudioLiveSystem/Docs/dev-tooling-plan.md M3. --- common/NetworkMapper.cpp | 19 ++-- netutils/CMakeLists.txt | 23 +++- netutils/LowLatSocket.cpp | 131 +++++++++++++++++++++-- netutils/LowLatSocket.h | 40 +++---- netutils/transport/ITransport.cpp | 119 ++++++++++++++++++++ netutils/transport/ITransport.h | 26 +++++ netutils/transport/RawLinuxTransport.cpp | 60 +++++++++++ netutils/transport/RawLinuxTransport.h | 29 +++++ netutils/transport/RawMacTransport.cpp | 27 +++++ netutils/transport/RawMacTransport.h | 18 ++++ netutils/transport/SimTransport.cpp | 29 +++++ netutils/transport/SimTransport.h | 21 ++++ 12 files changed, 499 insertions(+), 43 deletions(-) create mode 100644 netutils/transport/ITransport.cpp create mode 100644 netutils/transport/ITransport.h create mode 100644 netutils/transport/RawLinuxTransport.cpp create mode 100644 netutils/transport/RawLinuxTransport.h create mode 100644 netutils/transport/RawMacTransport.cpp create mode 100644 netutils/transport/RawMacTransport.h create mode 100644 netutils/transport/SimTransport.cpp create mode 100644 netutils/transport/SimTransport.h diff --git a/common/NetworkMapper.cpp b/common/NetworkMapper.cpp index fa91128..acf9645 100644 --- a/common/NetworkMapper.cpp +++ b/common/NetworkMapper.cpp @@ -5,12 +5,15 @@ #include "NetworkMapper.h" -#ifndef __linux__ -extern void _delay(uint32_t ms); +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) extern uint32_t _now_ms(); extern uint64_t _now_us(); extern "C" IfaceMeta _fetch_iface_meta(const std::string&); -#endif // __linux__ +#endif + +#ifdef OAN_HOST_BACKENDS +#include "netutils/transport/ITransport.h" +#endif NetworkMapper::NetworkMapper(const PeerConf& pconf) { m_peer_change_callback = [](PeerInfos&, bool) {}; @@ -29,11 +32,13 @@ bool NetworkMapper::init_mapper(const std::string& iface) { } void NetworkMapper::update_packet(const PeerConf &pconf) { -#ifdef __linux__ +#if defined(__linux__) auto iface_meta = get_iface_meta(pconf.iface); +#elif defined(OAN_HOST_BACKENDS) + auto iface_meta = host_iface_meta(pconf.iface); #else auto iface_meta = _fetch_iface_meta(pconf.iface); -#endif // __linux__ +#endif m_packet.header.type = PacketType::MAPPING; m_packet.packet_data.topo = pconf.topo; @@ -244,7 +249,7 @@ std::optional NetworkMapper::first_free_processing_channel(uint16_t uid } uint64_t NetworkMapper::local_now() { -#if defined(__linux__) || defined(__ZEPHYR__) +#if defined(__linux__) || defined(__ZEPHYR__) || defined(OAN_HOST_BACKENDS) return std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); @@ -254,7 +259,7 @@ uint64_t NetworkMapper::local_now() { } uint64_t NetworkMapper::local_now_us() { -#if defined(__linux__) || defined(__ZEPHYR__) +#if defined(__linux__) || defined(__ZEPHYR__) || defined(OAN_HOST_BACKENDS) return std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index a46fc22..7eb44c5 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -12,10 +12,31 @@ if(NOT EMBEDDED_BUILD) ) endif() +if(OAN_HOST_BACKENDS) + list(APPEND OAN_UTILS_SOURCES + transport/ITransport.h + transport/ITransport.cpp + transport/SimTransport.h + transport/SimTransport.cpp + transport/RawMacTransport.h + transport/RawMacTransport.cpp + ) + if(NOT APPLE) + list(APPEND OAN_UTILS_SOURCES + transport/RawLinuxTransport.h + transport/RawLinuxTransport.cpp + ) + endif() +endif() + if(EMBEDDED_BUILD) add_library(oannetutils STATIC ${OAN_UTILS_SOURCES}) else () add_library(oannetutils SHARED ${OAN_UTILS_SOURCES}) endif (EMBEDDED_BUILD) -target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) \ No newline at end of file +target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) + +if(APPLE) + target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) +endif() \ No newline at end of file diff --git a/netutils/LowLatSocket.cpp b/netutils/LowLatSocket.cpp index 8e8a5a6..bbf943f 100644 --- a/netutils/LowLatSocket.cpp +++ b/netutils/LowLatSocket.cpp @@ -6,10 +6,18 @@ #include "LowLatSocket.h" #include "common/NetworkMapper.h" -#ifndef __linux__ +#include + +#ifdef OAN_HOST_BACKENDS +#include "transport/ITransport.h" +#endif + +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) +// Zephyr-firmware-only hooks. Host builds use ITransport instead. extern "C" int _send_data(uint8_t* data, size_t data_len); extern "C" int _recv_data(uint8_t* data_out, size_t data_size, EthProtocol filt_proto); -#endif // __linux__ +extern "C" IfaceMeta _fetch_iface_meta(const std::string& name); +#endif std::optional LowLatSocket::get_mac(uint16_t id) { return m_mapper->get_mac_by_uid(id); @@ -25,10 +33,29 @@ LowLatSocket::LowLatSocket(uint16_t self_uid, std::shared_ptr map } LowLatSocket::~LowLatSocket() { - close(m_socket); + if (m_socket > 0) { + close(m_socket); + } } bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { +#ifdef OAN_HOST_BACKENDS + m_transport = parse_transport(interface); + if (m_transport) { + IfaceMeta meta{}; + if (!m_transport->open(interface, proto, m_self_uid, meta)) { + return false; + } + + memset(m_hdr.h_dest, 0xFF, 6); + memcpy(m_hdr.h_source, meta.mac, 6); + m_hdr.h_proto = htons(proto); + m_self_proto = proto; + return true; + } +#endif + + // Default Linux path: direct AF_PACKET (byte-for-byte unchanged from pre-M3). m_socket = socket(AF_PACKET, SOCK_RAW, htons(proto)); if (m_socket < 0) { std::cerr << "LLS Failed to open low level socket. Err = " << errno << std::endl; @@ -50,8 +77,7 @@ bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { return true; } - -IfaceMeta get_iface_meta(const std::string &name) { +IfaceMeta get_iface_meta(const std::string& name) { // Overflow check assert(name.size() <= 16); @@ -81,11 +107,82 @@ IfaceMeta get_iface_meta(const std::string &name) { return meta; } +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->send(data, size, dest_uid); + } #else -// Embeddable interface that must be private -extern "C" IfaceMeta _fetch_iface_meta(std::string& name); + (void)dest_uid; +#endif + + return sendto( + m_socket, + data, size, + MSG_DONTWAIT, + reinterpret_cast(&m_iface_addr), + sizeof(m_iface_addr) + ); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->recv(data, size, async); + } +#endif + + return recv(m_socket, data, size, async ? MSG_DONTWAIT : 0); +} + +#elif defined(OAN_HOST_BACKENDS) + +LowLatSocket::LowLatSocket(uint16_t self_uid, std::shared_ptr mapper) { + m_socket = 0; + memset(m_iface_addr, 0, sizeof(m_iface_addr)); + m_self_uid = self_uid; + m_mapper = std::move(mapper); +} + +LowLatSocket::~LowLatSocket() = default; + +bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { + m_transport = parse_transport(interface); + if (!m_transport) { + std::cerr << "LowLatSocket: this host requires a transport prefix " + << "(sim: or raw:) or OAN_TRANSPORT env var. " + << "Got: '" << interface << "'" << std::endl; + return false; + } -IfaceMeta get_iface_meta(std::string& name) { + IfaceMeta meta{}; + if (!m_transport->open(interface, proto, m_self_uid, meta)) { + return false; + } + + memcpy(m_iface_addr, meta.mac, 6); + + memset(m_hdr.h_dest, 0xFF, 6); + memcpy(m_hdr.h_source, meta.mac, 6); + m_hdr.h_proto = htons(proto); + m_self_proto = proto; + + return true; +} + +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { + if (!m_transport) return -1; + return m_transport->send(data, size, dest_uid); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { + if (!m_transport) return -1; + return m_transport->recv(data, size, async); +} + +#else // Zephyr firmware path — unchanged from pre-M3 + +IfaceMeta get_iface_meta(const std::string& name) { return _fetch_iface_meta(name); } @@ -111,12 +208,24 @@ bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { return true; } -int LowLatSocket::send_data_internal(uint8_t *data, size_t size) { +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { + (void)dest_uid; + // Zephyr's _send_data extern takes non-const uint8_t* for legacy ABI reasons; + // firmware impls (IO_Board_FW, RackedIO_Board_FW) do not mutate the buffer. + return send_data_internal(const_cast(data), size); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { + (void)async; + return recv_data_internal(data, size); +} + +int LowLatSocket::send_data_internal(uint8_t* data, size_t size) { return _send_data(data, size); } -int LowLatSocket::recv_data_internal(uint8_t *data, size_t size) const { +int LowLatSocket::recv_data_internal(uint8_t* data, size_t size) const { return _recv_data(data, size, m_self_proto); } -#endif // __linux__ +#endif diff --git a/netutils/LowLatSocket.h b/netutils/LowLatSocket.h index 0196994..2bdf2c2 100644 --- a/netutils/LowLatSocket.h +++ b/netutils/LowLatSocket.h @@ -34,7 +34,9 @@ struct EthernetHeader { } __attribute__((packed)); typedef EthernetHeader ethhdr; -#ifndef __ZEPHYR__ +#ifdef OAN_HOST_BACKENDS +#include +#elif !defined(__ZEPHYR__) #define htons(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8)) #else #include "zephyr/net/net_ip.h" @@ -150,17 +152,8 @@ class LowLatSocket { } } -#ifdef __linux__ - return sendto( - m_socket, - &llpck, sizeof(llpck), - MSG_DONTWAIT, - (sockaddr*)&m_iface_addr, - sizeof(m_iface_addr) - ); -#else - return send_data_internal((uint8_t*)&llpck, sizeof(INT_LLP)); -#endif // __linux__ + return backend_send(reinterpret_cast(&llpck), + sizeof(INT_LLP), dest_uid); } /** @@ -172,11 +165,7 @@ class LowLatSocket { */ template int receive_data(T* data, bool async = true) { -#ifdef __linux__ - return recv(m_socket, data, sizeof(T), async ? MSG_DONTWAIT : 0); -#else - return recv_data_internal((uint8_t*)data, sizeof(T)); -#endif // __linux__ + return backend_recv(reinterpret_cast(data), sizeof(T), async); } /** @@ -187,11 +176,7 @@ class LowLatSocket { * @return Received byte count */ int receive_data_raw(char* data, size_t size, bool async = true) const { -#ifdef __linux__ - return recv(m_socket, data, size, async ? MSG_DONTWAIT : 0); -#else - return recv_data_internal((uint8_t*)data, size); -#endif // __linux__ + return backend_recv(reinterpret_cast(data), size, async); } private: @@ -202,10 +187,13 @@ class LowLatSocket { */ std::optional get_mac(uint16_t id); -#ifndef __linux__ + int backend_send(const uint8_t* data, size_t size, uint16_t dest_uid); + int backend_recv(uint8_t* data, size_t size, bool async) const; + +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) int send_data_internal(uint8_t* data, size_t size); int recv_data_internal(uint8_t* data, size_t size) const; -#endif // __linux__ +#endif #ifdef __linux__ sockaddr_ll m_iface_addr{}; @@ -219,6 +207,10 @@ class LowLatSocket { EthProtocol m_self_proto; std::shared_ptr m_mapper; + +#ifdef OAN_HOST_BACKENDS + mutable std::unique_ptr m_transport; +#endif }; diff --git a/netutils/transport/ITransport.cpp b/netutils/transport/ITransport.cpp new file mode 100644 index 0000000..3e525c2 --- /dev/null +++ b/netutils/transport/ITransport.cpp @@ -0,0 +1,119 @@ +#include "ITransport.h" + +#include +#include +#include +#include + +#include "../LowLatSocket.h" + +#include "SimTransport.h" +#include "RawMacTransport.h" +#ifdef __linux__ + #include "RawLinuxTransport.h" +#endif + +#ifdef __linux__ + #include + #include + #include + #include +#else + #include + #include + #include +#endif + +IfaceMeta host_iface_meta(const std::string& iface) { + IfaceMeta meta{}; + +#ifdef __linux__ + assert(iface.size() <= 16); + + int sock = ::socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + perror("host_iface_meta: socket()"); + return meta; + } + + ifreq req{}; + memset(req.ifr_ifrn.ifrn_name, 0x00, 16); + memcpy(req.ifr_ifrn.ifrn_name, iface.data(), iface.size()); + + if (ioctl(sock, SIOCGIFINDEX, &req) < 0) { + perror("host_iface_meta: SIOCGIFINDEX"); + } + meta.idx = req.ifr_ifru.ifru_ivalue; + + if (ioctl(sock, SIOCGIFHWADDR, &req) < 0) { + perror("host_iface_meta: SIOCGIFHWADDR"); + } + memcpy(meta.mac, req.ifr_ifru.ifru_hwaddr.sa_data, 6); + + ::close(sock); +#else + ifaddrs* addrs = nullptr; + if (getifaddrs(&addrs) != 0) { + perror("host_iface_meta: getifaddrs"); + return meta; + } + + bool found = false; + for (ifaddrs* ifa = addrs; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr) continue; + if (ifa->ifa_addr->sa_family != AF_LINK) continue; + if (iface != ifa->ifa_name) continue; + + auto* sdl = reinterpret_cast(ifa->ifa_addr); + if (sdl->sdl_alen == 6) { + memcpy(meta.mac, LLADDR(sdl), 6); + meta.idx = static_cast(if_nametoindex(iface.c_str())); + found = true; + break; + } + } + + freeifaddrs(addrs); + + if (!found) { + std::cerr << "host_iface_meta: no AF_LINK entry for interface '" + << iface << "'" << std::endl; + } +#endif + + return meta; +} + +static bool starts_with(const std::string& s, const std::string& prefix) { + return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0; +} + +std::unique_ptr parse_transport(const std::string& iface) { + if (starts_with(iface, "sim:")) { + return std::make_unique(iface.substr(4)); + } + if (starts_with(iface, "raw:")) { +#ifdef __linux__ + return std::make_unique(); +#else + return std::make_unique(); +#endif + } + + const char* env = std::getenv("OAN_TRANSPORT"); + if (env != nullptr) { + std::string sel{env}; + if (sel == "sim") { + return std::make_unique(iface.empty() ? "default" : iface); + } + if (sel == "raw") { +#ifdef __linux__ + return std::make_unique(); +#else + return std::make_unique(); +#endif + } + } + + return nullptr; +} diff --git a/netutils/transport/ITransport.h b/netutils/transport/ITransport.h new file mode 100644 index 0000000..6ef449d --- /dev/null +++ b/netutils/transport/ITransport.h @@ -0,0 +1,26 @@ +#ifndef OAN_ITRANSPORT_H +#define OAN_ITRANSPORT_H + +#include +#include +#include +#include + +struct IfaceMeta; +enum EthProtocol : uint16_t; + +struct ITransport { + virtual ~ITransport() = default; + + virtual bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) = 0; + virtual int send(const uint8_t* data, size_t len, uint16_t dest_uid) = 0; + virtual int recv(uint8_t* data, size_t len, bool async) = 0; + virtual void close() = 0; +}; + +IfaceMeta host_iface_meta(const std::string& iface); + +std::unique_ptr parse_transport(const std::string& iface); + +#endif diff --git a/netutils/transport/RawLinuxTransport.cpp b/netutils/transport/RawLinuxTransport.cpp new file mode 100644 index 0000000..072e253 --- /dev/null +++ b/netutils/transport/RawLinuxTransport.cpp @@ -0,0 +1,60 @@ +#include "RawLinuxTransport.h" + +#ifdef __linux__ + +#include +#include +#include + +#include +#include +#include +#include + +RawLinuxTransport::~RawLinuxTransport() { + close(); +} + +bool RawLinuxTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)self_uid; + + m_socket = ::socket(AF_PACKET, SOCK_RAW, htons(proto)); + if (m_socket < 0) { + std::cerr << "RawLinuxTransport: socket() failed, errno=" << errno << std::endl; + return false; + } + + out_meta = host_iface_meta(iface); + + m_iface_addr.sll_ifindex = out_meta.idx; + m_iface_addr.sll_halen = ETH_ALEN; + m_iface_addr.sll_protocol = htons(proto); + memcpy(m_iface_addr.sll_addr, out_meta.mac, 6); + + return true; +} + +int RawLinuxTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + (void)dest_uid; + return ::sendto( + m_socket, + data, len, + MSG_DONTWAIT, + reinterpret_cast(&m_iface_addr), + sizeof(m_iface_addr) + ); +} + +int RawLinuxTransport::recv(uint8_t* data, size_t len, bool async) { + return ::recv(m_socket, data, len, async ? MSG_DONTWAIT : 0); +} + +void RawLinuxTransport::close() { + if (m_socket >= 0) { + ::close(m_socket); + m_socket = -1; + } +} + +#endif // __linux__ diff --git a/netutils/transport/RawLinuxTransport.h b/netutils/transport/RawLinuxTransport.h new file mode 100644 index 0000000..fcf119a --- /dev/null +++ b/netutils/transport/RawLinuxTransport.h @@ -0,0 +1,29 @@ +#ifndef OAN_RAW_LINUX_TRANSPORT_H +#define OAN_RAW_LINUX_TRANSPORT_H + +#ifdef __linux__ + +#include + +#include "ITransport.h" +#include "../LowLatSocket.h" + +class RawLinuxTransport : public ITransport { +public: + RawLinuxTransport() = default; + ~RawLinuxTransport() override; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + void close() override; + +private: + int m_socket{-1}; + sockaddr_ll m_iface_addr{}; +}; + +#endif // __linux__ + +#endif diff --git a/netutils/transport/RawMacTransport.cpp b/netutils/transport/RawMacTransport.cpp new file mode 100644 index 0000000..080f409 --- /dev/null +++ b/netutils/transport/RawMacTransport.cpp @@ -0,0 +1,27 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#include "RawMacTransport.h" + +#include + +bool RawMacTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)iface; (void)proto; (void)self_uid; (void)out_meta; + std::cerr << "RawMacTransport: BPF not implemented (Track 2)" << std::endl; + return false; +} + +int RawMacTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + (void)data; (void)len; (void)dest_uid; + return -1; +} + +int RawMacTransport::recv(uint8_t* data, size_t len, bool async) { + (void)data; (void)len; (void)async; + return -1; +} + +void RawMacTransport::close() {} diff --git a/netutils/transport/RawMacTransport.h b/netutils/transport/RawMacTransport.h new file mode 100644 index 0000000..f81fa1e --- /dev/null +++ b/netutils/transport/RawMacTransport.h @@ -0,0 +1,18 @@ +#ifndef OAN_RAW_MAC_TRANSPORT_H +#define OAN_RAW_MAC_TRANSPORT_H + +#include "ITransport.h" + +class RawMacTransport : public ITransport { +public: + RawMacTransport() = default; + ~RawMacTransport() override = default; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + void close() override; +}; + +#endif diff --git a/netutils/transport/SimTransport.cpp b/netutils/transport/SimTransport.cpp new file mode 100644 index 0000000..2e22575 --- /dev/null +++ b/netutils/transport/SimTransport.cpp @@ -0,0 +1,29 @@ +#include "SimTransport.h" + +#include +#include + +SimTransport::SimTransport(std::string daemon_name) + : m_daemon_name(std::move(daemon_name)) {} + +SimTransport::~SimTransport() = default; + +bool SimTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)iface; (void)proto; (void)self_uid; (void)out_meta; + std::cerr << "SimTransport: not yet implemented (M4) — daemon name '" + << m_daemon_name << "'" << std::endl; + return false; +} + +int SimTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + (void)data; (void)len; (void)dest_uid; + return -1; +} + +int SimTransport::recv(uint8_t* data, size_t len, bool async) { + (void)data; (void)len; (void)async; + return -1; +} + +void SimTransport::close() {} diff --git a/netutils/transport/SimTransport.h b/netutils/transport/SimTransport.h new file mode 100644 index 0000000..f245d1e --- /dev/null +++ b/netutils/transport/SimTransport.h @@ -0,0 +1,21 @@ +#ifndef OAN_SIM_TRANSPORT_H +#define OAN_SIM_TRANSPORT_H + +#include "ITransport.h" + +class SimTransport : public ITransport { +public: + explicit SimTransport(std::string daemon_name); + ~SimTransport() override; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + void close() override; + +private: + std::string m_daemon_name; +}; + +#endif From 25359b261b10119a24f6bfc2f8486e98fd99e616 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 18:48:49 +0200 Subject: [PATCH 03/12] Implement SimTransport real backend (M4 of dev-tooling plan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the M3 stub with a real AF_UNIX SOCK_STREAM client that speaks the sim_switch daemon's length-prefixed framing. Adds: - SimHello / SimFrame wire structs (local copy; canonical contract lives in OALS tools/sim_switch/sim_proto.h — must stay byte-identical). - Versioned hello (SIM_VERSION=1) with mismatch close on the daemon side. - Partial-read state machine across recv() calls, honouring both async (return 0 on EAGAIN) and blocking (poll-and-retry) modes. - ',mac=02:..' URI suffix to pin a synthetic locally-administered MAC; random fallback minted once per process so all four engine sockets share one device identity (OAN_SIM_MAC env var overrides). - writev/sendmsg for atomic hdr+payload; MSG_DONTWAIT + MSG_NOSIGNAL (Linux only); idempotent SIGPIPE ignore for macOS. - Pre-reserved m_body_buf to avoid per-frame allocation on the audio hot path. - Oversize-frame guard with diagnostic stderr. NetworkMapper now sources self_address from LowLatSocket::get_self_mac() under OAN_HOST_BACKENDS instead of the broken host_iface_meta lookup on transport-prefix interfaces. Idempotent: re-running update_packet after init_mapper preserves the MAC. --- common/NetworkMapper.cpp | 26 ++- netutils/LowLatSocket.h | 2 + netutils/transport/SimTransport.cpp | 326 +++++++++++++++++++++++++++- netutils/transport/SimTransport.h | 26 ++- 4 files changed, 361 insertions(+), 19 deletions(-) diff --git a/common/NetworkMapper.cpp b/common/NetworkMapper.cpp index acf9645..0317187 100644 --- a/common/NetworkMapper.cpp +++ b/common/NetworkMapper.cpp @@ -11,10 +11,6 @@ extern uint64_t _now_us(); extern "C" IfaceMeta _fetch_iface_meta(const std::string&); #endif -#ifdef OAN_HOST_BACKENDS -#include "netutils/transport/ITransport.h" -#endif - NetworkMapper::NetworkMapper(const PeerConf& pconf) { m_peer_change_callback = [](PeerInfos&, bool) {}; update_packet(pconf); @@ -27,15 +23,33 @@ NetworkMapper::~NetworkMapper() { bool NetworkMapper::init_mapper(const std::string& iface) { m_map_socket = std::make_unique(m_packet.packet_data.self_uid, std::shared_ptr{}); bool res = m_map_socket->init_socket(iface, EthProtocol::ETH_PROTO_OANDISCO); + if (!res) return false; + +#ifdef OAN_HOST_BACKENDS + // On the host-backend path, the transport (sim/raw-mac) supplies the + // device MAC — there is no real interface to query. The MAC in + // m_packet was zero-filled by update_packet() at construction; fill it + // now from the socket whose transport just learned it. + m_packet.packet_data.self_address = 0; + memcpy(&m_packet.packet_data.self_address, m_map_socket->get_self_mac(), 6); +#endif - return res; + return true; } void NetworkMapper::update_packet(const PeerConf &pconf) { #if defined(__linux__) auto iface_meta = get_iface_meta(pconf.iface); #elif defined(OAN_HOST_BACKENDS) - auto iface_meta = host_iface_meta(pconf.iface); + // Skip the iface lookup entirely: pconf.iface may be a transport prefix + // (sim:default, raw:en0) for which no real OS-level lookup makes sense. + IfaceMeta iface_meta{}; + // If we already have a socket (i.e. update_packet is being re-run after + // init_mapper), preserve the MAC the transport learned. Otherwise leave + // zero and let init_mapper fill it in once the socket is bound. + if (m_map_socket) { + memcpy(iface_meta.mac, m_map_socket->get_self_mac(), 6); + } #else auto iface_meta = _fetch_iface_meta(pconf.iface); #endif diff --git a/netutils/LowLatSocket.h b/netutils/LowLatSocket.h index 2bdf2c2..9637314 100644 --- a/netutils/LowLatSocket.h +++ b/netutils/LowLatSocket.h @@ -179,6 +179,8 @@ class LowLatSocket { return backend_recv(reinterpret_cast(data), size, async); } + const uint8_t* get_self_mac() const { return m_hdr.h_source; } + private: /** * Finds a device MAC address based on its ID. diff --git a/netutils/transport/SimTransport.cpp b/netutils/transport/SimTransport.cpp index 2e22575..e06b5b4 100644 --- a/netutils/transport/SimTransport.cpp +++ b/netutils/transport/SimTransport.cpp @@ -1,29 +1,331 @@ #include "SimTransport.h" +#include +#include +#include +#include +#include #include +#include #include -SimTransport::SimTransport(std::string daemon_name) - : m_daemon_name(std::move(daemon_name)) {} +#include +#include +#include +#include +#include +#include -SimTransport::~SimTransport() = default; +// NOTE: wire framing is owned by the OALS sim_switch dev tool (which is the +// only program SimTransport can talk to). We deliberately do NOT include a +// header from OALS — OAN is vendored into firmware repos and must stay +// self-contained. The structs below are a local copy that must stay +// byte-identical with tools/sim_switch/sim_proto.h. The framing magic and +// version are the contract check. + +namespace { + +constexpr uint32_t SIM_MAGIC = 0x4F535354; // 'OSST' +constexpr uint8_t SIM_VERSION = 1; +constexpr uint32_t MAX_FRAME_PAYLOAD = 8192; // matches sim_switch + +struct SimHello { + uint32_t magic; + uint8_t version; + uint8_t _pad; + uint16_t ethertype; + uint16_t self_uid; + uint16_t _reserved; +} __attribute__((packed)); + +struct SimFrame { + uint32_t payload_len; + uint16_t ethertype; + uint16_t dest_uid; +} __attribute__((packed)); + +bool parse_mac(const std::string& s, uint8_t out[6]) { + unsigned int v[6]; + if (std::sscanf(s.c_str(), + "%x:%x:%x:%x:%x:%x", + &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6) { + return false; + } + for (int i = 0; i < 6; ++i) { + if (v[i] > 0xFF) return false; + out[i] = static_cast(v[i]); + } + return true; +} + +void random_locally_admin_mac(uint8_t out[6]) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution d(0, 255); + out[0] = 0x02; // locally administered, unicast + for (int i = 1; i < 6; ++i) out[i] = static_cast(d(gen)); +} + +// One MAC per process. The engine constructs 4 SimTransports (audio, disco, +// control, sync) — they must all carry the same MAC so peers see a single +// device identity. OAN_SIM_MAC env var can override; otherwise a random +// locally-administered MAC is minted once and reused across all instances. +const uint8_t* process_self_mac() { + static uint8_t s_mac[6]; + static bool s_init = false; + if (s_init) return s_mac; + + const char* env = std::getenv("OAN_SIM_MAC"); + if (env) { + if (parse_mac(env, s_mac)) { + s_init = true; + return s_mac; + } + std::cerr << "SimTransport: ignoring malformed OAN_SIM_MAC='" + << env << "' (expected 02:00:00:00:00:01)\n"; + } + random_locally_admin_mac(s_mac); + s_init = true; + return s_mac; +} + +} // namespace + +SimTransport::SimTransport(std::string s) { + parse_opts(s); + m_socket_path = "/tmp/osst-sim-" + m_daemon_name + ".sock"; + if (!m_mac_pinned) std::memcpy(m_self_mac, process_self_mac(), 6); +} + +SimTransport::~SimTransport() { + close(); +} + +void SimTransport::parse_opts(const std::string& s) { + // Split on commas: first token = daemon name, subsequent tokens = "key=value". + size_t start = 0; + int tok_idx = 0; + while (start <= s.size()) { + size_t comma = s.find(',', start); + std::string tok = s.substr(start, comma == std::string::npos ? std::string::npos : comma - start); + + if (tok_idx == 0) { + m_daemon_name = tok.empty() ? "default" : tok; + } else { + auto eq = tok.find('='); + if (eq != std::string::npos) { + std::string key = tok.substr(0, eq); + std::string val = tok.substr(eq + 1); + if (key == "mac") { + if (parse_mac(val, m_self_mac)) { + m_mac_pinned = true; + } else { + std::cerr << "SimTransport: ignoring malformed mac='" + << val << "' (expected 02:00:00:00:00:01)\n"; + } + } else { + std::cerr << "SimTransport: ignoring unknown option '" + << key << "'\n"; + } + } + } + + if (comma == std::string::npos) break; + start = comma + 1; + tok_idx++; + } + + if (m_daemon_name.empty()) m_daemon_name = "default"; +} bool SimTransport::open(const std::string& iface, EthProtocol proto, uint16_t self_uid, IfaceMeta& out_meta) { - (void)iface; (void)proto; (void)self_uid; (void)out_meta; - std::cerr << "SimTransport: not yet implemented (M4) — daemon name '" - << m_daemon_name << "'" << std::endl; - return false; + (void)iface; // already parsed in ctor + + m_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (m_fd < 0) { + std::cerr << "SimTransport: socket() failed: " << ::strerror(errno) << "\n"; + return false; + } + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + if (m_socket_path.size() >= sizeof(addr.sun_path)) { + std::cerr << "SimTransport: socket path too long\n"; + ::close(m_fd); m_fd = -1; + return false; + } + std::strncpy(addr.sun_path, m_socket_path.c_str(), sizeof(addr.sun_path) - 1); + + if (::connect(m_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + std::cerr << "SimTransport: connect to " << m_socket_path + << " failed (is sim_switch running?): " + << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + SimHello h{ + SIM_MAGIC, + SIM_VERSION, + 0, + static_cast(proto), + self_uid, + 0 + }; + ssize_t hn = ::send(m_fd, &h, sizeof(h), 0); + if (hn != static_cast(sizeof(h))) { + std::cerr << "SimTransport: hello send failed: " << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + int flags = ::fcntl(m_fd, F_GETFL, 0); + if (flags < 0 || ::fcntl(m_fd, F_SETFL, flags | O_NONBLOCK) < 0) { + std::cerr << "SimTransport: fcntl(O_NONBLOCK) failed: " + << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + // SIGPIPE protection: macOS has no MSG_NOSIGNAL, so if the daemon dies + // mid-stream our next sendmsg would deliver SIGPIPE and kill the engine. + // Ignoring it process-wide is a library-side side effect, but it's the + // only portable knob — engines that need EPIPE behaviour explicitly + // were going to die from SIGPIPE anyway. Idempotent + harmless on Linux. + static bool s_sigpipe_ignored = false; + if (!s_sigpipe_ignored) { + std::signal(SIGPIPE, SIG_IGN); + s_sigpipe_ignored = true; + } + + // Pre-reserve the recv body buffer to the largest legal frame size so the + // audio hot path (recv at ~750 Hz/channel) never triggers a reallocation. + m_body_buf.reserve(MAX_FRAME_PAYLOAD); + + m_proto = proto; + m_self_uid = self_uid; + std::memcpy(out_meta.mac, m_self_mac, 6); + out_meta.idx = 0; + return true; } int SimTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { - (void)data; (void)len; (void)dest_uid; - return -1; + if (m_fd < 0) return -1; + + SimFrame hdr{ + static_cast(len), + static_cast(m_proto), + dest_uid + }; + + iovec iov[2] = { + { &hdr, sizeof(hdr) }, + { const_cast(data), len } + }; + msghdr m{}; + m.msg_iov = iov; + m.msg_iovlen = 2; + + ssize_t n; + do { + n = ::sendmsg(m_fd, &m, MSG_DONTWAIT +#ifdef __linux__ + | MSG_NOSIGNAL +#endif + ); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; + return -1; + } + return static_cast(len); // caller cares about payload bytes } int SimTransport::recv(uint8_t* data, size_t len, bool async) { - (void)data; (void)len; (void)async; - return -1; + if (m_fd < 0) return -1; + + while (true) { + // Phase 1: read SimFrame header. + if (!m_have_hdr) { + while (m_hdr_read < sizeof(m_hdr_buf)) { + ssize_t n = ::read(m_fd, m_hdr_buf + m_hdr_read, + sizeof(m_hdr_buf) - m_hdr_read); + if (n > 0) { m_hdr_read += n; continue; } + if (n == 0) return -1; // peer closed + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (async) return 0; + // Blocking mode: wait for more data. + pollfd p{m_fd, POLLIN, 0}; + int pr = ::poll(&p, 1, -1); + if (pr < 0 && errno != EINTR) return -1; + continue; + } + return -1; + } + SimFrame hdr{}; + std::memcpy(&hdr, m_hdr_buf, sizeof(hdr)); + if (hdr.payload_len > MAX_FRAME_PAYLOAD) { + std::cerr << "SimTransport: oversize frame len=" + << hdr.payload_len << "; disconnecting\n"; + return -1; + } + m_body_expected = hdr.payload_len; + // resize() value-initialises, but we always overwrite via read() + // before reading m_body_buf[i], so the zero-fill is wasted work. + // Since reserve(MAX_FRAME_PAYLOAD) ran in open(), no allocation + // happens here — only setting size(). + m_body_buf.resize(m_body_expected); + m_body_read = 0; + m_have_hdr = true; + } + + // Phase 2: read body. + while (m_body_read < m_body_expected) { + ssize_t n = ::read(m_fd, m_body_buf.data() + m_body_read, + m_body_expected - m_body_read); + if (n > 0) { m_body_read += n; continue; } + if (n == 0) return -1; + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (async) return 0; + pollfd p{m_fd, POLLIN, 0}; + int pr = ::poll(&p, 1, -1); + if (pr < 0 && errno != EINTR) return -1; + continue; + } + return -1; + } + + // Phase 3: deliver. + size_t out_len = std::min(len, m_body_expected); + if (m_body_expected > len) { + // Caller's buffer was smaller than the payload. This matches + // AF_PACKET's truncation semantics but is almost certainly a sizing + // bug — log once per frame to aid debugging without spamming. + std::cerr << "SimTransport: recv truncated frame (" + << m_body_expected << " → " << len << " bytes)\n"; + } + std::memcpy(data, m_body_buf.data(), out_len); + + // Reset state for next frame. Keep m_body_buf capacity intact + // (resize(0) preserves capacity; clear() does too, but resize is the + // explicit "shrink length, retain storage" idiom). + m_have_hdr = false; + m_hdr_read = 0; + m_body_read = 0; + m_body_expected = 0; + m_body_buf.resize(0); + + return static_cast(out_len); + } } -void SimTransport::close() {} +void SimTransport::close() { + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } +} diff --git a/netutils/transport/SimTransport.h b/netutils/transport/SimTransport.h index f245d1e..0a2e761 100644 --- a/netutils/transport/SimTransport.h +++ b/netutils/transport/SimTransport.h @@ -1,11 +1,19 @@ #ifndef OAN_SIM_TRANSPORT_H #define OAN_SIM_TRANSPORT_H +#include +#include +#include + #include "ITransport.h" +#include "../LowLatSocket.h" // EthProtocol class SimTransport : public ITransport { public: - explicit SimTransport(std::string daemon_name); + // Argument is the post-"sim:" string from parse_transport — either + // "" or ",mac=02:00:00:00:00:01". + // Empty string defaults to daemon name "default". + explicit SimTransport(std::string daemon_name_with_opts); ~SimTransport() override; bool open(const std::string& iface, EthProtocol proto, @@ -15,7 +23,23 @@ class SimTransport : public ITransport { void close() override; private: + void parse_opts(const std::string& s); + std::string m_daemon_name; + std::string m_socket_path; + uint8_t m_self_mac[6]{}; + bool m_mac_pinned{false}; + + int m_fd{-1}; + EthProtocol m_proto{}; + uint16_t m_self_uid{0}; + + bool m_have_hdr{false}; + uint8_t m_hdr_buf[8]{}; // sizeof(SimFrame) == 8 + size_t m_hdr_read{0}; + std::vector m_body_buf; + size_t m_body_read{0}; + uint32_t m_body_expected{0}; }; #endif From 0878f871bf527549e0afb64c95ec4a6a50907881 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 19:15:01 +0200 Subject: [PATCH 04/12] Throttle NetworkMapper send/age loops on host backends packet_sender and mapper_process were #ifdef __linux__ around their usleep pacing, leaving Mac (OAN_HOST_BACKENDS) to busy-spin both loops. Result: 99k disco msg/s and 11 MiB/s of broadcast traffic blowing past sim_switch's per-conn write buffers (60M+ drops in seconds). Extend the gate to also fire on OAN_HOST_BACKENDS so Mac gets the same 5s send / 1s age cadence as Linux. Embedded Zephyr (no host backends, no threads) stays untouched. --- common/NetworkMapper.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/NetworkMapper.cpp b/common/NetworkMapper.cpp index 0317187..89d0e73 100644 --- a/common/NetworkMapper.cpp +++ b/common/NetworkMapper.cpp @@ -5,6 +5,10 @@ #include "NetworkMapper.h" +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) +#include +#endif + #if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) extern uint32_t _now_ms(); extern uint64_t _now_us(); @@ -132,11 +136,9 @@ void NetworkMapper::packet_send_update() { void NetworkMapper::packet_sender() { while(true) { packet_send_update(); -#ifdef __linux__ +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) usleep(5000000); -#else - -#endif // __linux__ +#endif } } @@ -155,9 +157,9 @@ void NetworkMapper::mapper_process() { mapper_update(); } -#ifdef __linux__ +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) usleep(1000000); -#endif // __linux__ +#endif } } From 5c74aa919a390446d8e67ae3b065fec7629b692d Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 21:31:01 +0200 Subject: [PATCH 05/12] Bump sim wire framing to v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SimFrame now carries src_uid populated switch-side, so promiscuous clients (oaninspect) can see who sent each frame. SimHello._reserved becomes flags so the same client can opt into promiscuous mirroring. Header buffer grows 8 → 12 bytes to match SimFrame v2 layout. Engine clients zero-init src_uid on send; the switch overwrites with the sender's hello uid. --- netutils/transport/SimTransport.cpp | 12 ++++++++---- netutils/transport/SimTransport.h | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/netutils/transport/SimTransport.cpp b/netutils/transport/SimTransport.cpp index e06b5b4..49dc67d 100644 --- a/netutils/transport/SimTransport.cpp +++ b/netutils/transport/SimTransport.cpp @@ -26,7 +26,7 @@ namespace { constexpr uint32_t SIM_MAGIC = 0x4F535354; // 'OSST' -constexpr uint8_t SIM_VERSION = 1; +constexpr uint8_t SIM_VERSION = 2; constexpr uint32_t MAX_FRAME_PAYLOAD = 8192; // matches sim_switch struct SimHello { @@ -35,13 +35,15 @@ struct SimHello { uint8_t _pad; uint16_t ethertype; uint16_t self_uid; - uint16_t _reserved; + uint16_t flags; // SIM_HELLO_PROMISCUOUS etc. — engine never sets any. } __attribute__((packed)); struct SimFrame { uint32_t payload_len; uint16_t ethertype; uint16_t dest_uid; + uint16_t src_uid; // switch overwrites; sender writes 0 + uint16_t _pad; } __attribute__((packed)); bool parse_mac(const std::string& s, uint8_t out[6]) { @@ -171,7 +173,7 @@ bool SimTransport::open(const std::string& iface, EthProtocol proto, 0, static_cast(proto), self_uid, - 0 + 0 // flags — engine never sets PROMISCUOUS }; ssize_t hn = ::send(m_fd, &h, sizeof(h), 0); if (hn != static_cast(sizeof(h))) { @@ -216,7 +218,9 @@ int SimTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { SimFrame hdr{ static_cast(len), static_cast(m_proto), - dest_uid + dest_uid, + 0, // src_uid: switch populates from our hello + 0 }; iovec iov[2] = { diff --git a/netutils/transport/SimTransport.h b/netutils/transport/SimTransport.h index 0a2e761..3aa4f7e 100644 --- a/netutils/transport/SimTransport.h +++ b/netutils/transport/SimTransport.h @@ -35,7 +35,7 @@ class SimTransport : public ITransport { uint16_t m_self_uid{0}; bool m_have_hdr{false}; - uint8_t m_hdr_buf[8]{}; // sizeof(SimFrame) == 8 + uint8_t m_hdr_buf[12]{}; // sizeof(SimFrame) == 12 (v2) size_t m_hdr_read{0}; std::vector m_body_buf; size_t m_body_read{0}; From 5e21675d33252ea1004bb3a122a47da7447a6c71 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Wed, 3 Jun 2026 22:40:19 +0200 Subject: [PATCH 06/12] Add ITransport::wait_readable for host-backend loop pacing Introduces a wait_readable(timeout_ms) primitive on ITransport and plumbs it through LowLatSocket so callers can block on a fd until data arrives or a timeout elapses. Default impl returns 1 immediately so the Linux AF_PACKET RT path keeps its tight-loop semantics byte-identical; SimTransport overrides with poll(POLLIN, timeout_ms). ClockMaster / ClockSlave gain wait_sync_readable() helpers that delegate to the underlying socket. Lets OALS's clock-recv threads stop busy-polling on EAGAIN over AF_UNIX without changing the recv contract. SimTransport::recv is unchanged. --- common/ClockMaster.h | 6 ++++++ common/ClockSlave.h | 6 ++++++ netutils/LowLatSocket.cpp | 24 ++++++++++++++++++++++++ netutils/LowLatSocket.h | 7 +++++++ netutils/transport/ITransport.h | 8 ++++++++ netutils/transport/SimTransport.cpp | 11 +++++++++++ netutils/transport/SimTransport.h | 1 + 7 files changed, 63 insertions(+) diff --git a/common/ClockMaster.h b/common/ClockMaster.h index 8dda503..482f24c 100644 --- a/common/ClockMaster.h +++ b/common/ClockMaster.h @@ -20,6 +20,12 @@ class ClockMaster { void begin_sync_process(); void sync_process(); + // Block until a sync packet arrives or timeout_ms elapses. + // Lets the clock thread on host backends stop busy-polling the async recv. + int wait_sync_readable(int timeout_ms) const { + return m_sync_socket->wait_readable(timeout_ms); + } + private: void start_clock_sync(PeerInfos& slave); void process_packet(ClockSyncPacket& csp, uint16_t originator); diff --git a/common/ClockSlave.h b/common/ClockSlave.h index da548f6..0b1fa23 100644 --- a/common/ClockSlave.h +++ b/common/ClockSlave.h @@ -18,6 +18,12 @@ class ClockSlave { void sync_process(); int64_t get_ck_offset(); + // Block until a sync packet arrives or timeout_ms elapses. Lets the + // slave's clock thread on host backends stop busy-polling the async recv. + int wait_sync_readable(int timeout_ms) const { + return m_sync_socket->wait_readable(timeout_ms); + } + private: void send_delay_req(uint16_t dest); void calc_ck_offset(); diff --git a/netutils/LowLatSocket.cpp b/netutils/LowLatSocket.cpp index bbf943f..dc1f8b6 100644 --- a/netutils/LowLatSocket.cpp +++ b/netutils/LowLatSocket.cpp @@ -135,6 +135,17 @@ int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { return recv(m_socket, data, size, async ? MSG_DONTWAIT : 0); } +int LowLatSocket::wait_readable(int timeout_ms) const { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->wait_readable(timeout_ms); + } +#endif + // Linux AF_PACKET path: keep tight-loop semantics from M3, no-op. + (void)timeout_ms; + return 1; +} + #elif defined(OAN_HOST_BACKENDS) LowLatSocket::LowLatSocket(uint16_t self_uid, std::shared_ptr mapper) { @@ -180,6 +191,11 @@ int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { return m_transport->recv(data, size, async); } +int LowLatSocket::wait_readable(int timeout_ms) const { + if (!m_transport) return -1; + return m_transport->wait_readable(timeout_ms); +} + #else // Zephyr firmware path — unchanged from pre-M3 IfaceMeta get_iface_meta(const std::string& name) { @@ -220,6 +236,14 @@ int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { return recv_data_internal(data, size); } +int LowLatSocket::wait_readable(int timeout_ms) const { + // Zephyr firmware path: recv_data_internal is the blocking primitive, + // so we never busy-loop. Return readable immediately so any caller + // that pre-paces ends up calling recv right away. + (void)timeout_ms; + return 1; +} + int LowLatSocket::send_data_internal(uint8_t* data, size_t size) { return _send_data(data, size); } diff --git a/netutils/LowLatSocket.h b/netutils/LowLatSocket.h index 9637314..f3eafd9 100644 --- a/netutils/LowLatSocket.h +++ b/netutils/LowLatSocket.h @@ -181,6 +181,13 @@ class LowLatSocket { const uint8_t* get_self_mac() const { return m_hdr.h_source; } + // Block until this socket is readable or timeout_ms elapses. Lets RT + // loops on host backends pace themselves instead of spinning on + // EAGAIN. On Linux + AF_PACKET this is a no-op that returns 1 + // immediately so the existing tight-loop semantics are unchanged. + // Returns 1 readable / 0 timeout / -1 error. + int wait_readable(int timeout_ms) const; + private: /** * Finds a device MAC address based on its ID. diff --git a/netutils/transport/ITransport.h b/netutils/transport/ITransport.h index 6ef449d..8f8d0a9 100644 --- a/netutils/transport/ITransport.h +++ b/netutils/transport/ITransport.h @@ -17,6 +17,14 @@ struct ITransport { virtual int send(const uint8_t* data, size_t len, uint16_t dest_uid) = 0; virtual int recv(uint8_t* data, size_t len, bool async) = 0; virtual void close() = 0; + + // Block until the underlying fd is readable or timeout_ms elapses. + // Returns 1 on readable, 0 on timeout, -1 on error. Lets call sites + // pace busy-loops on host backends without touching recv(). Default + // impl returns 1 immediately so the Linux RT path (RawLinuxTransport) + // keeps its existing tight-loop semantics — only host-backend + // implementations override. + virtual int wait_readable(int timeout_ms) { (void)timeout_ms; return 1; } }; IfaceMeta host_iface_meta(const std::string& iface); diff --git a/netutils/transport/SimTransport.cpp b/netutils/transport/SimTransport.cpp index 49dc67d..77c18d6 100644 --- a/netutils/transport/SimTransport.cpp +++ b/netutils/transport/SimTransport.cpp @@ -327,6 +327,17 @@ int SimTransport::recv(uint8_t* data, size_t len, bool async) { } } +int SimTransport::wait_readable(int timeout_ms) { + if (m_fd < 0) return -1; + pollfd p{m_fd, POLLIN, 0}; + int r = ::poll(&p, 1, timeout_ms); + if (r < 0) { + if (errno == EINTR) return 0; // treat as timeout — caller will retry + return -1; + } + return r; // 0 = timeout, 1 = readable +} + void SimTransport::close() { if (m_fd >= 0) { ::close(m_fd); diff --git a/netutils/transport/SimTransport.h b/netutils/transport/SimTransport.h index 3aa4f7e..5032470 100644 --- a/netutils/transport/SimTransport.h +++ b/netutils/transport/SimTransport.h @@ -20,6 +20,7 @@ class SimTransport : public ITransport { uint16_t self_uid, IfaceMeta& out_meta) override; int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; int recv(uint8_t* data, size_t len, bool async) override; + int wait_readable(int timeout_ms) override; void close() override; private: From 604a0ab9a68c427cfb01fe56d5c70123f506310b Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:21:57 +0200 Subject: [PATCH 07/12] Implement Mac RT shim with time-constraint policy + mlockall (M7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oals::rt::set_thread_realtime was a no-op on macOS. Replace with thread_policy_set(THREAD_TIME_CONSTRAINT_POLICY): map the Linux SCHED_FIFO priority (5..100) to a fraction of one 64-sample audio block (~667 µs at 96 kHz), clamped to [50 µs, 500 µs]. The kernel treats this as "wake within `period`, give us up to `computation` of CPU, must finish within `constraint`" — same overrun semantics as SCHED_FIFO (we just run later, not killed). set_process_scheduler_rr on Mac becomes the natural one-time spot for mlockall(MCL_CURRENT|MCL_FUTURE) so RT stacks can't page out under load. Soft-fails with a warning since macOS returns ENOSYS for non-root processes; engine still runs fine without it. set_running_cpu stays a no-op: thread_affinity_policy_set is documented as a hint and is effectively unhonored on Apple Silicon. Verified: engine's four RT threads now show PRI=97R (high-priority time-constraint) in ps -M while the workqueue + mainline threads stay at PRI=31T. Idle CPU still 0.9 % (same as post-M6). --- netutils/platform/rt.cpp | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/netutils/platform/rt.cpp b/netutils/platform/rt.cpp index 5d84492..1312e5d 100644 --- a/netutils/platform/rt.cpp +++ b/netutils/platform/rt.cpp @@ -10,6 +10,18 @@ #include #include #include +#elif defined(__APPLE__) + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include #else #include #endif @@ -50,6 +62,105 @@ void precise_sleep_ns(long ns) { clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, nullptr); } +#elif defined(__APPLE__) + +namespace { + +// Cached host timebase: mach absolute ticks -> nanoseconds is +// (ticks * numer / denom). thread_time_constraint_policy_data_t takes +// values in absolute mach ticks, so anything we compute in ns has to go +// through this. Init-once, lock-free read after. +mach_timebase_info_data_t s_tb_info = {}; +bool s_tb_init = false; + +void ensure_timebase() { + if (s_tb_init) return; + mach_timebase_info(&s_tb_info); + s_tb_init = true; +} + +uint32_t ns_to_mach_ticks(uint64_t ns) { + ensure_timebase(); + // ns * denom / numer — order matters to avoid overflow at audio-block + // scale (period ~667 µs, computation a fraction of that). + uint64_t ticks = (ns * s_tb_info.denom) / s_tb_info.numer; + if (ticks > UINT32_MAX) ticks = UINT32_MAX; + return static_cast(ticks); +} + +} // namespace + +// Pin the calling thread as a time-constraint thread. We map our Linux +// SCHED_FIFO priority (20..80) to a fraction of the audio block period: +// higher priority → larger computation budget within the same period. +// The audio block is 64 samples @ 96 kHz ≈ 667 µs; the pipe_updater +// (prio 80) and audio recv (prio 25) both want to finish well inside +// that. We pick: +// period = 667 µs (one audio block) +// computation = (prio / 100) * period, clamped to [50 µs, 500 µs] +// constraint = period (must complete within one period) +// preemptible = true (yield if we overrun — matches SCHED_FIFO behaviour) +// +// macOS scheduler treats this as "wake within `period`, give us up to +// `computation` of compute, must finish within `constraint`." Overrun +// doesn't kill us; the kernel just keeps running us at lower priority +// until we yield. That's the same shape as Linux SCHED_FIFO overrun. +// +// Refs: Apple TN2169, Chromium base/threading/platform_thread_mac.mm. +void set_thread_realtime(uint8_t prio) { + constexpr uint64_t k_audio_block_ns = 667'000; // 64 / 96000 * 1e9 ≈ 667 µs + + uint64_t computation_ns = + static_cast(k_audio_block_ns) * std::max(prio, 5) / 100; + if (computation_ns < 50'000) computation_ns = 50'000; // floor 50 µs + if (computation_ns > 500'000) computation_ns = 500'000; // ceil 500 µs (75% of period) + + thread_time_constraint_policy_data_t policy; + policy.period = ns_to_mach_ticks(k_audio_block_ns); + policy.computation = ns_to_mach_ticks(computation_ns); + policy.constraint = ns_to_mach_ticks(k_audio_block_ns); + policy.preemptible = TRUE; + + kern_return_t kr = thread_policy_set( + pthread_mach_thread_np(pthread_self()), + THREAD_TIME_CONSTRAINT_POLICY, + reinterpret_cast(&policy), + THREAD_TIME_CONSTRAINT_POLICY_COUNT); + + if (kr != KERN_SUCCESS) { + std::cerr << "[oals::rt] thread_policy_set(time_constraint) failed: " + << kr << std::endl; + } +} + +// thread_affinity_policy_set exists but is documented as a hint and is +// effectively a no-op on Apple Silicon (no P-core pinning). Skip — the +// scheduler does a better job placing time-constraint threads than we +// could anyway. +void set_running_cpu(int) {} + +// On Linux this sets the whole process to SCHED_RR. On Mac we use the +// hook as the natural one-time-per-process spot for mlockall: paging out +// the engine's RT stacks while a show is running is the worst failure +// mode there is. mlockall is per-process so this is idempotent if a +// caller invokes us more than once. +void set_process_scheduler_rr(int) { + static bool s_mlocked = false; + if (s_mlocked) return; + if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) { + std::cerr << "[oals::rt] mlockall failed (continuing): " + << strerror(errno) << std::endl; + } + s_mlocked = true; +} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + nanosleep(&ts, nullptr); +} + #else void set_thread_realtime(uint8_t) {} From dbfd5b4d8c089c34524c3059d1900eb5f7df1475 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:22:06 +0200 Subject: [PATCH 08/12] Add CI, clang-format, pre-commit hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI: GitHub Actions matrix runs on every push and PR to any branch, across (ubuntu, OAN_HOST_BACKENDS=OFF), (ubuntu, host=ON), and (macos-latest, host=ON). The host=OFF Linux job keeps the Zephyr- shaped firmware path green; host=ON exercises SimTransport. clang-format: LLVM-based with K&R braces, 100-col limit, left-aligned references to match the codebase's auto& majority. Apply via editor on save — do NOT mass-format existing files. pre-commit: clang-format + trailing-whitespace + EOF fixer + merge- conflict + large-file guard. Install via 'pip install pre-commit && pre-commit install'. --- .clang-format | 38 ++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 19 ++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yml create mode 100644 .pre-commit-config.yaml diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..67ecad0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,38 @@ +# Pinned style for OALS. Tuned to match what's already in the tree +# (4-space indent, K&R braces, ref/pointer attached to the variable, +# no hard column cap). Apply to new code via editor-on-save; do NOT +# mass-format existing files in a single sweep. +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 + +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 + +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +SpaceAfterCStyleCast: false +PointerAlignment: Left +ReferenceAlignment: Left +DerivePointerAlignment: false + +AccessModifierOffset: -4 +NamespaceIndentation: None +FixNamespaceComments: true + +IncludeBlocks: Preserve +SortIncludes: false + +AlignAfterOpenBracket: Align +BinPackParameters: false +BinPackArguments: true +AllowAllParametersOfDeclarationOnNextLine: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true + +EmptyLineBeforeAccessModifier: LogicalBlock +SeparateDefinitionBlocks: Leave diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd91b63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: ['**'] + pull_request: + branches: ['**'] + +jobs: + build: + name: ${{ matrix.os }} (host=${{ matrix.host_backends }}) + strategy: + fail-fast: false + matrix: + include: + # Linux: default Zephyr-firmware-shaped build (no host backends) is + # the path the firmware repos consume — keep it green. + - os: ubuntu-latest + host_backends: OFF + # Host-dev build path used by OALSEngine + sim_switch. + - os: ubuntu-latest + host_backends: ON + - os: macos-latest + host_backends: ON + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake ninja-build + + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: brew install cmake ninja + + - name: Configure + run: | + cmake -G Ninja -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DOAN_HOST_BACKENDS=${{ matrix.host_backends }} + + - name: Build + run: cmake --build build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f3dc516 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# pre-commit hooks for OpenAudioNetwork. Install once per clone: +# pip install pre-commit && pre-commit install +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v17.0.6 + hooks: + - id: clang-format + types_or: [c++, c] + exclude: '^build/' + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=512'] From 3d9800d697ecaa7cb735feb10a80abd27c6cb28b Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:26:12 +0200 Subject: [PATCH 09/12] Mirror OAN_HOST_BACKENDS option at OAN root for standalone builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host-backend define was only injected by the parent OALS CMakeLists. That meant standalone OAN builds (e.g. CI on this repo) would compile with OAN_HOST_BACKENDS undefined regardless of the -D flag — and then NetworkMapper.cpp's #else branch would reference firmware externs (_now_ms, _now_us, _fetch_iface_meta), failing to link. Mirror OALS's option block here: opt-in OFF by default, forced ON on APPLE (no other transport works), translated to a compile definition. --- CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d59585..5d7c8cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,5 +7,18 @@ if(NO_THREADS) add_compile_definitions(NO_THREADS) endif (NO_THREADS) +# OAN_HOST_BACKENDS: switches in the host-side transport seam (SimTransport, +# RawLinuxTransport, RawMacTransport). The define is normally injected by +# the parent OALS build; mirror the logic here so OAN is self-buildable +# (e.g. for CI of OAN-only changes). Forced ON on macOS — there is no +# other working transport on Apple at the moment. +option(OAN_HOST_BACKENDS "Build host dev transports + RT shim" OFF) +if(APPLE) + set(OAN_HOST_BACKENDS ON CACHE BOOL "Build host dev transports + RT shim" FORCE) +endif() +if(OAN_HOST_BACKENDS) + add_compile_definitions(OAN_HOST_BACKENDS) +endif() + add_subdirectory(common) add_subdirectory(netutils) \ No newline at end of file From f0c5701f02c3ccc4f36f43a04f3bb4a98ae53758 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:31:56 +0200 Subject: [PATCH 10/12] Defer NetworkMapper symbol resolution to load time on Linux too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oannetutils' LowLatSocket calls NetworkMapper::get_mac_by_uid which lives in oancommon — but oancommon already links oannetutils, so the cycle can't be closed at the CMake level. macOS used -undefined,dynamic_lookup to paper over this; Linux defaulted to strict -no-undefined and the .so itself failed to link. Add --unresolved-symbols=ignore-in-shared-libs on Linux to match the macOS behavior. The engine binary always loads both .so's so the symbol resolves correctly at runtime. --- netutils/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index 7eb44c5..c27c86b 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -39,4 +39,12 @@ target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) if(APPLE) target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) +elseif(UNIX) + # LowLatSocket calls NetworkMapper::get_mac_by_uid, which lives in + # oancommon — and oancommon already links oannetutils, so we can't + # close the cycle at the CMake level. macOS' dynamic_lookup option + # papers over this; on Linux we tell ld to defer the symbol to load + # time. The engine binary always loads both .so's so the symbol + # resolves there. + target_link_options(oannetutils PRIVATE LINKER:--unresolved-symbols=ignore-in-shared-libs) endif() \ No newline at end of file From 2c387f5505b9ec478841454f8591948fbbedcd57 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:34:26 +0200 Subject: [PATCH 11/12] Use -z undefs instead of --unresolved-symbols=ignore-in-shared-libs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous flag tells ld to be lenient about undefined symbols in *sibling .so files* we link against — but oannetutils links no other .so, so the flag did nothing and the missing NetworkMapper symbol still failed the build. -z undefs (the Linux equivalent of Mac's -undefined,dynamic_lookup) allows undefined symbols *in this output .so*, deferring them to load-time resolution. --- netutils/CMakeLists.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index c27c86b..362dfc2 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -41,10 +41,10 @@ if(APPLE) target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) elseif(UNIX) # LowLatSocket calls NetworkMapper::get_mac_by_uid, which lives in - # oancommon — and oancommon already links oannetutils, so we can't - # close the cycle at the CMake level. macOS' dynamic_lookup option - # papers over this; on Linux we tell ld to defer the symbol to load - # time. The engine binary always loads both .so's so the symbol - # resolves there. - target_link_options(oannetutils PRIVATE LINKER:--unresolved-symbols=ignore-in-shared-libs) + # oancommon — and oancommon already PUBLIC-links oannetutils, so we + # can't close the cycle by linking the other way at CMake level. + # Mac papers over this with -undefined,dynamic_lookup; the Linux + # equivalent is -z undefs (allow undefined symbols in this .so; + # they'll resolve at load time when the engine pulls in both .so's). + target_link_options(oannetutils PRIVATE LINKER:-z,undefs) endif() \ No newline at end of file From bafa7554b8070c22cc64820c87293c4aa1a00b69 Mon Sep 17 00:00:00 2001 From: "jonathan.reichardt" Date: Thu, 4 Jun 2026 00:42:24 +0200 Subject: [PATCH 12/12] Tighten -z undefs explanation comment --- netutils/CMakeLists.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index 362dfc2..67524e2 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -37,14 +37,15 @@ endif (EMBEDDED_BUILD) target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) +# oannetutils and oancommon have a mutual symbol dependency: +# LowLatSocket (in netutils) calls NetworkMapper::get_mac_by_uid +# (in common), and common's ClockMaster/ClockSlave/AudioRouter all +# instantiate LowLatSocket. CMake forbids cyclic target_link_libraries +# on shared libs, so we relax each library's own link step (the symbol +# is unresolved in the .so but always resolved at exe link time when +# both .so's appear on the line — see executables that link them). if(APPLE) target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) elseif(UNIX) - # LowLatSocket calls NetworkMapper::get_mac_by_uid, which lives in - # oancommon — and oancommon already PUBLIC-links oannetutils, so we - # can't close the cycle by linking the other way at CMake level. - # Mac papers over this with -undefined,dynamic_lookup; the Linux - # equivalent is -z undefs (allow undefined symbols in this .so; - # they'll resolve at load time when the engine pulls in both .so's). target_link_options(oannetutils PRIVATE LINKER:-z,undefs) endif() \ No newline at end of file