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 vcvarsall — lib 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:
- 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.
- 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.
- 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.
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 withLNK2001on the ZDSR, PCTalker and BoyPCReader imports.The cause is archiver selection, not the
.deffiles:MSVC's
lib.exeis only onPATHinside 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 novcvarsall—libis not found andfind_programfalls through to whateverllvm-lib.exehappens to be onPATH(e.g. from an unrelated LLVM install).llvm-libdoes not apply the x86__stdcallleading-underscore decoration when generating an import library from a.def, so the resulting library exports__imp_InitTTS@12while MSVC-compiled code references__imp__InitTTS@12.Reproduction
On a Windows machine with LLVM on
PATHand no developer prompt active:Prism's own
prism.dllfails to link with 18 unresolved externals:A static build shows the same failure in any consumer instead of in Prism itself.
CMakeCache.txtconfirms the selection:Evidence
Both tools run on the unmodified
defs/zdsr32.def, same arguments Prism uses(
/nologo /def:... /out:... /machine:X86), symbols viadumpbin /linkermember:1:lib.exe(14.44.35207)_InitTTS@12,__imp__InitTTS@12— correctllvm-lib.exe(LLVM on PATH)InitTTS@12,__imp_InitTTS@12— missing the underscoreSwitching only the archiver, changing nothing else, makes the 32-bit build link and run correctly.
Please note: the
.deffiles are correctIt is tempting to "fix" this by adding a leading underscore in
defs/*32.def. That makes things worse — MSVC'slib.exeadds the prefix itself. Verified:.defentrylib.exeInitTTS@12(current)__imp__InitTTS@12— correct_InitTTS@12__imp___InitTTS@12— wrong_InitTTS@12 = InitTTS@12__imp___InitTTS@12— wrongWhy CI does not catch this
.github/workflows/build-windows.ymlbuildsx64andamd64_arm64only, and both useilammy/msvc-dev-cmd, which puts MSVC'slib.exeonPATH. So the matrix misses it twice over: x64/arm64 have no__stdcalldecoration for the mismatch to show up in, and the developer environment meansfind_programnever reaches thellvm-libfallback.Suggested fixes
Any of these would resolve it; the first seems most robust:
MSVCis set, preferlib.exenext toCMAKE_CXX_COMPILER(${CMAKE_CXX_COMPILER}/../lib.exe) or viaCMAKE_AR, and only considerllvm-libfor clang-cl/LLVM toolchains.PRISM_LIB_TOOLresolves tollvm-libwhileMSVCis set andPRISM_ARCH_CLASSisx86, fail with a clear message rather than emitting silently-wrong libraries.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:
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
dce6eee62621e37617be5f1cc17d5ed8232dac01(v0.17.3-19-gdce6eee)C:\Program Files\LLVMand onPATHThanks for Prism — being able to reach NVDA over its RPC interface with no
nvdaControllerClient.dllto redistribute is exactly what this project needed.