diff --git a/CMakeLists.txt b/CMakeLists.txt index cfbcd6f4..16fef794 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,12 +77,50 @@ else() # A version test rather than a flag probe, deliberately: the flags all EXIST here, it is # their judgement that is wrong, so CheckCXXCompilerFlag would happily confirm them. if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) - add_compile_options(-Wno-error=null-dereference - -Wno-error=restrict + add_compile_options(-Wno-error=restrict -Wno-error=stringop-overflow -Wno-error=format-truncation) endif() + # -Wnull-dereference also fires on GCC 16.2.1, past the version bound above, but only for one + # real class of site: a plain `this->member_` read inlined through a lambda/callback chain + # (MoonModule::enabled()'s `enabled_ && !held_`, reached through several tests' std::function + # plumbing) where GCC's analysis loses track of `this` and can't prove it non-null. There is no + # source fix for that shape (testing `this == nullptr` in a member function is not meaningful), + # so it stays unconditional for GCC pending a version where it is confirmed to stop. Two other + # sites GCC flagged under the same warning turned out to be real, provably-safe `rootNode()` + # calls gated on a prior `json::parse()` success and are fixed directly instead (StateDocument.cpp, + # ControlModule.h::readHeader) — this exception covers only the this-across-a-thunk shape now. + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wno-error=null-dereference) + endif() + + # -Warray-bounds= fires on GCC 16.2.1 too: MoonModule::setName's std::memcpy(name_, n, len) + # is bound-checked (len < sizeof(name_), a real fixed array) before the call, but GCC 16.2.1, + # inlining it through several unit tests' DOCTEST_ANON_FUNCs, deduces a 0-byte region for + # name_ at some of those call sites and reports offset 0 as out of [0, 0] bounds — a false + # positive from the same aggressive-inlining-loses-the-real-size class as the others here. + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wno-error=array-bounds=) + endif() + + # -Wmaybe-uninitialized fires on GCC 16.2.1 too: unit_Hub75Driver_pins.cpp reads out[i] only + # for i < n (n itself checked == 14 just above), but GCC 16.2.1 flags out[14]/out[15] anyway, + # past the loop's own dynamic bound — a `fixedPins(out, 16)` return value it isn't correlating + # with the loop condition after inlining, not a real read of memory the loop never reaches. + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wno-error=maybe-uninitialized) + endif() + + # Vendored doctest.h always includes on Clang, unconditionally, to detect libc++ via + # _LIBCPP_VERSION (doctest's own long-standing quirk: github.com/doctest/doctest/issues/126, #356). + # A Clang new enough to deprecate that pre-C++20 header turns its #warning into a build failure + # under -Werror. Third-party header code we don't own, so this stays unconditional rather than + # version-gated, matching the MSVC C5285 suppression above for the same vendored file. + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options("-Wno-error=#warnings") # quoted: a bare # starts a CMake comment, truncating the rest of the file + endif() + # Hot-path discipline, enforced by the compiler. MM_NONBLOCKING marks tick/tick20ms/tick1s # (platform.h); -Wfunction-effects then checks TRANSITIVELY that nothing they reach # allocates or blocks, through the whole call graph. Clang 20+ only; the flag does not diff --git a/src/core/module/StateDocument.cpp b/src/core/module/StateDocument.cpp index cf0f1bb1..fbb9a973 100644 --- a/src/core/module/StateDocument.cpp +++ b/src/core/module/StateDocument.cpp @@ -648,7 +648,7 @@ StateDocumentResult applyStateDocument(Scheduler& scheduler, const char* text, S JsonDoc doc; if (!json::parse(text, doc)) { r.ok = false; r.error = doc.outOfMemory ? "out of memory" : "malformed JSON"; return r; } const JsonNode* root = doc.rootNode(); - if (root->type != JsonType::Object) { r.ok = false; r.error = "a document is a JSON object"; return r; } + if (!root || root->type != JsonType::Object) { r.ok = false; r.error = "a document is a JSON object"; return r; } Applier a{scheduler, doc, r, source}; for (int pass = 0; pass < 3 && a.runPass(pass, root); pass++) {} @@ -702,6 +702,7 @@ namespace { // The flat file's values directly under `prefix`, each as `,"key":value`. void writeFlatValues(JsonSink& out, const JsonDoc& doc, const char* prefix) { const JsonNode* root = doc.rootNode(); + if (!root) return; const size_t pl = std::strlen(prefix); for (const JsonNode* n = doc.node(root->firstChild); n; n = doc.node(n->next)) { if (std::strncmp(n->key, prefix, pl) != 0) continue; diff --git a/src/core/system/ControlModule.h b/src/core/system/ControlModule.h index b38d6dfe..2e340f6b 100644 --- a/src/core/system/ControlModule.h +++ b/src/core/system/ControlModule.h @@ -970,8 +970,11 @@ class ControlModule : public MoonModule, public ListSource { char* body = readFile(path); if (!body) return; json::JsonDoc doc; - if (json::parse(body, doc) && doc.rootNode()->type == json::JsonType::Object) - for (const json::JsonNode* m = doc.node(doc.rootNode()->firstChild); m; m = doc.node(m->next)) readRootKey(p, *m); + if (json::parse(body, doc)) { + const json::JsonNode* root = doc.rootNode(); + if (root && root->type == json::JsonType::Object) + for (const json::JsonNode* m = doc.node(root->firstChild); m; m = doc.node(m->next)) readRootKey(p, *m); + } platform::free(body); } diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 71d06964..ca3822f0 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -2965,6 +2965,8 @@ bool ndiLoad() { } // NDIlib_initialize returns false when the CPU is unsupported, a real "cannot use it" that must not read as "installed and working". if (!ndiInit_()) { ndiSendVideo_ = nullptr; return false; } + // NDIlib_destroy pairs with NDIlib_initialize at process exit, since a sender may reopen later in the same run. Only with no sender open: a quit closes it first, while a reboot exits with the render thread still sending, where the process ending is the teardown. + std::atexit([]() { if (ndiDestroy_ && !ndiSender_) ndiDestroy_(); }); return true; } diff --git a/test/unit/core/unit_Buffer.cpp b/test/unit/core/unit_Buffer.cpp index 175f3caa..cf2bd957 100644 --- a/test/unit/core/unit_Buffer.cpp +++ b/test/unit/core/unit_Buffer.cpp @@ -17,7 +17,7 @@ TEST_CASE("Buffer allocate and accessors") { // clear() zeroes every byte in the allocated range. TEST_CASE("Buffer clear zeros data") { mm::Buffer buf; - buf.allocate(10, 3); + REQUIRE(buf.allocate(10, 3)); // Write some data buf.data()[0] = 0xFF; buf.data()[15] = 0xAB;