Skip to content

x86: find_program selects llvm-lib for import libraries, which omits __stdcall decoration, breaking all 32-bit builds (LNK2001) #93

Description

@alex19EP

Summary

On x86 (Win32) targets, prism_add_import_library() can generate import libraries with the wrong symbol decoration, so every 32-bit build fails to link with LNK2001 on the ZDSR, PCTalker and BoyPCReader imports.

The cause is archiver selection, not the .def files:

# cmake/PrismPlatformWindows.cmake:32
find_program(PRISM_LIB_TOOL NAMES lib llvm-lib)

MSVC's lib.exe is only on PATH inside a developer command prompt. When CMake is driven by the Visual Studio generator from an ordinary shell — the common case, since the VS generator locates the toolchain itself and needs no vcvarsalllib is not found and find_program falls through to whatever llvm-lib.exe happens to be on PATH (e.g. from an unrelated LLVM install).

llvm-lib does not apply the x86 __stdcall leading-underscore decoration when generating an import library from a .def, so the resulting library exports __imp_InitTTS@12 while MSVC-compiled code references __imp__InitTTS@12.

Reproduction

On a Windows machine with LLVM on PATH and no developer prompt active:

cmake -S . -B build32 -A Win32 -DBUILD_SHARED_LIBS=ON -DPRISM_ENABLE_TESTS=OFF -DPRISM_ENABLE_DEMOS=OFF
cmake --build build32 --config Release

Prism's own prism.dll fails to link with 18 unresolved externals:

zdsr.obj : error LNK2001: unresolved external symbol __imp__InitTTS@12
zdsr.obj : error LNK2001: unresolved external symbol __imp__Speak@8
zdsr.obj : error LNK2001: unresolved external symbol __imp__StopSpeak@0
zdsr.obj : error LNK2001: unresolved external symbol __imp__GetSpeakState@0
zdsr.obj : error LNK2001: unresolved external symbol __imp__Braille@8
pc_talker.obj : error LNK2001: unresolved external symbol __imp__PCTKStatus@0
pc_talker.obj : error LNK2001: unresolved external symbol __imp__PCTKPinWriteW@12
... (8 total from pc_talker.obj)
boy_pc_reader.obj : error LNK2001: unresolved external symbol __imp__BoyCtrlSpeak@12
... (5 total from boy_pc_reader.obj)
fatal error LNK1120: 18 unresolved externals

A static build shows the same failure in any consumer instead of in Prism itself.

CMakeCache.txt confirms the selection:

PRISM_LIB_TOOL:FILEPATH=C:/Program Files/LLVM/bin/llvm-lib.exe

Evidence

Both tools run on the unmodified defs/zdsr32.def, same arguments Prism uses
(/nologo /def:... /out:... /machine:X86), symbols via dumpbin /linkermember:1:

archiver symbols produced
MSVC lib.exe (14.44.35207) _InitTTS@12, __imp__InitTTS@12 — correct
llvm-lib.exe (LLVM on PATH) InitTTS@12, __imp_InitTTS@12 — missing the underscore

Switching only the archiver, changing nothing else, makes the 32-bit build link and run correctly.

Please note: the .def files are correct

It is tempting to "fix" this by adding a leading underscore in defs/*32.def. That makes things worse — MSVC's lib.exe adds the prefix itself. Verified:

.def entry symbol from MSVC lib.exe
InitTTS@12 (current) __imp__InitTTS@12 — correct
_InitTTS@12 __imp___InitTTS@12 — wrong
_InitTTS@12 = InitTTS@12 __imp___InitTTS@12 — wrong

Why CI does not catch this

.github/workflows/build-windows.yml builds x64 and amd64_arm64 only, and both use ilammy/msvc-dev-cmd, which puts MSVC's lib.exe on PATH. So the matrix misses it twice over: x64/arm64 have no __stdcall decoration for the mismatch to show up in, and the developer environment means find_program never reaches the llvm-lib fallback.

Suggested fixes

Any of these would resolve it; the first seems most robust:

  1. Select the archiver matching the active compiler. When MSVC is set, prefer lib.exe next to CMAKE_CXX_COMPILER (${CMAKE_CXX_COMPILER}/../lib.exe) or via CMAKE_AR, and only consider llvm-lib for clang-cl/LLVM toolchains.
  2. Reject a mismatched tool. If PRISM_LIB_TOOL resolves to llvm-lib while MSVC is set and PRISM_ARCH_CLASS is x86, fail with a clear message rather than emitting silently-wrong libraries.
  3. Add an x86 job to the Windows CI matrix (msvc_arch: amd64_x86), which would have caught the decoration mismatch immediately.

A message(STATUS "Prism import archiver: ${PRISM_LIB_TOOL}") would also make this far easier to diagnose — the failure currently surfaces only as unresolved externals with no hint that tool selection was involved.

Workaround

Consumers can pre-set the cache variable:

cmake -S . -B build32 -A Win32 -DPRISM_LIB_TOOL="C:/.../VC/Tools/MSVC/<ver>/bin/Hostx64/x64/lib.exe"

With that, all nine Windows backends build and work on x86 — I have NVDA speaking through a 32-bit host with this in place.

Environment

  • Prism dce6eee62621e37617be5f1cc17d5ed8232dac01 (v0.17.3-19-gdce6eee)
  • Windows 11 Pro 26200, CMake 4.4.1, Visual Studio 2022 Build Tools (MSVC 14.44.35207)
  • LLVM installed at C:\Program Files\LLVM and on PATH
  • Target: Win32/x86, consumed by a 32-bit game plugin

Thanks for Prism — being able to reach NVDA over its RPC interface with no nvdaControllerClient.dll to redistribute is exactly what this project needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions