Embedded LLD Phase 5: Windows #4995
SeanTAllen
started this conversation in
ponyc
Replies: 1 comment
-
|
Implemented in PR #4996. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Phase 5 implementation plan for the Embedded LLD project. This phase switches native Windows linking from the external MSVC
link.execommand (invoked viasystem()throughcmd /C) to embedded LLD's COFF driver (lld::coff::link).Scope
What this phase delivers:
--linkerkept as escape hatch to legacysystem()pathWhat this phase does NOT do:
Prerequisites
All in place from earlier phases:
LLD_HAS_DRIVER(coff)declaration already present ingenexe.ccline 7genexe.ccis C++ (Phase 1)program_lib_build_args_embedded()API available (Phase 1)link_exe_lld_elf()andlink_exe_lld_macho()for argument vector construction, LLD invocation, verbose logging, and error handling (Phases 2-4)Steps
Step 1: Move C++ and LLD includes outside the POSIX guard
File:
src/libponyc/codegen/genexe.ccThe LLVM and C++ standard library headers needed for LLD invocation are currently inside
#ifdef PLATFORM_IS_POSIX_BASED(lines 27-35). Windows needs them too. Move<llvm/Support/raw_ostream.h>,<vector>, and<string>outside the guard. Keep<unistd.h>,<sys/stat.h>,<dirent.h>, and<llvm/TargetParser/Triple.h>inside — these are POSIX-specific or only used by ELF/Mach-O helpers.Before:
After:
Step 2: Implement
link_exe_lld_coff()File:
src/libponyc/codegen/genexe.ccAdd a new static function inside an
#ifdef PLATFORM_IS_WINDOWSblock, placed after the#endifclosing thePLATFORM_IS_POSIX_BASEDblock (after line 1228) and before thelink_exe()function (line 1230).Function flow (mirrors the pattern from
link_exe_lld_elf()andlink_exe_lld_macho()):vcvars_get()— same as the legacy path. Error if it fails.suffix_filename(c, c->opt->output, "", c->filename, ".exe").verbosity >= VERBOSITY_MINIMAL.program_lib_build_args_embedded(program, c->opt)._M_ARM64/_M_X64macros — same as the legacy path.c->opt->link_ldcmd != NULL, emit a warning.verbosity >= VERBOSITY_TOOL_INFO.lld::lldMain()with COFF driver.Argument vector construction:
Detailed argument construction:
What's NOT in the argument list (and why):
msvcrt.lib/vcruntime.lib. The MSVC linker (and LLD's COFF driver) automatically finds the entry point (mainCRTStartup).--sysroot: Windows doesn't use the sysroot concept. SDK/MSVC paths are discovered via vcvars.-rpath: Windows doesn't embed runtime library search paths in executables. DLLs are found via PATH or side-by-side manifests.--export-dynamic: Not applicable to COFF/PE. The legacy Windows code doesn't pass an equivalent of-rdynamic.--strip-debug: The legacy code always passes/DEBUGand doesn't checkc->opt->strip_debug. Phase 5 preserves this behavior./DYNAMICBASEis on by default in LLD-link). No explicit flag needed.-static: The--staticoption is Linux/musl only. Windows doesn't have a static-binary mode in ponyc.LLD invocation (same pattern as ELF/Mach-O):
Note:
lldMain()selects the driver based onargv[0]. When argv[0] islld-link, it selects the COFF driver. All five drivers are registered to match the fiveLLD_HAS_DRIVERdeclarations at the top of the file.Step 3: Add routing logic in
link_exe()File:
src/libponyc/codegen/genexe.ccAdd routing between the end of the POSIX LLD routing (line 1259,
#endif) and the start of the legacy code (line 1261,const char* ponyrt):Routing logic:
--linkersystem()viacmd /C)When
--linkeris set, control falls through to the legacy Windows code at line 1441, which picks up the user-specified linker and invokes it viasystem().Why this routing is simpler than Linux/macOS:
PLATFORM_IS_WINDOWSis a compile-time guard; if it's defined, the target is WindowsStep 4: No CI workflow changes needed
All existing Windows CI jobs exercise the new code path automatically — no workflow changes required:
Tier 1 (PR):
.\make.ps1 -Command testwith Debug and Release configsTier 2 (daily):
.\make.ps1 -Command testwith Debug and Release configsBoth tiers also build examples (
.\make.ps1 -Command build-examples).The existing CI jobs compile and link Pony programs. With the routing change, these programs will be linked via embedded LLD instead of
link.exe. If embedded LLD produces incorrect binaries, the tests will fail — no additional test infrastructure is needed.Step 5: Verification with verbose output
After the change, running with
-V4on Windows should showlld-link /DEBUG /NOLOGO ...instead ofcmd /C ""...\link.exe" /DEBUG .... This is implicitly tested by CI (if linking fails, tests fail).Step 6: Release notes
Classification: changed. Native Windows builds now use embedded LLD instead of the external MSVC
link.exelinker.--linkerprovides an escape hatch to use an external linker.Since this is a single change type, use the standard automation: create
.release-notes/embedded-lld-windows.mdand apply thechangelog - changedlabel.Release note content:
Design Decisions
Why Windows is the simplest platform switch
Windows linking is structurally the simplest of all the platform switches because:
msvcrt.lib,vcruntime.lib).lld-link) accepts the same arguments as MSVC'slink.exe. The switch is essentially replacingsystem("cmd /C ...")withlld::lldMain()using the same argument list.vcvars_get() reuse
The existing
vcvars_get()function discovers SDK and MSVC installation paths via the Windows registry andvswhere.exe. Embedded LLD still needs these paths to findkernel32.lib,ucrt.lib, and the MSVC runtime libraries. The discovery logic is reused unchanged.The vcvars fields
vcvars.link(path tolink.exe) andvcvars.ar(path tolib.exe) are still populated byvcvars_get()but not used when embedded LLD handles linking. This is harmless — the discovery is a side effect of finding the MSVC installation path./LIBPATH:quoting: drop the quotesThe legacy code wraps vcvars paths in double quotes within the
/LIBPATH:argument (/LIBPATH:"%s"). This is necessary because the entire link command is passed throughcmd /Candsystem(), where shell tokenization would split on spaces in paths likeC:\Program Files (x86)\....With embedded LLD, each argument is a discrete entry in the vector — no shell involved. Including literal quote characters in the path would cause LLD to look for a path that literally includes
"characters, which would fail. The embedded LLD path uses/LIBPATH:<path>without quotes, consistent with how-L<path>is handled in the ELF and Mach-O paths.default_libstokenizationThe legacy code pastes
vcvars.default_libsas a single string into thesystem()command, relying on shell tokenization to split it. For embedded LLD, each library must be a separate argument.strtok()on a local copy is the simplest approach and matches the string's structure (space-separated.libnames).An alternative would be to change
vcvars.cto store libraries as an array instead of a string. This would be cleaner but touches more code than necessary. If Phase 7 (cleanup) removes the legacy path entirely,vcvars.ccan be refactored then.No sanitizer guard
The Linux and macOS routing includes a sanitizer guard (
#if defined(PONY_SANITIZER)) that falls back to the legacy path for native sanitizer builds. Windows doesn't need this guard because ponyc doesn't support sanitizer builds on Windows — there's noPONY_SANITIZERhandling in the Windows linking code, and-fsanitizeis a GCC/Clang concept.Architecture: compile-time detection preserved
The legacy code uses
_M_ARM64and_M_X64compile-time macros to determine the target architecture. This is preserved because Windows builds are always native (cross-compilation to Windows is future work). When cross-compilation support is added later, this would change to runtime triple inspection.No
--strip-debugon WindowsThe legacy code always passes
/DEBUGtolink.exeand doesn't checkc->opt->strip_debug. Phase 5 preserves this behavior. Adding--stripsupport on Windows is a separate improvement that would need its own consideration.All LLD drivers registered
The
lldMain()call registers all five LLD drivers, matching the fiveLLD_HAS_DRIVERdeclarations at the top of the file, consistent with the pattern established in Phase 4.What doesn't change
--linkerescape hatch — still falls through to legacy path--link-ldcmd— ignored with a warning under embedded LLD (currently meaningless on Windows anyway, but now explicitly warned)_M_ARM64/_M_X64link_exe_lld_elf()/link_exe_lld_macho()Testing
Build command:
.\make.ps1 -Command test -Config DebugCI coverage (all existing, no new entries):
pr-ponyc.ymlponyc-tier2.ymlVerification: Running with
-V4on Windows should showlld-link /DEBUG /NOLOGO ...instead ofcmd /C ""...\link.exe" /DEBUG ....Risk Assessment
Low risk. Windows linking is the simplest platform because the argument list is a near-1:1 translation from the legacy
link.exeinvocation. No CRT objects, no sysroot, no static/dynamic split, no libc variants. LLD's COFF driver is specifically designed to be a drop-in replacement forlink.exe. The--linkerescape hatch provides an immediate workaround if anything unexpected arises.Known risk: LLD COFF driver compatibility. LLD's COFF driver may handle some edge cases differently from MSVC's
link.exe(e.g.,/ignore:4099warning suppression, PDB generation details). The existing Windows CI (x86-64 tier 1, arm64 tier 2) provides solid coverage. If LLD COFF issues surface,--linker=<path-to-link.exe>is the immediate mitigation.Known risk: Path spaces without quoting. The switch from quoted
/LIBPATH:"..."(forcmd.exeshell) to unquoted/LIBPATH:...(direct API) is a behavioral change. Windows SDK paths commonly contain spaces (e.g.,C:\Program Files (x86)\Windows Kits\...). This works correctly because each argument is a discrete entry in the vector — no shell tokenization can split on spaces. LLD's COFF driver handles/LIBPATH:parsing correctly for paths with spaces when passed as individual arguments.Files Modified
src/libponyc/codegen/genexe.cclink_exe_lld_coff(), add routing inlink_exe().release-notes/embedded-lld-windows.mdBeta Was this translation helpful? Give feedback.
All reactions