Skip to content

build: the Windows DLL carries its own runtime - #1

Merged
A13xB0 merged 1 commit into
mainfrom
fix/windows-static-runtime
Sep 6, 2026
Merged

A13xB0 merged 1 commit into
mainfrom
fix/windows-static-runtime

Conversation

@A13xB0

@A13xB0 A13xB0 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes the root cause of MeshBench#632: no emulated board can boot on Windows, because the chip model cannot be loaded.

What is wrong

build_shared linked the runtime dynamically on every platform. On Linux and macOS that is correct — libstdc++ is part of the base system and a plugin is better off sharing the host's copy. On Windows it produces a DLL that only loads inside an MSYS2 environment, and nothing that opens 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 — and there it fails:

qemu-system-xtensa.exe: sx1262: cannot load the chip model at
...\libvirtualsx1262.dll: The specified module could not be found.

The message names us rather than the dependency we are missing, which is why it reads as a corrupt or absent library. Downstream in MeshBench 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.

Why the artefact is not at fault, and neither is MeshBench's pipeline

Worth stating, because both were suspected first. release.yml stages exactly what build.sh produces, and MeshBench's package.yml ships exactly what it unpacks. Nothing is dropped along the way. I rebuilt v1.3.0's source locally with the unmodified flags and got a DLL with imports identical to the shipped one, so the released file is faithfully what this repo asked for. The build asked for the wrong thing.

Why not ship the runtime instead

The obvious first try, and it does not work — recorded here so nobody spends the afternoon on it. Windows resolves a dynamically loaded DLL's imports from the loading process's directory, not from the directory of the DLL being loaded. I put libstdc++-6.dll, libgcc_s_seh-1.dll and libwinpthread-1.dll next to libvirtualsx1262.dll in a real MeshBench install and the load failed identically. A copy would have to live beside every host that opens us — QEMU's bin, wherever Renode is unpacked — and be kept in step with the toolchain that built us.

-static rather than -static-libstdc++: the latter alone leaves libwinpthread-1.dll behind it, because libstdc++'s threading support pulls it in.

The change

One line of build flags, scoped to the platform that needs it, plus a guard.

imports
before KERNEL32.dll, msvcrt.dll, libstdc++-6.dll
after KERNEL32.dll, msvcrt.dll

Both remaining ones are on every supported Windows. The DLL goes from 112 kB to 666 kB, which is the whole cost.

check_shared_imports fails the build on any import beyond those two. This bug looked like a green build, passing tests, an artefact of the right size and a library nothing could open — so the build saying what its own product depends on is the one thing that would have caught it. Tested in both directions, since a check nothing tests can quietly stop checking:

$ sh ./build.sh shared            # with the fix
built build/libvirtualsx1262.dll
checked build/libvirtualsx1262.dll: nothing imported beyond kernel32 and msvcrt

$ sh ./build-unfixed.sh shared    # SHARED_RUNTIME forced back to ""
built build/libvirtualsx1262.dll
build.sh: the DLL imports something that will not be on the target:
  libstdc++-6.dll
  a host opening it gets "The specified module could not be found",
  naming this DLL rather than the one above.
exit=1

Verification

On Windows 10 Home 22H2 (19045), MSYS2 MINGW64 gcc 16.1.0:

  • STRICT=1 ./build.sh — full build and both test binaries, 0 failures.
  • Built this DLL, dropped it in place of the shipped v1.3.0 one in a real MeshBench v0.0.8 install, cleared the probe cache, and ran the board probe that could not get past boot:
board.probe Generic_E22_sx1262
  before   boot  failed   firmware started but never connected within 40s
  after    boot  passed   attached and answering

The node then pins its board image properly (firmware_board: "Heltec_v3", which stayed empty while the boot was failing) and board.screen reports a real 128x64 SSD1306 rather than refusing.

After this merges

MeshBench needs a release of this repo to pin: CHIPMODEL_RELEASE in packaging/emulator-pins.env is v1.3.0 today. Cutting v1.3.1 and bumping that pin is what actually closes MeshBench#632 — this PR only makes the artefact correct.

Not addressed here: the same -static question for the Linux .so and macOS .dylib. I left both dynamic deliberately, but if a host is ever expected to load us somewhere without a system libstdc++, that is the conversation to have.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
@A13xB0

A13xB0 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

CI: everything passes including windows / mingw, and the new guard ran there on your own runner rather than only on my machine:

built build/libvirtualsx1262.a
built build/libvirtualsx1262.dll
checked build/libvirtualsx1262.dll: nothing imported beyond kernel32 and msvcrt

consumers are up to date fails, and I do not think this PR can make it pass.

::warning::MeshBench/meshcore-native pins 0debbb9125bb, 2 commits behind:
           build: the Windows DLL carries its own runtime

That is the check working as written: meshcore-native pins this repository as a submodule at what is currently main, and any commit here puts it behind. The pin can only move to a commit that exists, so on a pull_request the consumer is behind by construction — the PR head is a merge commit nothing could have pinned yet. The job already reasons about this for pushes:

if [ "pull_request" = push ]; then
  echo "advisory on a push: the pin cannot have moved to this commit yet"
  exit 0
fi
exit 1

The same argument applies more strongly to a pull request than to a push, so as it stands the job goes red on every PR to this repository regardless of what the PR does — a check that cannot pass is one people learn to scroll past, which is the opposite of what it is for. Two options, neither of which belongs in this PR:

  1. Make it advisory on pull_request too, and keep it hard on push to main — where a lagging pin is a real, fixable state.
  2. Keep it hard on PRs but compare the consumer's pin against the PR's base rather than its head, so it only fails when the consumer was already behind before this change.

The follow-up this is correctly reminding us about is real either way: after this merges, MeshBench/meshcore-native needs its vendor/virtual-sx1262 submodule moved to the new commit, or it keeps building the old chip model. And separately, MeshBench pins a release rather than a commit — CHIPMODEL_RELEASE=v1.3.0 in packaging/emulator-pins.env — so closing MeshBench#632 needs a v1.3.1 cut here and that pin bumped. Happy to do both once this lands, if you want them.

@A13xB0
A13xB0 merged commit 2861950 into main Sep 6, 2026
7 of 8 checks passed
@A13xB0
A13xB0 deleted the fix/windows-static-runtime branch September 6, 2026 18:19
A13xB0 added a commit to MeshBench/meshbench that referenced this pull request Sep 6, 2026
v1.3.0's Windows DLL imported libstdc++-6.dll. That exists inside the
MSYS2 environment it was built in and almost nowhere else, so on any
ordinary Windows machine neither emulator could open the chip model:

  qemu-system-xtensa.exe: sx1262: cannot load the chip model at
  ...\libvirtualsx1262.dll: The specified module could not be found.

which names the DLL rather than the dependency behind it. Renode reaches
the same library through LoadLibraryW and failed the same way, so this was
every emulated board on Windows, ESP32 and nRF52 alike. It surfaced three
steps downstream as "firmware started but never connected within 40s" - a
radio that never answers, because the model behind it never loaded.

Nothing was lost in packaging: the release staged what build.sh produced
and we shipped what we unpacked. The build asked for a dynamic runtime on
a platform that has none. MeshBench/virtual-sx1262#1 links it statically;
v1.3.1 is that build, same ABI and same model, and imports only kernel32
and msvcrt.

Verified against the published artefact rather than a local build: the
v1.3.1 windows tarball dropped into a v0.0.8 install, probe cache cleared.

  board.probe Generic_E22_sx1262
    v1.3.0   boot  failed   firmware started but never connected within 40s
    v1.3.1   boot  passed   attached and answering

The Windows tarball grows from 28 kB to 143 kB, which is the static
runtime and the whole cost of the fix. The catalogue's digests and sizes
move with the pin, as the rule at the top of emulator-pins.env requires.

Closes #632.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 added a commit to MeshBench/meshcore-native that referenced this pull request Sep 6, 2026
The pin was two commits behind and the consumers check in that repository
was saying so on every run. It now names the merge of
MeshBench/virtual-sx1262#1.

What that commit changes is the upstream build.sh: the Windows shared
library is linked statically, because linked the default way it imported
libstdc++-6.dll and neither emulator could open it on an ordinary Windows
machine (MeshBench#632). Nothing in src/ moved.

So this bump is a no-op for what this repository produces. build.sh here
compiles the model from vendor/virtual-sx1262/src rather than loading the
shared library, and already links -static -static-libgcc -static-libstdc++
for Windows, so the fault never reached the native firmware and the fix
does not change it either.

It is worth taking anyway. A submodule is a commit and nothing moves it on
its own, so a pin left behind stays behind - and the next change upstream
that does touch src/ should arrive on its own rather than stacked behind
an unrelated lag nobody has looked at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant