Skip to content

runtime: give ten cross-language globals C linkage in main.cpp (MSVC) - #246

Merged
mstan merged 1 commit into
masterfrom
fix/msvc-main-cpp-linkage
Aug 28, 2026
Merged

runtime: give ten cross-language globals C linkage in main.cpp (MSVC)#246
mstan merged 1 commit into
masterfrom
fix/msvc-main-cpp-linkage

Conversation

@TechnicallyComputers

Copy link
Copy Markdown
Member

Follow-up to #245 (merged), covering the second problem @TheRealBiggs reported in #244:

There are also issues in main.cpp for game projects due to linkage differences with external variables. There are a few variables declared inside functions in main.cpp that needs to have C linkage. Moving them out of said functions and into an extern "C" block is enough.

Confirmed, and #245 did not address it — that PR was merged from a snapshot taken before this commit was pushed to the branch.

The problem

MSVC mangles namespace-scope variables; the Itanium ABI does not. So a C++ reference to a C-defined global links fine on GCC/Clang and fails on MSVC with an unresolved external.

main.cpp:180 already carries the file-scope extern "C" block from the earlier MSVC pass (PR #16 / df4cc5c), and its comment describes exactly this failure mode. Ten globals added since then never made it into the block:

defined in symbols
memory.c i_stat, i_mask, g_guest_store_count, g_vblank_ack_count
interrupts.c g_vblank_raise_count, g_vblank_deliver_count
dirty_ram_interp.c g_dirty_ram_blocks_run, g_dirty_pump_count
overlay_loader.c g_call_unit_depth
psx_bios_backend.c g_psx_dispatch_depth

Each is reached only through a block-scope extern inside a function. With no namespace-scope declaration in view, that block-scope declaration gives the entity C++ linkage — the failure. Adding them to the existing block fixes it without touching a single use site: the block-scope redeclarations inherit C linkage, exactly as the block's own comment already describes for the symbols that were there.

+13 lines, one file.

Which ten was measured, not eyeballed

Whether a given block-scope extern is a problem depends on whether some included header already declares it inside extern "C" — reading the file doesn't tell you. So every distinct block-scope extern variable in main.cpp (32 on current master) was probed by appending extern "C++" { extern T sym; } to a copy compiled with the target's real command line: a conflicting-linkage diagnostic means the symbol already has C linkage, silence means it doesn't.

  • Before: 11 report C++ linkage
  • After: 1

The remaining one is g_audio_unmute_resync, and it is not a bug — it's defined in main.cpp itself (main.cpp:1937, file scope) and referenced nowhere outside it, so C++ linkage is correct and consistent for it. Flagging it here so a reviewer re-running the probe doesn't think it was missed.

The audit was re-run against current master (which moved twice since #245 merged, 88f07bc9 touching main.cpp) — same ten, no new ones.

main.cpp is the only C++ TU in the runtime with this pattern. beetle_libretro.cpp's extern PS_GPU GPU / extern PS_CDC *PSX_CDC are genuine Beetle C++ symbols, correctly left alone, and psx-beetle isn't built under MSVC anyway.

Verification

  • main.cpp.o is byte-identical to its unpatched-master build (540,360 bytes both sides) — a strict no-op on GCC/Clang, as expected for declarations
  • Clean-build runtime suite: 49/50. The single failure is gte_register_access_test, pre-existing and unrelated — it links gte.cpp + pgxp.cpp but references gpu_ws_precise_nclip_enabled, defined in gpu.c, which isn't in that target's source list (runtime/CMakeLists.txt:101-105). It fails identically without this patch.

As with #245, I can't run cl.exe here — this is the standard extern "C" remedy and a no-op on the toolchains I can test, but a Windows build is the real confirmation.

Still outstanding from #244: nothing in CI compiles runtime/src with cl.exe. cli-release.yml has a windows-latest job, but it builds the CLI with the portable toolchain. That's why both the GNU-only constructs and these ten globals landed unnoticed, and why the next batch will too. A windows-latest MSVC configure+build job is the durable fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_014YD7rrVK63Kncki6prB65S

Second report on #244: main.cpp references C-defined globals that MSVC
cannot resolve. MSVC mangles namespace-scope variables; the Itanium ABI
does not, so a C++ reference to a C global links on GCC/Clang and fails
under MSVC with an unresolved external.

main.cpp already has the fix pattern -- the file-scope extern "C" block
added by the earlier MSVC pass, with a comment saying exactly this. Ten
globals added since then never made it into the block:

  memory.c            i_stat, i_mask, g_guest_store_count,
                      g_vblank_ack_count
  interrupts.c        g_vblank_raise_count, g_vblank_deliver_count
  dirty_ram_interp.c  g_dirty_ram_blocks_run, g_dirty_pump_count
  overlay_loader.c    g_call_unit_depth
  psx_bios_backend.c  g_psx_dispatch_depth

Each is referenced only through a block-scope `extern` inside a function.
With no namespace-scope declaration in view, that block-scope declaration
gives the entity C++ linkage, which is the failure. Declaring them in the
file-scope extern "C" block fixes it without touching the use sites: the
block-scope redeclarations now inherit C linkage, as the block's comment
already describes for the symbols that were there.

Which ten was measured, not eyeballed. Every block-scope extern in
main.cpp (31 distinct symbols) was probed by appending
`extern "C++" { extern T sym; }` to a copy of the file compiled with the
target's real command line: a conflicting-linkage diagnostic means the
symbol already has C linkage from an extern "C" header, silence means it
does not. 21 came back already-C (declared in psx_cycles.h, cpu_state.h,
debug_server.h and friends, or already in this block); these 10 came back
C++. After the change all 31 probe as C linkage.

Audited beyond main.cpp: it is the only C++ TU in the runtime with this
pattern. beetle_libretro.cpp's `extern PS_GPU GPU` / `extern PS_CDC *PSX_CDC`
are genuine C++ symbols from the Beetle core, correctly left with C++
linkage, and psx-beetle is not built under MSVC in any case.

No behaviour change on the existing toolchains: main.cpp.o is byte-identical
to its origin/master build. Full runtime suite on a clean build dir is 47/48,
the single failure being gte_register_access_test, which fails identically at
origin/master (43/44 there) -- it links gte.cpp + pgxp.cpp but references
gpu_ws_precise_nclip_enabled, defined in gpu.c, which is not in that target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YD7rrVK63Kncki6prB65S
@mstan
mstan merged commit ad5e5a2 into master Aug 28, 2026
@TheRealBiggs

Copy link
Copy Markdown
Contributor

One more thing I forgot to mention. In main.cpp, the first 2 lines of main() will cause the game to fail to run after compiling with MSVC. This is because 0 is passed as the last argument, which is valid under GCC but not MSVC. MSVC requires a size to be passed when using _IOLBF. Changing the 0 to BUFSIZ fixes this and allows the game to run correctly.

@TechnicallyComputers

Copy link
Copy Markdown
Member Author

do you have a PR you could submit for that?

@mstan
mstan deleted the fix/msvc-main-cpp-linkage branch September 3, 2026 18:24
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.

3 participants