diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50137a4..113f75a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,38 @@ jobs: - name: Run unit + realtime tests under RTSan run: ctest --test-dir build-rtsan --output-on-failure + freestanding: + name: Freestanding profile (${{ matrix.cxx }}, -fno-exceptions -fno-rtti) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { cc: gcc, cxx: g++ } + - { cc: clang, cxx: clang++ } + steps: + - uses: actions/checkout@v4 + + # Builds only the embedded-profile smoke test: the parse/serialize core + # with C++ exceptions and RTTI disabled and the hosted-only facilities + # dropped (-DOSCTAP_FREESTANDING). This is the standing guard that the + # core stays buildable for embedded targets (e.g. a Raspberry Pi Pico 2W; + # see docs/EMBEDDED_PICO2W.md). The OSCTAP_THROW seam routes validation + # failures to a fatal handler when exceptions are off. + - name: Configure (freestanding profile) + run: > + cmake -S . -B build-freestanding -DOSCPACK_BUILD_EXAMPLES=OFF + -DOSCTAP_FREESTANDING=ON -DOSCTAP_WARNINGS_AS_ERRORS=ON + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + + - name: Build + run: cmake --build build-freestanding --target OscFreestandingTest + + - name: Run freestanding smoke test + run: ctest --test-dir build-freestanding -R OscFreestandingTest --output-on-failure + tsan: name: ThreadSanitizer (receive loop concurrency) runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 04aeca9..a9f9e49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ enable_testing() set(OSCPACK_BUILD_EXAMPLES ON CACHE BOOL "Should we build examples") set(OSCTAP_RTSAN OFF CACHE BOOL "Build the realtime-safety test under RealtimeSanitizer + function-effect analysis (requires Clang >= 20)") set(OSCTAP_TSAN OFF CACHE BOOL "Build the concurrency test under ThreadSanitizer (POSIX; GCC/Clang)") +set(OSCTAP_FREESTANDING OFF CACHE BOOL "Build the freestanding/embedded-profile smoke test with exceptions and RTTI disabled (Phase 2 'Reach')") set(CMAKE_INCLUDE_CURRENT_DIR 1) set(CMAKE_POSITION_INDEPENDENT_CODE 1) @@ -92,6 +93,24 @@ if(OSCPACK_BUILD_EXAMPLES) #target_link_libraries(SimpleSend oscpack) endif() +# Freestanding / embedded profile (Phase 2 "Reach"). Builds a single smoke test +# of the parse/serialize core with C++ exceptions and RTTI disabled and the +# hosted-only facilities dropped (-DOSCTAP_FREESTANDING). The flags are PRIVATE +# to this target so the exception-based tests above are unaffected; this target +# is the standing guard that the core stays embedded-buildable (e.g. a Raspberry +# Pi Pico 2W — see docs/EMBEDDED_PICO2W.md). +if(OSCTAP_FREESTANDING) + add_executable(OscFreestandingTest tests/OscFreestandingTest.cpp) + target_link_libraries(OscFreestandingTest oscpack) + target_compile_definitions(OscFreestandingTest PRIVATE OSCTAP_FREESTANDING) + if(MSVC) + target_compile_options(OscFreestandingTest PRIVATE /EHs-c- /GR-) + else() + target_compile_options(OscFreestandingTest PRIVATE -fno-exceptions -fno-rtti) + endif() + add_test(NAME OscFreestandingTest COMMAND OscFreestandingTest) +endif() + set(OSCTAP_BUILD_FUZZERS OFF CACHE BOOL "Build the libFuzzer fuzz target (requires Clang)") set(OSCTAP_FUZZER_STANDALONE OFF CACHE BOOL "Build the fuzz target with the standalone driver instead of libFuzzer (works with g++/ASan, no fuzzer runtime needed)") diff --git a/ROADMAP.md b/ROADMAP.md index 94fa8b4..5bdc2ef 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,7 +5,9 @@ OscTap is the actively-maintained, security-hardened, modern-C++ continuation of document is the source of truth for the rebrand and the work plan. See [`docs/HERITAGE.md`](docs/HERITAGE.md) for lineage and credits. -> Status: planning / Phase 0 in progress on branch `claude/oscpack-review-audit-h8n268`. +> Status: Phase 0 and Phase 1 complete. Phase 2 ("Reach") prep in progress — the +> freestanding/embedded profile groundwork has landed (see Phase 2 below); the +> remaining Reach items stay demand-gated. ## Why OscTap exists @@ -140,10 +142,27 @@ See [Sanitizer strategy](#sanitizer-strategy) for scope and rationale. | MSan | optional | later | catches uninitialized-memory reads (cf. the past "uninitialized OSC address bytes" fix); high setup friction (instrumented libc++), so not a standing job. | ### Phase 2 — Reach (only as demand appears) -- [ ] QEMU aarch64 / armv7 CI. -- [ ] Android NDK build. -- [ ] Freestanding/embedded profile (no exceptions/RTTI option). -- [ ] Multicast receive (cherry-pick from `stephram/oscpack`). +- [x] **Freestanding/embedded profile (no exceptions/RTTI option)** — *groundwork + landed.* A single build seam (`osc/OscConfig.h`) auto-detects + `OSCTAP_HAS_EXCEPTIONS` and routes every core `throw` through `OSCTAP_THROW` + (a plain `throw` when exceptions are on; a non-returning, user-overridable + fatal handler — `OSCTAP_FATAL_HANDLER`, default `std::abort()` — when they are + off). `OSCTAP_FREESTANDING` drops the hosted-only facilities (``, the + `std::vector`-backed `OwnedMessage`, the `std::string` `operator<<`). The + `OSCTAP_FREESTANDING` CMake option builds `tests/OscFreestandingTest.cpp` with + `-fno-exceptions -fno-rtti`; a `freestanding` CI job (GCC + Clang) keeps it + green. Hosted builds are byte-for-byte unchanged. **Demand signal: Raspberry + Pi Pico 2W (RP2350)** — see [`docs/EMBEDDED_PICO2W.md`](docs/EMBEDDED_PICO2W.md). + Deferred: a **non-throwing `TryInit`/validate** entry point so a no-exceptions + build can *reject* untrusted packets by returning an error instead of aborting + (today, malformed input on an exceptions-off build is fatal — safe only on a + trusted link; open networks should keep exceptions on and `catch`). +- [ ] QEMU aarch64 / armv7 CI. *(Demand-gated — adds standing CI surface; stand up + only when a real big-endian/cross target needs it. The freestanding profile + already exercises the no-OS code paths without QEMU's cost.)* +- [ ] Android NDK build. *(Demand-gated — pursue when an Android consumer appears.)* +- [ ] Multicast receive (cherry-pick from `stephram/oscpack`). *(Self-contained; + the next demand-driven feature pickup after the freestanding groundwork.)* ## Milestones → GitHub diff --git a/docs/EMBEDDED_PICO2W.md b/docs/EMBEDDED_PICO2W.md new file mode 100644 index 0000000..8dfb600 --- /dev/null +++ b/docs/EMBEDDED_PICO2W.md @@ -0,0 +1,194 @@ +# OscTap on the Raspberry Pi Pico 2W (RP2350) + +> Status: Phase 2 ("Reach") groundwork. The freestanding profile this guide uses +> is landed and CI-guarded; the Pico SDK glue below is an integration recipe, not +> a vendored board build. See [`../ROADMAP.md`](../ROADMAP.md). + +The Pico 2W pairs the dual-core **RP2350** (Arm Cortex-M33 @ 150 MHz) with the +**CYW43439** wireless chip, so it can speak OSC over Wi-Fi/UDP. OscTap's core — +the `osc/` headers — is header-only, dependency-free, allocation-free on the +read/serialize hot path, and freestanding-friendly, which makes it a natural fit +for an embedded OSC endpoint. + +This guide shows how to build the OscTap **core** against the Pico SDK + lwIP, +using the freestanding profile (exceptions/RTTI off) introduced for Phase 2. + +## What you use — and what you don't + +| OscTap layer | On the Pico 2W | +|--------------|----------------| +| `osc/` core — `OscReceivedElements.h`, `OscOutboundPacketStream.h`, `OscTypes.h`, `OscUtilities.h` | **Use it.** Parses/builds OSC into/out of a plain byte buffer. | +| `ip/` sockets — `ip/posix`, `ip/win32` (`UdpSocket`, `NetworkingUtils`) | **Don't use it.** Those are POSIX/WinSock backends. On the Pico, networking is **lwIP** (the SDK's TCP/IP stack), so you call lwIP's UDP API directly and hand the payload to the OscTap core. | + +The split is deliberate: the OSC wire format is the reusable part; the transport +is whatever your platform provides. + +## The freestanding profile (`OSCTAP_FREESTANDING`) + +The Pico SDK builds C++ **without exceptions or RTTI by default** +(`PICO_CXX_ENABLE_EXCEPTIONS=0`, `PICO_CXX_ENABLE_RTTI=0`). OscTap supports that +through a single build seam (`osc/OscConfig.h`): + +- `OSCTAP_HAS_EXCEPTIONS` is auto-detected from the compiler. Under + `-fno-exceptions` it becomes `0` and every `throw` in the library is routed + through `OSCTAP_THROW`. +- `OSCTAP_FREESTANDING` (you define it) drops the hosted-only conveniences: + ``, the `std::vector`-backed `OwnedMessage`, and the + `operator<<(const std::string&)` overload. Pass `const char*`, a string + literal, or `osctap::string_view` instead. + +That combination is exactly what the `freestanding` CI job and +`tests/OscFreestandingTest.cpp` build and run on every push, so the core is kept +embedded-buildable. + +### Handling validation when exceptions are off + +OSC packets arriving off the network are **untrusted input**. With exceptions +enabled, OscTap's parser *throws* (`MalformedPacketException`, +`MalformedMessageException`, `MalformedBundleException`) when it rejects a bad +packet, and you `catch` it to drop that packet. With exceptions **disabled**, +there is nothing to catch — `OSCTAP_THROW` instead calls a fatal handler that +**must not return** (the default `std::abort()`s). + +That has a security consequence you must design around: + +> On a no-exceptions build, a single malformed packet from anyone who can reach +> the device will trigger the fatal handler — i.e. a remote reset / DoS. + +Pick the model that matches your threat surface: + +1. **Trusted / closed link** (a fixed sender on a private wire or AP, no exposure + to arbitrary senders): the freestanding no-exceptions build is fine. Define + `OSCTAP_FATAL_HANDLER` to log and reset deliberately — reaching it means a + programming error or a genuinely corrupt link, not routine traffic. + +2. **Untrusted / open network**: keep **exceptions enabled** on the Pico + (`pico_enable_exceptions()` / `PICO_CXX_ENABLE_EXCEPTIONS=1`) and + `catch` the `Malformed*Exception` types to drop bad packets. The OscTap API is + identical either way — `OSCTAP_THROW` is just `throw` here. You still get the + rest of the embedded posture (no heap on the hot path, small code). + +Either way, **validation runs in the lwIP receive callback, off any audio/render +thread** — consistent with OscTap's realtime contract (validation may throw and +is not part of the RT hot path; the throw-free `*Unchecked` accessors are). + +> Future work (tracked in Phase 2): a non-throwing `TryInit`/validate entry point +> so a no-exceptions build can *reject* untrusted packets by returning an error +> instead of aborting. Until then, option 2 is the safe choice for open networks. + +## Build integration (CMake + Pico SDK) + +Add the OscTap include root and define the profile. OscTap's core is +header-only, so there is no library to compile or link. + +```cmake +# After pico_sdk_init() and your add_executable(osc_demo ...) + +target_include_directories(osc_demo PRIVATE + ${OSCTAP_DIR}) # repo root: enables and the + # in-tree quoted "osc/..." includes + +target_compile_definitions(osc_demo PRIVATE + OSCTAP_FREESTANDING) # drop hosted-only facilities + +# Wi-Fi UDP via lwIP (threadsafe-background or poll arch): +target_link_libraries(osc_demo + pico_stdlib + pico_cyw43_arch_lwip_threadsafe_background) + +# Trusted-link model: keep exceptions off (SDK default) and provide a handler. +# Open-network model instead: pico_enable_exceptions(osc_demo) +``` + +If you choose the trusted-link model, define the fatal handler **before** +including any OscTap header (e.g. via a small `osctap_config.h` you force-include, +or a `-D`): + +```cpp +// Logs and resets; must not return. +#define OSCTAP_FATAL_HANDLER(whatCStr) osc_fatal((whatCStr)) + +[[noreturn]] void osc_fatal(const char* what); // defined in your app +``` + +## Receiving OSC (lwIP UDP → OscTap) + +lwIP hands you the datagram in a `pbuf` from your `udp_recv` callback. Validate + +dispatch there (off the realtime thread), then read with the throw-free +accessors in your hot loop. *Illustrative — adapt names to your SDK version:* + +```cpp +#include "osc/OscReceivedElements.h" + +static void on_osc_packet(void* /*arg*/, struct udp_pcb* /*pcb*/, + struct pbuf* p, const ip_addr_t* /*addr*/, u16_t /*port*/) +{ + if (!p) return; + + // Copy the (possibly chained) pbuf into a contiguous, 4-byte-aligned buffer. + alignas(4) static char buf[1472]; // <= typical Ethernet MTU payload + const u16_t n = pbuf_copy_partial(p, buf, sizeof(buf), 0); + pbuf_free(p); + + // Open-network model: wrap construction in try/catch (exceptions ON) so a + // malformed packet is dropped, not fatal. Trusted-link model: drop the + // try/catch — construction aborts via OSCTAP_FATAL_HANDLER on bad input. + osctap::ReceivedPacket packet(buf, n); + if (!packet.IsMessage()) return; // (handle bundles similarly) + + osctap::ReceivedMessage msg(packet); + + if (std::strcmp(msg.AddressPattern(), "/led") == 0) { + // Known-valid from here: realtime-safe, throw-free reads. + auto arg = msg.ArgumentsBegin(); + const bool on = arg->AsBoolUnchecked(); + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, on); + } +} + +// setup: udp_recv(pcb, on_osc_packet, nullptr); +``` + +## Sending OSC (OscTap → lwIP UDP) + +Serialize into a stack buffer (no heap), then ship it through an lwIP `pbuf`: + +```cpp +#include "osc/OscOutboundPacketStream.h" + +void send_fader(udp_pcb* pcb, const ip_addr_t* dst, uint16_t port, float v) +{ + char buffer[64]; + osctap::OutboundPacketStream p(buffer, sizeof(buffer)); + p << osctap::BeginMessage("/fader/1") << v << osctap::EndMessage(); + + struct pbuf* pb = pbuf_alloc(PBUF_TRANSPORT, p.Size(), PBUF_RAM); + std::memcpy(pb->payload, p.Data(), p.Size()); + udp_sendto(pcb, pb, dst, port); + pbuf_free(pb); +} +``` + +`OutboundPacketStream` never allocates and writes only into the buffer you give +it; if the message would overflow that buffer it raises +`OutOfBufferMemoryException` (caught, or fatal, per your exception model), so size +the buffer for your largest message. + +## Realtime / no-heap checklist + +- Parse/serialize touch **only** your byte buffer — no `malloc` on the OscTap + side. Keep the buffers static or on the stack. +- Read the audio-thread hot path through the `*Unchecked` accessors + (`AsInt32Unchecked`, `AsFloatUnchecked`, …); they are the functions OscTap + annotates realtime-safe (`OSCTAP_REALTIME`). +- Do all **validation** (constructing `ReceivedPacket`/`ReceivedMessage`) in the + network callback, never on the audio render core. +- lwIP's own `pbuf`/heap pools are separate from OscTap; tune them via + `lwipopts.h` as usual. + +## See also + +- [`../ROADMAP.md`](../ROADMAP.md) — Phase 2 plan and the realtime contract. +- [`STATUS.md`](STATUS.md) — landmines and build matrix. +- `tests/OscFreestandingTest.cpp` — the CI-built proof that the core compiles and + runs with exceptions/RTTI disabled. diff --git a/docs/STATUS.md b/docs/STATUS.md index 28b2f40..37581da 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -8,6 +8,13 @@ the original conversation. For the full plan and rationale see - **Phase 0 is complete** (security audit fixes, fuzzer, CI, docs, namespace rename). See the scorecard in `ROADMAP.md`. +- **Phase 1 is complete** (directory rename + shim, ClusterFuzzLite, `bit_cast`/ + `constexpr` parsing, warnings-as-errors, RTSan, TSan). +- **Phase 2 ("Reach") prep has started**: the freestanding/embedded profile + groundwork is landed — the `OSCTAP_THROW` seam (`osc/OscConfig.h`), the + `OSCTAP_FREESTANDING` CMake option + `freestanding` CI job, and the Raspberry Pi + Pico 2W guide ([`EMBEDDED_PICO2W.md`](EMBEDDED_PICO2W.md)). The remaining Reach + items (QEMU, Android NDK, multicast) stay demand-gated. - All six audit findings are fixed with regression tests; see commit history and `tests/OscUnitTests.cpp` (`test4`/`test5`). - **CI is the source of truth for build health.** `.github/workflows/ci.yml` builds and @@ -34,6 +41,10 @@ cmake --build build-fuzz --target fuzz_parse && ./build-fuzz/fuzz_parse fuzz/cor # fuzzing — standalone driver (g++, no libFuzzer runtime needed) cmake -S . -B build-fuzz -DOSCTAP_FUZZER_STANDALONE=ON cmake --build build-fuzz --target fuzz_parse && ./build-fuzz/fuzz_parse fuzz/corpus/* + +# freestanding / embedded profile (exceptions + RTTI off) +cmake -S . -B build-fs -DOSCPACK_BUILD_EXAMPLES=OFF -DOSCTAP_FREESTANDING=ON +cmake --build build-fs --target OscFreestandingTest && ./build-fs/OscFreestandingTest ``` ## Landmines — read before changing things @@ -74,6 +85,26 @@ cmake --build build-fuzz --target fuzz_parse && ./build-fuzz/fuzz_parse fuzz/cor `reinterpret_cast` over the byte buffer, or `#ifdef OSC_HOST_*_ENDIAN`** — that was the audit-#6 UB, and UBSan guards against it. The byte helpers are `constexpr`; keep the RT read accessors routed through them (`memcpy` is RTSan/function-effects-safe). +- **`OSCTAP_THROW` is the only way the core raises** (`osc/OscConfig.h`). Every + `throw` in `OscReceivedElements.h`/`OscOutboundPacketStream.h` goes through it so + the library compiles under `-fno-exceptions`. **Do not reintroduce a bare `throw` + in the core** — it breaks the `freestanding` CI job (a bare `throw` is a hard + error under `-fno-exceptions`). With exceptions on, `OSCTAP_THROW(X)` *is* `throw X`, + so hosted behaviour (and the `test4`/`test5` malformed-input asserts) is unchanged. + Under `-fno-exceptions` it calls a non-returning fatal handler (default + `std::abort()`, overridable via `OSCTAP_FATAL_HANDLER`). +- **`OSCTAP_FREESTANDING` drops hosted-only facilities** — ``, the + `std::vector`-backed `OwnedMessage`, and the `std::string` `operator<<`. If you add + a new core feature that needs ``/``/`std::string`, guard it with + `#ifndef OSCTAP_FREESTANDING` (and keep `tests/OscFreestandingTest.cpp` compiling), + or it will break the `freestanding` job. The freestanding flags are **PRIVATE to + the `OscFreestandingTest` target**, so the exception-based tests are unaffected. + Embedded posture and the Pico 2W recipe live in `docs/EMBEDDED_PICO2W.md`. +- **Untrusted input on a no-exceptions build is fatal.** The parser validates by + throwing; with exceptions off there is nothing to catch, so a malformed packet + hits the fatal handler (a remote reset/DoS). Safe only on a trusted link; open + networks should keep exceptions on and `catch` the `Malformed*Exception` types. A + non-throwing `TryInit`/validate is the tracked Phase 2 follow-up. - **Include guards are still named `INCLUDED_OSCPACK_*`** — cosmetic, left as-is. - The test harness (`NewMessageBuffer`/`AllocateAligned4`) **intentionally leaks** its aligned scratch buffers, which is why the ASan job runs with diff --git a/oscpack/osc/OscConfig.h b/oscpack/osc/OscConfig.h new file mode 100644 index 0000000..bcb7ce7 --- /dev/null +++ b/oscpack/osc/OscConfig.h @@ -0,0 +1,11 @@ +/* + OscTap compatibility shim. + + The library directory and public include prefix moved from + to (the namespace likewise moved oscpack -> osctap, kept as a + deprecated alias). This header redirects the old include path to the new one + so existing consumers keep compiling unchanged. + + Deprecated: prefer . See ROADMAP.md / docs/STATUS.md. +*/ +#include diff --git a/osctap/osc/OscConfig.h b/osctap/osc/OscConfig.h new file mode 100644 index 0000000..c3cf8d9 --- /dev/null +++ b/osctap/osc/OscConfig.h @@ -0,0 +1,106 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCCONFIG_H +#define INCLUDED_OSCPACK_OSCCONFIG_H + +/* + OscTap build-configuration seam. + + This header centralises the knobs the Phase 2 "freestanding / embedded + profile" relies on: whether C++ exceptions are available, and how the library + should report an unrecoverable validation error when they are not. Hosted + builds are unaffected -- OSCTAP_THROW expands to a plain `throw`, exactly as + before, so the existing exception-based API and tests are unchanged. + + See docs/EMBEDDED_PICO2W.md for a worked embedded target (Raspberry Pi + Pico 2W / RP2350) that builds the core with exceptions disabled. +*/ + +/* --- OSCTAP_HAS_EXCEPTIONS -------------------------------------------------- + Auto-detected from the compiler unless the user forces it. A build with + -fno-exceptions (GCC/Clang) or /EHs-c- (MSVC) sets this to 0; everything + else leaves the normal throwing behaviour in place. Force it explicitly by + pre-defining OSCTAP_HAS_EXCEPTIONS to 0 or 1 on the command line. */ +#ifndef OSCTAP_HAS_EXCEPTIONS +# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (defined(_MSC_VER) && defined(_CPPUNWIND)) +# define OSCTAP_HAS_EXCEPTIONS 1 +# else +# define OSCTAP_HAS_EXCEPTIONS 0 +# endif +#endif + +/* --- OSCTAP_FREESTANDING ---------------------------------------------------- + User-defined (e.g. -DOSCTAP_FREESTANDING) to drop the parts of the library + that pull in hosted/heavyweight facilities -- and the + std::vector-backed OwnedMessage. The realtime parse/serialize core needs + neither, so an embedded target can build the core without them. Defining + OSCTAP_FREESTANDING does NOT by itself disable exceptions; pair it with + -fno-exceptions on the toolchain (which flips OSCTAP_HAS_EXCEPTIONS to 0). */ + +/* --- OSCTAP_THROW ----------------------------------------------------------- + Raise an OscTap exception, or -- when exceptions are disabled -- report it + to a fatal handler that does not return. Usage mirrors `throw`: + + OSCTAP_THROW( MalformedPacketException( "invalid packet size" ) ); + + With exceptions enabled this is literally `throw EXC`. With exceptions + disabled, validation failures are unrecoverable, so the default is to + terminate via std::abort(). Embedded integrators that prefer to log + reset + can route this anywhere by pre-defining OSCTAP_FATAL_HANDLER(whatCStr) + before including any OscTap header -- it receives the exception's .what() + string and must not return. */ +#if OSCTAP_HAS_EXCEPTIONS +# define OSCTAP_THROW(EXC) throw EXC +#else +# if defined(OSCTAP_FATAL_HANDLER) +# define OSCTAP_THROW(EXC) (OSCTAP_FATAL_HANDLER((EXC).what())) +# else +# include // std::abort +namespace osctap { +namespace detail { +// Default fatal handler used when exceptions are disabled and the integrator +// has not supplied OSCTAP_FATAL_HANDLER. Marked [[noreturn]] so the compiler +// knows the post-validation code is unreachable (no spurious "control reaches +// end of non-void function" diagnostics at the former throw sites). +[[noreturn]] inline void OscFatalError(const char* /*what*/) { std::abort(); } +} // namespace detail +} // namespace osctap +# define OSCTAP_THROW(EXC) (::osctap::detail::OscFatalError((EXC).what())) +# endif +#endif + +#endif /* INCLUDED_OSCPACK_OSCCONFIG_H */ diff --git a/osctap/osc/OscOutboundPacketStream.h b/osctap/osc/OscOutboundPacketStream.h index 979d39d..8979acc 100644 --- a/osctap/osc/OscOutboundPacketStream.h +++ b/osctap/osc/OscOutboundPacketStream.h @@ -42,7 +42,10 @@ #include #include // memcpy, memmove, strcpy, strlen #include // ptrdiff_t +#ifndef OSCTAP_FREESTANDING #include +#include // std::string operator<< overload (hosted convenience) +#endif #include namespace osctap @@ -52,6 +55,7 @@ namespace osctap #include "OscTypes.h" #include "OscException.h" +#include "OscConfig.h" // OSCTAP_THROW, OSCTAP_FREESTANDING #include "OscUtilities.h" #include "OscHostEndianness.h" @@ -183,7 +187,7 @@ class OutboundPacketStream{ OutboundPacketStream& operator<<(BundleInitiator rhs ) { if( IsMessageInProgress() ) - throw MessageInProgressException(); + OSCTAP_THROW( MessageInProgressException() ); CheckForAvailableBundleSpace(); @@ -203,9 +207,9 @@ class OutboundPacketStream{ (void) rhs; if( !IsBundleInProgress() ) - throw BundleNotInProgressException(); + OSCTAP_THROW( BundleNotInProgressException() ); if( IsMessageInProgress() ) - throw MessageInProgressException(); + OSCTAP_THROW( MessageInProgressException() ); EndElement( messageCursor_ ); @@ -216,7 +220,7 @@ class OutboundPacketStream{ OutboundPacketStream& operator<<(BeginMessage rhs ) { if( IsMessageInProgress() ) - throw MessageInProgressException(); + OSCTAP_THROW( MessageInProgressException() ); std::size_t rhsLength = std::strlen(rhs.addressPattern); CheckForAvailableMessageSpace( rhsLength ); @@ -245,7 +249,7 @@ class OutboundPacketStream{ OutboundPacketStream& operator<<(BeginMessageN rhs) { if( IsMessageInProgress() ) - throw MessageInProgressException(); + OSCTAP_THROW( MessageInProgressException() ); CheckForAvailableMessageSpace( rhs.addressPattern.size() ); @@ -275,7 +279,7 @@ class OutboundPacketStream{ (void) rhs; if( !IsMessageInProgress() ) - throw MessageNotInProgressException(); + OSCTAP_THROW( MessageNotInProgressException() ); std::size_t typeTagsCount = end_ - typeTagsCurrent_; @@ -462,12 +466,17 @@ class OutboundPacketStream{ return *this; } +#ifndef OSCTAP_FREESTANDING + // Hosted convenience: std::string pulls in (and heap). The + // freestanding profile omits it; pass const char*, a char array, or + // osctap::string_view instead. OutboundPacketStream& operator<<( const std::string& rhs) { operator<<(osctap::string_view(rhs)); return *this; } +#endif template OutboundPacketStream& operator<<( @@ -613,7 +622,7 @@ class OutboundPacketStream{ std::size_t required = Size() + ((ElementSizeSlotRequired())?4:0) + 16; if( required > Capacity() ) - throw OutOfBufferMemoryException(); + OSCTAP_THROW( OutOfBufferMemoryException() ); } void CheckForAvailableMessageSpace( std::size_t addressPatternSize ) { @@ -622,7 +631,7 @@ class OutboundPacketStream{ + RoundUp4(static_cast(addressPatternSize + 1)) + 4; if( required > Capacity() ) - throw OutOfBufferMemoryException(); + OSCTAP_THROW( OutOfBufferMemoryException() ); } void CheckForAvailableArgumentSpace( std::size_t argumentLength ) { @@ -631,7 +640,7 @@ class OutboundPacketStream{ + RoundUp4( static_cast((end_ - typeTagsCurrent_) + 3) ); if( required > Capacity() ) - throw OutOfBufferMemoryException(); + OSCTAP_THROW( OutOfBufferMemoryException() ); } char * const data_; diff --git a/osctap/osc/OscReceivedElements.h b/osctap/osc/OscReceivedElements.h index e9e655b..0f5b058 100644 --- a/osctap/osc/OscReceivedElements.h +++ b/osctap/osc/OscReceivedElements.h @@ -40,11 +40,14 @@ #include #include #include // size_t -#include #include "OscTypes.h" #include "OscException.h" +#include "OscConfig.h" // OSCTAP_THROW, OSCTAP_FREESTANDING #include "OscUtilities.h" #include // ptrdiff_t +#ifndef OSCTAP_FREESTANDING +#include // std::vector backs OwnedMessage (hosted-only) +#endif namespace osctap{ @@ -123,13 +126,13 @@ class ReceivedPacket{ // sanity check integer types declared in OscTypes.h // you'll need to fix OscTypes.h if any of these asserts fail if( !IsValidElementSizeValue(size) ) - throw MalformedPacketException( "invalid packet size" ); + OSCTAP_THROW( MalformedPacketException( "invalid packet size" ) ); if( size == 0 ) - throw MalformedPacketException( "zero length elements not permitted" ); + OSCTAP_THROW( MalformedPacketException( "zero length elements not permitted" ) ); if( !IsMultipleOf4(size) ) - throw MalformedPacketException( "element size must be multiple of four" ); + OSCTAP_THROW( MalformedPacketException( "element size must be multiple of four" ) ); return size; } @@ -229,18 +232,18 @@ class ReceivedMessageArgument{ bool AsBool() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == TRUE_TYPE_TAG ) return true; else if( *typeTagPtr_ == FALSE_TYPE_TAG ) return false; else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } bool AsBoolUnchecked() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == TRUE_TYPE_TAG ) return true; else @@ -254,11 +257,11 @@ class ReceivedMessageArgument{ int32_t AsInt32() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == INT32_TYPE_TAG ) return AsInt32Unchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } int32_t AsInt32Unchecked() const OSCTAP_REALTIME { @@ -269,11 +272,11 @@ class ReceivedMessageArgument{ float AsFloat() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == FLOAT_TYPE_TAG ) return AsFloatUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } float AsFloatUnchecked() const OSCTAP_REALTIME { @@ -284,11 +287,11 @@ class ReceivedMessageArgument{ char AsChar() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == CHAR_TYPE_TAG ) return AsCharUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } char AsCharUnchecked() const OSCTAP_REALTIME { @@ -299,11 +302,11 @@ class ReceivedMessageArgument{ uint32_t AsRgbaColor() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == RGBA_COLOR_TYPE_TAG ) return AsRgbaColorUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } uint32_t AsRgbaColorUnchecked() const OSCTAP_REALTIME { @@ -314,11 +317,11 @@ class ReceivedMessageArgument{ uint32_t AsMidiMessage() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == MIDI_MESSAGE_TYPE_TAG ) return AsMidiMessageUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } uint32_t AsMidiMessageUnchecked() const OSCTAP_REALTIME { @@ -329,11 +332,11 @@ class ReceivedMessageArgument{ int64_t AsInt64() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == INT64_TYPE_TAG ) return AsInt64Unchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } int64_t AsInt64Unchecked() const OSCTAP_REALTIME { @@ -344,11 +347,11 @@ class ReceivedMessageArgument{ uint64_t AsTimeTag() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == TIME_TAG_TYPE_TAG ) return AsTimeTagUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } uint64_t AsTimeTagUnchecked() const OSCTAP_REALTIME { @@ -359,11 +362,11 @@ class ReceivedMessageArgument{ double AsDouble() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == DOUBLE_TYPE_TAG ) return AsDoubleUnchecked(); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } double AsDoubleUnchecked() const OSCTAP_REALTIME { @@ -374,11 +377,11 @@ class ReceivedMessageArgument{ const char* AsString() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == STRING_TYPE_TAG ) return argumentPtr_; else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } const char* AsStringUnchecked() const OSCTAP_REALTIME { return argumentPtr_; } @@ -386,11 +389,11 @@ class ReceivedMessageArgument{ const char* AsSymbol() const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == SYMBOL_TYPE_TAG ) return argumentPtr_; else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } const char* AsSymbolUnchecked() const OSCTAP_REALTIME { return argumentPtr_; } @@ -398,18 +401,18 @@ class ReceivedMessageArgument{ void AsBlob( const void*& data, osc_bundle_element_size_t& size ) const { if( !typeTagPtr_ ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); else if( *typeTagPtr_ == BLOB_TYPE_TAG ) AsBlobUnchecked( data, size ); else - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); } void AsBlobUnchecked( const void*& data, osc_bundle_element_size_t& size ) const { // read blob size as an unsigned int then validate osc_bundle_element_size_t sizeResult = (osc_bundle_element_size_t)ToUInt32( argumentPtr_ ); if( !IsValidElementSizeValue(sizeResult) ) - throw MalformedMessageException("invalid blob size"); + OSCTAP_THROW( MalformedMessageException("invalid blob size") ); size = sizeResult; data = (void*)(argumentPtr_+ osctap::OSC_SIZEOF_INT32); @@ -423,7 +426,7 @@ class ReceivedMessageArgument{ { // it is only valid to call ComputeArrayItemCount when the argument is the array start marker if( !IsArrayBegin() ) - throw WrongArgumentTypeException(); + OSCTAP_THROW( WrongArgumentTypeException() ); std::size_t result = 0; unsigned int level = 0; @@ -592,7 +595,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( bool& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsBool(); return *this; @@ -605,7 +608,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( int32_t& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsInt32(); return *this; @@ -614,7 +617,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( float& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsFloat(); return *this; @@ -623,7 +626,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( char& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsChar(); return *this; @@ -632,7 +635,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( RgbaColor& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs.value = (*p_++).AsRgbaColor(); return *this; @@ -641,7 +644,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( MidiMessage& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs.value = (*p_++).AsMidiMessage(); return *this; @@ -650,7 +653,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( int64_t& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsInt64(); return *this; @@ -659,7 +662,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( TimeTag& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs.value = (*p_++).AsTimeTag(); return *this; @@ -668,7 +671,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( double& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsDouble(); return *this; @@ -677,7 +680,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( Blob& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); (*p_++).AsBlob( rhs.data, rhs.size ); return *this; @@ -686,7 +689,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( const char*& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs = (*p_++).AsString(); return *this; @@ -695,7 +698,7 @@ class ReceivedMessageArgumentStream{ ReceivedMessageArgumentStream& operator>>( Symbol& rhs ) { if( Eos() ) - throw MissingArgumentException(); + OSCTAP_THROW( MissingArgumentException() ); rhs.value = (*p_++).AsSymbol(); return *this; @@ -706,7 +709,7 @@ class ReceivedMessageArgumentStream{ (void) rhs; // suppress unused parameter warning if( !Eos() ) - throw ExcessArgumentException(); + OSCTAP_THROW( ExcessArgumentException() ); return *this; } @@ -717,20 +720,20 @@ class ReceivedMessage{ void Init( const char *message, osc_bundle_element_size_t size ) { if( !IsValidElementSizeValue(size) ) - throw MalformedMessageException( "invalid message size" ); + OSCTAP_THROW( MalformedMessageException( "invalid message size" ) ); if( size == 0 ) - throw MalformedMessageException( "zero length messages not permitted" ); + OSCTAP_THROW( MalformedMessageException( "zero length messages not permitted" ) ); if( !IsMultipleOf4(size) ) - throw MalformedMessageException( "message size must be multiple of four" ); + OSCTAP_THROW( MalformedMessageException( "message size must be multiple of four" ) ); const char *end = message + size; typeTagsBegin_ = FindStr4End( addressPattern_, end ); if( typeTagsBegin_ == 0 ){ // address pattern was not terminated before end - throw MalformedMessageException( "unterminated address pattern" ); + OSCTAP_THROW( MalformedMessageException( "unterminated address pattern" ) ); } if( typeTagsBegin_ == end ){ @@ -741,7 +744,7 @@ class ReceivedMessage{ }else{ if( *typeTagsBegin_ != ',' ) - throw MalformedMessageException( "type tags not present" ); + OSCTAP_THROW( MalformedMessageException( "type tags not present" ) ); if( *(typeTagsBegin_ + 1) == '\0' ){ // zero length type tags @@ -754,7 +757,7 @@ class ReceivedMessage{ arguments_ = FindStr4End( typeTagsBegin_, end ); if( arguments_ == 0 ){ - throw MalformedMessageException( "type tags were not terminated before end of message" ); + OSCTAP_THROW( MalformedMessageException( "type tags were not terminated before end of message" ) ); } ++typeTagsBegin_; // advance past initial ',' @@ -782,7 +785,7 @@ class ReceivedMessage{ case ARRAY_END_TYPE_TAG: if( arrayLevel == 0 ) - throw MalformedMessageException( "array close tag ']' without matching open tag '['" ); + OSCTAP_THROW( MalformedMessageException( "array close tag ']' without matching open tag '['" ) ); --arrayLevel; // (zero length argument data) break; @@ -794,10 +797,10 @@ class ReceivedMessage{ case MIDI_MESSAGE_TYPE_TAG: if( argument == end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); argument += 4; if( argument > end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); break; case INT64_TYPE_TAG: @@ -805,31 +808,31 @@ class ReceivedMessage{ case DOUBLE_TYPE_TAG: if( argument == end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); argument += 8; if( argument > end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); break; case STRING_TYPE_TAG: case SYMBOL_TYPE_TAG: if( argument == end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); argument = FindStr4End( argument, end ); if( argument == 0 ) - throw MalformedMessageException( "unterminated string argument" ); + OSCTAP_THROW( MalformedMessageException( "unterminated string argument" ) ); break; case BLOB_TYPE_TAG: { if( argument + osctap::OSC_SIZEOF_INT32 > end ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); // treat blob size as an unsigned int for the purposes of this calculation uint32_t blobSize = ToUInt32( argument ); if( !IsValidElementSizeValue( (osc_bundle_element_size_t)blobSize ) ) - throw MalformedMessageException( "invalid blob size" ); + OSCTAP_THROW( MalformedMessageException( "invalid blob size" ) ); // Compare sizes rather than advancing the pointer first: a huge // blobSize must not be allowed to overflow the pointer (or RoundUp4) @@ -837,21 +840,21 @@ class ReceivedMessage{ // guaranteed by the check above. const char *blobData = argument + osctap::OSC_SIZEOF_INT32; if( RoundUp4( blobSize ) > (uint32_t)(end - blobData) ) - throw MalformedMessageException( "arguments exceed message size" ); + OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); argument = blobData + RoundUp4( blobSize ); } break; default: - throw MalformedMessageException( "unknown type tag" ); + OSCTAP_THROW( MalformedMessageException( "unknown type tag" ) ); } }while( *++typeTag != '\0' ); typeTagsEnd_ = typeTag; if( arrayLevel != 0 ) - throw MalformedMessageException( "array was not terminated before end of message (expected ']' end of array tag)" ); + OSCTAP_THROW( MalformedMessageException( "array was not terminated before end of message (expected ']' end of array tag)" ) ); } // These invariants should be guaranteed by the above code. @@ -936,6 +939,9 @@ class ReceivedMessage{ const osc_bundle_element_size_t size_; }; +#ifndef OSCTAP_FREESTANDING +// OwnedMessage copies the message into a std::vector. It is hosted-only and +// excluded from the freestanding profile (no dynamic allocation / no ). class OwnedMessage { explicit OwnedMessage(const ReceivedMessage& other): @@ -954,19 +960,20 @@ class OwnedMessage std::vector buffer_; ReceivedMessage message_; }; +#endif // OSCTAP_FREESTANDING class ReceivedBundle{ void Init( const char *bundle, osc_bundle_element_size_t size ) { if( !IsValidElementSizeValue(size) ) - throw MalformedBundleException( "invalid bundle size" ); + OSCTAP_THROW( MalformedBundleException( "invalid bundle size" ) ); if( size < 16 ) - throw MalformedBundleException( "packet too short for bundle" ); + OSCTAP_THROW( MalformedBundleException( "packet too short for bundle" ) ); if( !IsMultipleOf4(size) ) - throw MalformedBundleException( "bundle size must be multiple of four" ); + OSCTAP_THROW( MalformedBundleException( "bundle size must be multiple of four" ) ); if( bundle[0] != '#' || bundle[1] != 'b' @@ -976,7 +983,7 @@ class ReceivedBundle{ || bundle[5] != 'l' || bundle[6] != 'e' || bundle[7] != '\0' ) - throw MalformedBundleException( "bad bundle address pattern" ); + OSCTAP_THROW( MalformedBundleException( "bad bundle address pattern" ) ); end_ = bundle + size; @@ -986,18 +993,18 @@ class ReceivedBundle{ while( p < end_ ){ if( p + osctap::OSC_SIZEOF_INT32 > end_ ) - throw MalformedBundleException( "packet too short for elementSize" ); + OSCTAP_THROW( MalformedBundleException( "packet too short for elementSize" ) ); // treat element size as an unsigned int for the purposes of this calculation uint32_t elementSize = ToUInt32( p ); if( (elementSize & ((uint32_t)0x03)) != 0 ) - throw MalformedBundleException( "bundle element size must be multiple of four" ); + OSCTAP_THROW( MalformedBundleException( "bundle element size must be multiple of four" ) ); // Compare sizes rather than advancing the pointer first, so that a huge // elementSize can't overflow the pointer and slip past the bounds check. const char *elementData = p + osctap::OSC_SIZEOF_INT32; if( elementSize > (uint32_t)(end_ - elementData) ) - throw MalformedBundleException( "packet too short for bundle element" ); + OSCTAP_THROW( MalformedBundleException( "packet too short for bundle element" ) ); p = elementData + elementSize; @@ -1005,7 +1012,7 @@ class ReceivedBundle{ } if( p != end_ ) - throw MalformedBundleException( "bundle contents " ); + OSCTAP_THROW( MalformedBundleException( "bundle contents " ) ); } public: explicit ReceivedBundle( const ReceivedPacket& packet ) diff --git a/tests/OscFreestandingTest.cpp b/tests/OscFreestandingTest.cpp new file mode 100644 index 0000000..e8b9237 --- /dev/null +++ b/tests/OscFreestandingTest.cpp @@ -0,0 +1,74 @@ +/* + OscTap freestanding / embedded-profile smoke test. + + Built with -fno-exceptions -fno-rtti -DOSCTAP_FREESTANDING (see CMake option + OSCTAP_FREESTANDING and the `freestanding` CI job). Its job is to prove that + the parse/serialize core compiles and runs with C++ exceptions disabled and + without the hosted-only facilities (, std::vector OwnedMessage) + that the freestanding profile drops -- the exact shape an embedded target + such as a Raspberry Pi Pico 2W builds. See docs/EMBEDDED_PICO2W.md. + + Note: with exceptions disabled, validation failures abort (OSCTAP_THROW -> + fatal handler). This test therefore exercises only the valid-input happy + path; malformed-input behaviour is covered by the hosted OscUnitTests suite. +*/ + +#include +#include +#include + +#include "osc/OscOutboundPacketStream.h" +#include "osc/OscReceivedElements.h" + +// Guard rails: this TU must be compiled as the freestanding profile. +#if OSCTAP_HAS_EXCEPTIONS +# error "OscFreestandingTest must be built with exceptions disabled (-fno-exceptions)" +#endif +#ifndef OSCTAP_FREESTANDING +# error "OscFreestandingTest must be built with -DOSCTAP_FREESTANDING" +#endif + +static int failures = 0; + +#define CHECK(cond) \ + do { if(!(cond)){ std::printf("FAIL: %s (line %d)\n", #cond, __LINE__); ++failures; } } while(0) + +int main() +{ + // --- serialize a message on the stack (no heap) ------------------------ + char buffer[256]; + osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); + p << osctap::BeginMessage( "/freestanding" ) + << true << (int32_t)2350 << (float)3.14159f << "pico" + << osctap::EndMessage(); + + CHECK( p.IsReady() ); + CHECK( p.Size() > 0 ); + + // --- parse it back ----------------------------------------------------- + osctap::ReceivedMessage msg( osctap::ReceivedPacket( p.Data(), p.Size() ) ); + + CHECK( std::strcmp( msg.AddressPattern(), "/freestanding" ) == 0 ); + + // Checked accessors: these route validation through OSCTAP_THROW, so their + // mere compilation here proves the no-exceptions seam builds. Input is + // valid, so no fatal handler fires. + osctap::ReceivedMessage::const_iterator arg = msg.ArgumentsBegin(); + CHECK( arg->AsBool() == true ); ++arg; + CHECK( arg->AsInt32() == 2350 ); ++arg; + CHECK( arg->AsFloat() > 3.14f && arg->AsFloat() < 3.15f ); ++arg; + CHECK( std::strcmp( arg->AsString(), "pico" ) == 0 ); ++arg; + CHECK( arg == msg.ArgumentsEnd() ); + + // Realtime read path: the throw-free *Unchecked accessors over a known-valid + // message -- the hot loop an audio/embedded integrator runs every packet. + arg = msg.ArgumentsBegin(); + CHECK( arg->AsBoolUnchecked() == true ); ++arg; + CHECK( arg->AsInt32Unchecked() == 2350 ); ++arg; + CHECK( arg->AsFloatUnchecked() > 3.14f ); ++arg; + CHECK( std::strcmp( arg->AsStringUnchecked(), "pico" ) == 0 ); + + if( failures == 0 ) + std::printf( "OscFreestandingTest: OK (exceptions disabled, freestanding)\n" ); + return failures == 0 ? 0 : 1; +}