Skip to content

Implement Windows privatization of bundled libjulia#151

Draft
asinghvi17 wants to merge 12 commits into
JuliaLang:mainfrom
asinghvi17:as/windows-privatize
Draft

Implement Windows privatization of bundled libjulia#151
asinghvi17 wants to merge 12 commits into
JuliaLang:mainfrom
asinghvi17:as/windows-privatize

Conversation

@asinghvi17

Copy link
Copy Markdown
Contributor

Summary

JuliaC's privatize_libjulia! handles macOS and Linux, but on Windows it was a no-op (@warn "Privatization not implemented for this OS"). This PR implements Windows privatization so BundleRecipe(privatize=true) / --privatize works on Windows too.

Mechanism

Windows PEs have no SONAME, so the Unix salt-rename approach doesn't apply. This uses the standard Windows side-by-side (SxS) isolation mechanism instead:

  • Private manifest: an application manifest is injected as a .rsrc resource into the built product, so a directly-launched product resolves its own bundled libjulia.dll via an activation context. The manifest XML is generated in code (identity JuliaC.PrivateRuntime.<product>, with a <file> list built from the libjulia*.dll actually present — so trim builds that drop libjulia-codegen.dll are handled). Only an 88-byte precompiled resource-directory header (src/template/rsrc.bin, no text content) is shipped as a static asset.
  • libpath fix: the embedded ../bin/ prefix in libjulia.dll's libpath string is stripped in place.

Implementation

  • New src/privatize_windows.jl with privatize_libjulia_windows! — standalone (not the plat_* hooks, which model Unix salt-renaming).
  • inject_private_manifest!: build the .rsrc section, add it via objcopy, then patch the COFF optional-header ResourceTable data directory and the section's manifest address/size fields using ObjectFile + StructIO.
  • No new dependency: objcopy is taken from the mingw-w64 artifact JuliaC already uses for Windows linking (resolution factored into shared mingw_bindir / mingw_tool helpers in linking.jl).
  • Wired into the privatize_libjulia! dispatch in bundling.jl, replacing the @warn.

Isolation contract (v1)

This makes a directly-launched product load its own bundled libjulia.dll; it does not rename libjulia.dll, so it does not guarantee multiple privatized bundles coexisting in a single host process. Per-bundle DLL renaming + import-table rewriting is a possible follow-up.

Tests

A guarded if Sys.iswindows() testset in test/programatic.jl builds a bundle with privatize=true and reads the injected resource directory back from the product's PE headers (raw seek/read, no external tools). Locally (Linux) the COFF parse/patch offset math, manifest generation, and libpath-fix algorithm were verified against real PE binaries (including the 32-bit branch); actually running objcopy --add-section and the SxS loader honoring the manifest are exercised by this testset on Windows CI.

🤖 Generated with Claude Code

asinghvi17 and others added 9 commits May 28, 2026 22:48
Ship the 88-byte precompiled .rsrc resource-directory header
(src/template/rsrc.bin) used to inject an SxS private-assembly
RT_MANIFEST resource into Windows PE products. The blob encodes the
resource tree RT_MANIFEST(0x18) -> id 0x2 -> langid 0x409 -> data entry
at byte 0x48, with the OffsetToData/Size fields zeroed (patched at
injection time). It is pure binary (no embedded strings).

sha256: 8e50045a2142f22092d43ada239841c192f79ea9ec1e56697cca2e9e4de001b2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the mingw-w64 artifact bin/ directory lookup out of
get_compiler_cmd into reusable mingw_bindir() and mingw_tool(name)
helpers so linking and the upcoming Windows privatization share one
source of truth for the toolchain location. The Windows branch of
get_compiler_cmd now calls mingw_tool, preserving existing behavior.

The artifact lookup is wrapped in @static if Sys.iswindows() inside
mingw_bindir() because the @artifact_str macro expands at compile time
and the artifact entry is os="windows"-gated, so it is unresolvable
off-Windows; this keeps the package loadable on non-Windows hosts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Create src/privatize_windows.jl with the module header, ObjectFile /
StructIO imports, the TEMPLATE_DIR RelocatableFolders constant pointing
at the shipped rsrc.bin, the .rsrc data-entry patch offsets
(MANIFEST_ADDRESS_OFFSET 0x48, MANIFEST_SIZE_OFFSET 0x4c), and the
generate_manifest_xml / manifest_identity_for helpers plus the
LIBJULIA_DLL_CANDIDATES tuple.

The identity is JuliaC.PrivateRuntime.<basename>. The file is not yet
included by JuliaC.jl (wired in a later step), so the package still
loads unchanged. Manifest generator verified in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Append inject_private_manifest!(product_path, dll_names) to
privatize_windows.jl. It builds the .rsrc section payload (precompiled
header ++ generated manifest ++ 4-byte pad), adds it as a .rsrc section
via the mingw objcopy tool, then patches the COFF optional header's
ResourceTable data directory (VirtualAddress/Size) and the section's
internal IMAGE_RESOURCE_DATA_ENTRY OffsetToData/Size fields.

The COFF offset math, COFFImageDataDirectory pack, and ResourceTable
field offset were verified on Linux against real 32-bit and 64-bit PE
binaries (computed datadirs offset reproduces the actual .rsrc section
VA). Running objcopy.exe and resolving the mingw artifact are
Windows-CI-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Append the in-place libpath fix that strips the stale ../bin/ prefix
(and @../bin/ -> @) from the bundled libjulia.dll's embedded,
colon-separated, NUL-terminated library search path; present_libjulia_dlls
to list the libjulia*.dll files actually present in the bundle bin/ dir
(so trim builds that drop libjulia-codegen.dll produce a correct
manifest); and the standalone privatize_libjulia_windows! entry point
that injects the manifest into the product and fixes the libpath, with
the try/catch + error(..., e) convention matching the macOS/Linux
wrappers.

The libpath rewrite algorithm was verified on Linux fixtures: rewrites
../bin/a:../bin/b:@../bin/c -> a:b:@c, leaves prefix-less entries
untouched (../bin/x:plainentry:@../bin/y -> x:plainentry:@y), preserves
trailing bytes, and errors with "Unable to open" on a missing file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add include("privatize_windows.jl") after privatize_macos.jl in
JuliaC.jl, and replace the Windows @warn no-op in privatize_libjulia!
with an elseif Sys.iswindows() branch dispatching to
privatize_libjulia_windows!. The full package loads with all new symbols
defined, the patch-offset consts correct (0x48/0x4c), and the
TEMPLATE_DIR RelocatableFolders path resolving to the 88-byte rsrc.bin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Sys.iswindows()-guarded testset inside the Library flows (trim)
block that builds a privatized library bundle, then asserts the injected
.rsrc resource directory is present (read via ObjectFile), the COFF
optional-header ResourceTable data directory points at the .rsrc section
VA (read independently via raw seek/read, recomputing the
datadirs offset from the file), and the bundled libjulia.dll no longer
contains the ../bin/ prefix. No objdump/dumpbin dependency.

ObjectFile is referenced as JuliaC.ObjectFile since it is a JuliaC dep
but not in the test target extras. On Linux the body is skipped; the
full suite passes (Pkg.test). The Windows assertions run on
windows-latest CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment-only: trim the three verbose explanatory blocks in the Windows
privatization file down to one line each.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@topolarity

Copy link
Copy Markdown
Member

This looks pretty good to me, but would be good to have a list of changes versus the original implementation and also a test that actually loads the library within Julia and confirms that dllist() shows 2 copies of the Julia libraries.

@asinghvi17 anything that's keeping it in draft?

asinghvi17 and others added 3 commits May 29, 2026 23:10
Add a Windows-gated test that loads the bundled, privatized product
within a fresh Julia process and confirms `Libdl.dllist()` reports two
distinct copies of the runtime libraries: the host process's
already-loaded copy plus the bundled private copy that the side-by-side
(SxS) manifest steers the product to load from its own directory. Also
checks that the exported symbol still resolves through the privatized
runtime. Gated to `Sys.iswindows()` so the suite is a no-op elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the wrapped explanatory comment blocks in the privatized-load testset
to single lines. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add flushed progress markers (M1_PRE_DLOPEN .. M5_POST_CCALL) so the CI log
shows whether the hang is at SxS activation (load) or the first-ccall runtime
init, and report Libdl.dllist() dirs right after dlopen (before the ccall) to
tell whether the product bound its own private libjulia. Run the subprocess
under a 180s watchdog so the job can never again consume the 6h CI limit.

Diagnostic step toward fixing the Windows-only deadlock; not the final form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants