Skip to content

One garbage collector per process: TypeScriptRuntime.dll on gc.dll (Windows) - #309

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-single-gc-collector
Sep 12, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-single-gc-collector

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

Under -mm=gc, every binary that links Boehm statically runs its own collector. When two of them share a process, a collection frees objects that only the other binary's memory still references. Nothing crashes; the program reads wrong data.

TypeScriptRuntime.dll and the default library's TypeScriptDefaultLib.dll both linked Boehm statically. So a JIT run that loads the default library, or any gc DLL, had two collectors. An exe that loads a user DLL also had two, because the default-library DLL brought its own. This PR is step 1, 5 and part of 6 of tslang/docs/single-gc-collector-design.md, included here as a proposal.

  • Runtime on gc.dll (Windows only).
    • The top-level CMake defines an imported tslang_gc_shared target from 3rdParty/gcdll/x64/<build>. TSLANG_GC_SHARED_PREFIX overrides the path, and GC_DLL makes the Boehm headers declare its functions as imported.
    • Configure now fails on Windows if the shared Boehm is missing.
    • TypeScriptRuntime links the new target and copies gc.dll into bin/. gc.cpp no longer forces static linking when GC_DLL is set.
  • Builds and packaging.
    • prepare_3rdParty.bat builds the shared Boehm.
    • The release zip ships bin/gc.dll in its root.
    • The release workflow passes GC_SHARED_LIB_PATH to the default-library build.
  • Tests. New import_gc_single_collector.ts / export_gc_single_collector.ts, registered as -jit -shared and AOT -shared. The library side builds the strings the importer holds, then churns with different content. That makes the library's own collector run, and it must see the importer's array. The existing owned-returns tests churn in the importer, which is why their JIT variant passed with two collectors.
  • Docs. docs/memory-models.md and the design doc.

Companion PR: TypeScriptCompilerDefaultLib, branch fix-single-gc-collector. It links the gc TypeScriptDefaultLib.dll against gc.dll.

Linux is unchanged; nothing has been measured there yet.

Measured (Windows release)

Every "before" failure also passed with collection suppressed (GC_INITIAL_HEAP_SIZE=1GB), so the extra collector is the cause.

Case Before After
test-jit-shared-export-import-gc-single-collector assertion failed pass
test-compile-shared-export-import-gc-single-collector pass pass
JIT + default-library DLL, 2000 held strings 2000 bad 0 bad
exe + user DLL + default-library DLL 1984 bad 0 bad

The new JIT test was also rerun with the old static-Boehm runtime to confirm it still fails there.

Test plan

  • Full ctest, Windows release: 2,704 of 2,704
  • TypeScriptRuntime.dll and TypeScriptDefaultLib.dll both import gc.dll (llvm-readobj --coff-imports)
  • Plain JIT and async JIT smoke tests (thread registration through gc.dll)
  • CI green on Windows and Linux
  • Release workflow (only runs on a tag): zip contains gc.dll in the root and gcdll/

Not in this PR

  • Steps 3, 4 and 6 of the design:
    • tslang choosing the shared GC for --emit=dll and for exes that import a DLL
    • an import-time error for a DLL that carries its own collector
    • packaging docs
  • A debug build of the default library against the debug shared Boehm.
  • Linux.
  • A separate compiler bug found here, not fixed: in a module compiled as an import, a const declared in a for body in a namespace function gives "can't resolve name". The new test uses let.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits September 12, 2026 23:25
The gc default-library DLL links the static gc.lib, so it runs a collector of
its own beside TypeScriptRuntime.dll (JIT) and beside gc.dll (exe + user
DLL). Measured: 2000/2000 and 1984/2000 held strings read freed memory,
0 with collection suppressed. The proposal takes Boehm from gc.dll in every
binary that can share a process, and keeps the static gc.lib for a lone exe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….dll

TypeScriptRuntime.dll linked Boehm statically, so a JIT run that loads a gc
shared library - a user's, or the default library's DLL - had two
collectors, and one freed strings only the other's memory referenced
(2000/2000 under the JIT).

On Windows the runtime now takes Boehm from gc.dll: the top-level CMake
defines an imported tslang_gc_shared target from 3rdParty/gcdll
(TSLANG_GC_SHARED_PREFIX, GC_DLL), fails to configure without it, and copies
gc.dll into bin/. prepare_3rdParty.bat builds it; the release zip ships it
and passes GC_SHARED_LIB_PATH to the default-library build, whose DLL now
links gc.dll too (TypeScriptCompilerDefaultLib, same branch).

New test pair gc_single_collector: the library builds the held strings AND
churns, so its collector is the one that must run. Fails with the old
runtime, passes with the new one; JIT + default-lib DLL and exe + user DLL +
default-lib DLL repros go from 2000/2000 and 1984/2000 bad to 0. Linux
unchanged. Suite: 2,704 of 2,704.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 997a366 into main Sep 12, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-single-gc-collector branch September 12, 2026 23:00
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.

1 participant