From 3f63781ce1dbb553af8a1a7aa6b0bc9b66d88ab5 Mon Sep 17 00:00:00 2001 From: Alex B <142788550+A13xB0@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:01:37 +0100 Subject: [PATCH] build: the Windows DLL carries its own runtime The shared build linked libstdc++ dynamically everywhere. On Linux and macOS that is right - the runtime is part of the base system and a plugin should share the host's copy. On Windows it produced a DLL that only loads inside an MSYS2 environment, and nothing that loads it has one. Both emulators open this library at run time on an ordinary machine: QEMU through LoadLibrary, Renode through LoadLibraryW behind a P/Invoke. There the load fails, and the message names us rather than what we are missing: qemu-system-xtensa.exe: sx1262: cannot load the chip model at ...\libvirtualsx1262.dll: The specified module could not be found. which is why it reads as a corrupt or absent library rather than an absent runtime. Downstream it surfaces three steps later still, as "firmware started but never connected within 40s" - a radio that never answers, because the model behind it was never loaded. MeshBench#632. Shipping libstdc++-6.dll beside this file does not fix it, which is worth recording because it is the obvious first try: Windows resolves a dynamically loaded DLL's imports from the loading *process's* directory, not from the directory of the DLL being loaded. Tested - the runtime sat next to libvirtualsx1262.dll and the load failed identically. A copy would have to live beside every host that opens us, in QEMU's directory and wherever Renode is unpacked, and be kept in step with the toolchain that built us. Static is the only version that travels. -static rather than -static-libstdc++, because -static-libstdc++ alone leaves libwinpthread-1.dll behind it: libstdc++'s threading support pulls it in, so the two have to go together. Imports before and after, on MINGW64 gcc 16.1.0: before KERNEL32.dll msvcrt.dll libstdc++-6.dll after KERNEL32.dll msvcrt.dll Both of those are on every supported Windows. The DLL goes from 112 kB to 666 kB, which is the whole of the fix's cost. And the build now checks. What this looked like from the outside was a green build, passing tests, an artefact of the right size, and a library nothing could open - so the one thing that would have caught it is the build saying what its own product depends on. check_shared_imports fails the build on any import beyond kernel32 and msvcrt, and was tested in both directions: it passes on the fixed link and fails on the old one, naming libstdc++-6.dll. Verified end to end against MeshBench v0.0.8 on Windows 10 22H2. Built with this change, dropped in place of the shipped v1.3.0 DLL, and the board that could not boot boots: board.probe Generic_E22_sx1262 before boot failed firmware started but never connected within 40s after boot passed attached and answering Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG --- build.sh | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index de262f8..ba338d9 100755 --- a/build.sh +++ b/build.sh @@ -37,10 +37,35 @@ CFLAGS_C="-std=c11 -O2 -Iinclude -Wall -Wextra -Wpedantic ${CFLAGS}" SRC="src/VirtualSX1262.cpp src/spi.cpp src/abi.cpp" +# SHARED_RUNTIME is the toolchain runtime the shared build links against. +# +# Windows takes it statically. The DLL is opened by an emulator at run time - +# QEMU through LoadLibrary, Renode through LoadLibraryW behind a P/Invoke - on a +# machine that has never had a toolchain on it. Linked the default way it +# imports libstdc++-6.dll, which exists inside MSYS2 and almost nowhere else, so +# the build and its tests pass on the runner and the library cannot be opened +# on the target: +# +# qemu-system-xtensa.exe: sx1262: cannot load the chip model at +# ...\libvirtualsx1262.dll: The specified module could not be found. +# +# That message names this DLL rather than the dependency it could not find, +# which is why it reads as a missing library rather than a missing runtime. +# +# Shipping the runtime beside this file does not fix it: Windows resolves a +# dynamically loaded DLL's own imports from the loading *process's* directory, +# not from the directory of the DLL being loaded, so a copy would have to sit +# next to every host that opens us. Static is the only version that travels. +# -static rather than -static-libstdc++, because libstdc++ brings +# libwinpthread-1.dll in behind it and the pair have to go together. +# +# Linux and macOS keep the dynamic link. libstdc++ is part of the base system +# on both, and a plugin carrying its own copy into a host process that already +# has one is the worse bet. case "$(uname -s 2>/dev/null)" in - MINGW*|MSYS*|CYGWIN*) SHARED_EXT=dll ;; - Darwin) SHARED_EXT=dylib ;; - *) SHARED_EXT=so ;; + MINGW*|MSYS*|CYGWIN*) SHARED_EXT=dll; SHARED_RUNTIME="-static" ;; + Darwin) SHARED_EXT=dylib; SHARED_RUNTIME="" ;; + *) SHARED_EXT=so; SHARED_RUNTIME="" ;; esac mkdir -p "$OUT" @@ -56,8 +81,35 @@ build_static() { } build_shared() { - $CXX $CXXFLAGS -fPIC -fno-exceptions -shared $SRC -o "$OUT/libvirtualsx1262.$SHARED_EXT" + $CXX $CXXFLAGS -fPIC -fno-exceptions -shared $SRC $SHARED_RUNTIME \ + -o "$OUT/libvirtualsx1262.$SHARED_EXT" echo "built $OUT/libvirtualsx1262.$SHARED_EXT" + check_shared_imports +} + +# The last time this went wrong the build was green, the tests passed, the +# artefact was the right size and nothing could open it. So the build now says +# what its own product needs rather than leaving that to be found on somebody +# else's machine. kernel32 and msvcrt are on every supported Windows; anything +# else is a file that has to travel with us, and does not. +check_shared_imports() { + [ "$SHARED_EXT" = dll ] || return 0 + if ! command -v objdump >/dev/null 2>&1; then + echo "build.sh: no objdump, so the DLL's imports went unchecked" >&2 + return 0 + fi + stray=$(objdump -p "$OUT/libvirtualsx1262.dll" \ + | sed -n 's/^ DLL Name: //p' \ + | tr 'A-Z' 'a-z' | sort -u \ + | grep -v -e '^kernel32\.dll$' -e '^msvcrt\.dll$' || true) + if [ -n "$stray" ]; then + echo "build.sh: the DLL imports something that will not be on the target:" >&2 + echo "$stray" | sed 's/^/ /' >&2 + echo " a host opening it gets \"The specified module could not be found\"," >&2 + echo " naming this DLL rather than the one above." >&2 + exit 1 + fi + echo "checked $OUT/libvirtualsx1262.dll: nothing imported beyond kernel32 and msvcrt" } # One binary per subject rather than one big one, because the house limit on