Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ jobs:
.\build.bat
shell: pwsh

# The compiler suite runs before the default library exists, so its default-library tests
# skip there; run them now against the library just built. One collector per process with
# the default library in it: tslang/test/tester/defaultlib-collector.cmake.
- name: Test Compiler with Default Library
continue-on-error: false
working-directory: ${{github.workspace}}/__build/tslang/msbuild/x64/release
env:
DEFAULT_LIB_PATH: ${{github.workspace}}/TypeScriptCompilerDefaultLib/__build
TSLANG_REQUIRE_DEFAULT_LIB: 1
run: ctest -C ${{ env.BUILD_TYPE }} -R gc-defaultlib-collector --output-on-failure
shell: pwsh

- name: Test Default Library
continue-on-error: false
working-directory: ${{github.workspace}}/TypeScriptCompilerDefaultLib
Expand Down Expand Up @@ -379,6 +391,16 @@ jobs:
TSLANG_LIB_PATH: ${{github.workspace}}/__build/tslang/ninja/release/lib
run: chmod +x ./build.sh ./scripts/*.sh; ./build.sh

# See the Windows job: the default-library tests skip in the compiler suite and run here.
- name: Test Compiler with Default Library
continue-on-error: false
working-directory: ${{github.workspace}}/__build/tslang/ninja/release
shell: sh
env:
DEFAULT_LIB_PATH: ${{github.workspace}}/TypeScriptCompilerDefaultLib/__build
TSLANG_REQUIRE_DEFAULT_LIB: 1
run: ctest -C ${{ env.BUILD_TYPE }} -R gc-defaultlib-collector --output-on-failure

- name: Test Default Library
continue-on-error: false
working-directory: ${{github.workspace}}/TypeScriptCompilerDefaultLib
Expand Down
19 changes: 18 additions & 1 deletion docs/how/cmake_tslang/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ endif()
set(TSLANG_MEMORY_MODEL "gc" CACHE STRING "Memory model of compiled code: gc, rc or none")
set_property(CACHE TSLANG_MEMORY_MODEL PROPERTY STRINGS gc rc none)

# Turn on when the program imports a tslang shared library (`import './lib'`, built with
# `tslang --emit=dll`). Under gc the process must then have one collector, shared by the program
# and the library: on Windows both take it from gc.dll, on Linux the program exports its own.
# Two collectors free each other's live objects. See docs/memory-models.md.
option(TSLANG_SHARED_GC "The program loads tslang shared libraries (gc: share one collector)" OFF)

# Lib folders
link_directories(${CMAKE_TSLANG_DIR} ${CMAKE_TSLANG_DIR}/defaultlib/lib/${TSLANG_DEFAULTLIB_BUILD}/${TSLANG_MEMORY_MODEL})

Expand Down Expand Up @@ -56,7 +62,18 @@ set(TSLANG_LINK_LIBS "TypeScriptDefaultLib" "TypeScriptAsyncRuntime" "LLVMSuppor
# Boehm is only referenced by the gc default lib; the rc and none builds allocate through the
# CRT and must not drag a collector in.
if (TSLANG_MEMORY_MODEL STREQUAL "gc")
list(APPEND TSLANG_LINK_LIBS "gc")
if (NOT TSLANG_SHARED_GC)
list(APPEND TSLANG_LINK_LIBS "gc")
elseif(WIN32)
# gc.dll's import library. It has the same name as the static gc.lib in the package root,
# so it is named by its full path rather than found through the link directories.
list(APPEND TSLANG_LINK_LIBS "${CMAKE_TSLANG_DIR}/gcdll/gc.lib")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_TSLANG_DIR}/gcdll/gc.dll" "$<TARGET_FILE_DIR:${PROJECT_NAME}>")
else()
# the whole collector, exported, so a shared object's GC_* calls bind to this copy
list(APPEND TSLANG_LINK_LIBS "-Wl,--whole-archive" "gc" "-Wl,--no-whole-archive" "-Wl,--export-dynamic-symbol=GC_*")
endif()
endif()

# ntdll provides RtlGetLastNtStatus (pulled in by LLVMSupport) on Windows
Expand Down
19 changes: 19 additions & 0 deletions docs/how/cmake_tslang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ It selects `defaultlib/lib/<debug|release>/<model>` as the link directory and ad
to the compile flags, so the two cannot disagree. Valid values are `gc` (default), `rc` and
`none`; only `gc` links Boehm.

## Programs that load tslang shared libraries

A program that imports a tslang shared library (`import './lib'`) has to share one garbage
collector with it under `gc`. With two, each frees objects the other still holds. This template
does not build shared libraries itself: build them with `tslang --emit=dll`, which does this on its
own. Then configure the program with:

```
cmake --preset default -DTSLANG_SHARED_GC=ON
```

- Windows: links `gcdll/gc.lib` (gc.dll's import library) instead of the static `gc.lib`, and copies
`gc.dll` beside the program. Ship it with `TypeScriptDefaultLib.dll`
(`defaultlib/dll/<release|debug>/gc`) and your libraries.
- Linux: links the whole static collector and exports its `GC_*` symbols, so the shared library
uses the program's collector.

Leave it `OFF` for a program that loads none: it then ships as a single file.

## Minimal alternative

If you don't need a first-class language, either:
Expand Down
47 changes: 41 additions & 6 deletions docs/memory-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,48 @@ the executable's roots, so it frees objects the executable is still holding. The
a crash: the freed memory is reallocated and the program reads a plausible wrong value, which
only shows up when what was written over it differs from what was there.

The Windows release package ships the shared collector in its `gcdll` folder, beside the static
`gc.lib` at its root — both files are named `gc.lib`, so the folder is what tells them apart.
Compile the executable **and** every shared library with `--gc-lib-path=<package>/gcdll`, and
ship `gcdll/gc.dll` beside the executable.
On Windows `tslang` makes the choice itself:

From a source build, the same files come from `scripts/build_gc_release_shared_vs.bat`: link
against `3rdParty/gcdll/x64/release/lib/gc.lib` and ship `3rdParty/gcdll/x64/release/bin/gc.dll`.
- `--emit=dll`, and `--emit=exe` for a program that imports a tslang shared library, link the
shared collector and copy `gc.dll` beside the output.
- `--emit=exe` for a program that imports none keeps the static `gc.lib`, and ships as one file.

It finds the shared collector through `--gc-shared-lib-path` (or `GC_SHARED_LIB_PATH`), else the
`gcdll` folder inside `--gc-lib-path` — which is where the Windows release package ships it, beside
the static `gc.lib` at its root; both files are named `gc.lib`, so the folder is what tells them
apart — else `--gc-lib-path` itself when that already names a shared build. If none of those has
one, the build stops with an error rather than linking a collector of its own.

From a source build, the shared collector comes from `scripts/build_gc_release_shared_vs.bat`:
`--gc-shared-lib-path=3rdParty/gcdll/x64/release/lib` (with `gc.dll` in its `../bin`).

Linking by hand (`--emit=obj` and your own linker) makes no choice for you: link the executable
**and** every shared library against the shared `gc.lib`, and ship `gc.dll` beside the executable.

What to ship with a Windows program that loads a tslang shared library, all in one folder:

- the program and its shared libraries,
- `gc.dll` (from `gcdll/` in the release package),
- `TypeScriptDefaultLib.dll` (from `defaultlib/dll/<release|debug>/gc`), which every shared library
built with the default library imports.

A shared library that was linked against the static `gc.lib` anyway — by an older `tslang`, or by
hand — is refused when you import it, under `--emit=exe` and `--emit=jit` alike:

```
error: shared library 'foo.dll' links its own garbage collector (the static gc.lib). Objects
crossing between it and this module can be freed while still in use. Rebuild it with tslang
--emit=dll, which links gc.dll.
```

The compiler reads this from the library itself, so it holds however the library was linked. The
JIT checks the default library's DLL the same way before loading it.

On Linux nothing needs to ship. A program that imports a tslang shared object exports its own
collector (`--whole-archive` plus `--export-dynamic-symbol=GC_*`), and the dynamic loader binds
the shared object's `GC_*` calls to it, so the copy linked into the shared object is never used.
The default library's `.so` links no collector at all. Under the JIT, `libTypeScriptRuntime.so`
exports the collector the same way. If you link a program by hand, pass those two options.

Statically linked programs are unaffected and keep the static `gc.lib` — one binary already
means one collector. `-mm=rc` and `-mm=none` are unaffected either way: neither has a collector.
Expand Down
2 changes: 1 addition & 1 deletion tag.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
git tag -a v0.0-pre-alpha81 -m "pre alpha v0.0-81"
git tag -a v0.0-pre-alpha82 -m "pre alpha v0.0-82"
git push origin --tags
4 changes: 2 additions & 2 deletions tag_del.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
git push --delete origin v0.0-pre-alpha81
git tag -d v0.0-pre-alpha81
git push --delete origin v0.0-pre-alpha82
git tag -d v0.0-pre-alpha82
128 changes: 124 additions & 4 deletions tslang/docs/single-gc-collector-design.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# One collector per process: design proposal

Status: **PR 1 implemented** (steps 1, 2, 5 - Windows) on branch `fix-single-gc-collector`;
steps 3, 4, 6 and Linux still open. See [Progress](#progress) at the end.
Status: **PR 1 merged** (steps 1, 2, 5 - Windows); **PR 2** on branch `gc-shared-lib-auto`: step 3,
step 4 (by fingerprint, not a marker), step 6, the CMake template, the debug default library, and
Linux. Open: suite tests that keep the default library. See [Progress](#progress) at the end.

## Problem

Expand Down Expand Up @@ -222,5 +223,124 @@ suppressed):
| JIT + default-lib DLL repro | 2000 / 2000 bad | 0 bad |
| exe + user DLL + default-lib DLL repro | 1984 / 2000 bad | 0 bad |

Still open: the tests that would include the default library itself (the suite still passes
`--no-default-lib`), steps 3, 4 and 6, the debug default-lib build, and all of Linux.
Merged as #309 (compiler) and TypeScriptCompilerDefaultLib #6.

### PR 2 - `tslang` chooses the collector's linkage (step 3, Windows)

- `CompileOptions::importsSharedLibrary`, set in `tslang.cpp` by walking the generated module for
`LoadLibraryPermanentlyOp` before the passes lower it. Every `import` that resolves to a DLL -
dynamic or `@static` - goes through `mlirGenImportSharedLib`, which emits that op.
- `exe.cpp`: under `-mm=gc` on Windows, `--emit=dll` or an importing `--emit=exe` links
`-L<shared>` instead of the static directory, and after a successful link copies `gc.dll` beside
the output (a warning names the file to ship if it cannot).
- The shared directory: `--gc-shared-lib-path`, else `GC_SHARED_LIB_PATH`, else
`<gc-lib-path>/gcdll` (the release package), else `--gc-lib-path` itself when `gc.dll` sits
beside it or in `../bin` (so the default library's `build_core.bat` keeps working). Nothing found
is an **error**, not a fallback to the static `gc.lib`.
- A lone program is unchanged: static `gc.lib`, no `gc.dll` copied.
- `test-compile-gc-shared-auto` (`shared-collector-auto.cmake`) drives `tslang --emit=dll/exe` itself -
`test-runner` links with lld directly and never exercised the compiler's choice. Five cases: the
library gets `gc.dll`; its importer runs clean; `--gc-lib-path` at a shared build alone is
enough; a lone exe runs with no `gc.dll` near it and gets none copied; a library with only a
static collector fails and names `gc.dll`.
- Teeth, with the previous compiler: a library and its importer built with only
`--gc-lib-path=<static>` link two collectors and fail the gc_single_collector assertion; with
collection suppressed the same binary prints 0 bad.
- The VS Code template's `--emit=dll` task passes `--gc-lib-path=<package root>`, so it resolves
`<package>/gcdll` without a change.

### PR 2, continued - step 4, step 6, template, debug default library, Linux

**Step 4 reads the library, not a marker.** The proposed `__tsgc_<static|shared>_...` marker would
be written at compile time, and the compile does not know how the binary gets linked: `test-runner`
builds its shared libraries with `--emit=obj` and links them with lld, the default library's DLL is
built with `--embed-declarations=false` (so it carries no markers at all), and libraries from before
the change have none. Instead `Dump::containsGarbageCollector` (ObjDumper.cpp) looks for the string
`GC_INITIAL_HEAP_SIZE` in the binary's data sections. Boehm's `GC_init` reads that environment
variable, so the name is present in every binary that contains the collector and in none that only
calls it. Checked against: stale debug `TypeScriptDefaultLib.dll` (static gc) and `gc.dll` - present;
release `gc`/`rc` default-lib DLLs and `TypeScriptRuntime.dll` (both flavours) - absent.

- `mlirGenImportSharedLib`: importing, under `-mm=gc` on Windows, a `gc` library that contains a
collector is an **error** (AOT and JIT). Because the fingerprint is evidence rather than a
missing marker, an old library gets the error too; it is broken, not merely old.
- `jit.cpp`: the same check on `TypeScriptDefaultLib.dll` before loading it - this is what a stale
default library looks like.
- Test: `test-compile-gc-shared-auto` case 6 builds a library against the static `gc.lib` (through a
stand-in shared directory) and expects both `--emit=exe` and `--emit=jit` importing it to fail
naming the collector.

**Linux, measured** (WSL Ubuntu, release package v0.0-pre-alpha81, the gc_single_collector pair):

| Case | Result | No-collection control |
| --- | --- | --- |
| exe + user `.so`, as linked today | assertion failed | 0 bad |
| JIT + user `.so` | 0 bad | 0 bad |
| exe linked with `--whole-archive libgc.a` + `--export-dynamic-symbol=GC_*` | 0 bad | 0 bad |

The user `.so` does link its own `libgc.a` copy. Under the JIT it is harmless because
`libTypeScriptRuntime.so` exports `GC_*` (482 symbols) and ELF symbol interposition sends the `.so`'s
calls there. A plain exe exports none, so the `.so` ran its own collector. The default library's
`.so` links no collector (`GC_*` undefined) and already binds to the host. So Linux needs no
`gc.so` and no fingerprint check: `exe.cpp` now links an importing `--emit=exe` with the whole
collector exported. Whole archive because the `.so` may call `GC_*` functions the program does not
(without it 478 of 486 were exported, and the repro happened to pass). Not exercised on Linux beyond
the hand link: no Linux build of this branch was available locally, so the Linux CI run is its test.

**Step 6.** The zip already ships `gc.dll` in its root and `gcdll/` (PR 1). `docs/memory-models.md`
now lists what to ship beside a program that loads a tslang DLL (`gc.dll`,
`TypeScriptDefaultLib.dll`), the step 4 error, and the Linux behaviour.

**CMake template** (`docs/how/cmake_tslang`): `TSLANG_SHARED_GC` (default OFF). On Windows it links
`gcdll/gc.lib` by full path (same file name as the static one in the package root) and copies
`gc.dll` beside the target; on Linux it adds the two export options. The template still builds no
shared libraries itself - those come from `tslang --emit=dll`, which chooses on its own.

**Debug default library.** `dll/debug/gc/TypeScriptDefaultLib.dll` was still the static-gc build from
before PR 1, and the local debug `TypeScriptRuntime.dll` predated it too. Rebuilt both: the debug
compiler (reconfigured, so it picks up `TSLANG_GC_SHARED_PREFIX`) and `scripts\build_vs.bat debug gc`,
which already linked `3rdParty/gcdll/x64/debug/lib`. Both now import `gc.dll`; no script change
needed. The release workflow builds the default library with release `GC_SHARED_LIB_PATH` for every
flavour; a debug DLL imports `gc.dll` by name and binds to the one already in the process.

### Tests with the default library in the process (step 5, completed)

`test-jit-gc-defaultlib-collector` and `test-compile-gc-defaultlib-collector`
(`test/tester/defaultlib-collector.cmake`, sources in `test/tester/defaultlib/`) drive `tslang` with
the real default library, no `--no-default-lib`:

- JIT: the program alone (strings built by `padStart`, churn by `repeat`, both in the default-lib
DLL), and the program importing a user shared library.
- AOT: the exe alone (static default library), and an exe importing a user DLL that links
`TypeScriptDefaultLib.dll`, with the library building the held strings and churning.

The sources are outside `tests/`: they do not compile without the default library, and the ownership
verifier compiles every file in `tests/` alone with `--no-default-lib`.

**CI.** The compiler suite runs before any default library exists, so with none found the tests print
`SKIPPED` (`SKIP_REGULAR_EXPRESSION`) instead of failing. The library is looked up in
`DEFAULT_LIB_PATH` first, then in the `TypeScriptCompilerDefaultLib/__build` sibling (local layout) or
child (CI workspace) of the repository. Both jobs of `create-release.yml` gained a step after
**Build Default Library** that runs `ctest -R gc-defaultlib-collector` with `DEFAULT_LIB_PATH` at the
library just built and `TSLANG_REQUIRE_DEFAULT_LIB=1`, which turns a missing library into a failure.
The Linux variant has not run anywhere yet.

**A gap the tests found.** Step 4 checks the library being imported, but a user DLL links
`TypeScriptDefaultLib.dll` without importing it by name. `exe.cpp` now refuses `--emit=dll` under `gc`
on Windows when the default-lib DLL it links contains a collector.

**Teeth** (release, a default library built by its own script with `GC_SHARED_LIB_PATH` at the static
`gc.lib`, so its DLL carries a collector):

| Case | Result |
| --- | --- |
| both tests against the rebuilt default library | pass, `bad: 0` in all four programs |
| both tests against the static-collector library | fail: `--emit=dll` refuses the default-lib DLL |
| exe + user DLL built correctly, stale DLL swapped in beside it at run time | assertion failed |
| the same, collection suppressed (`GC_INITIAL_HEAP_SIZE=1GB`) | `bad: 0` |

A machine whose `DEFAULT_LIB_PATH` names an installation with a default library from before PR 1 fails
these two tests - correctly: `--emit=dll` and JIT runs against that library now fail the same way.

Nothing from this design is open on Windows. On Linux, the `exe.cpp` export change and the
default-library tests have their first run in CI.
7 changes: 7 additions & 0 deletions tslang/include/TypeScript/DataStructs.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ struct CompileOptions
bool strictNullChecks;
bool enableFastMath;

// Whether the module imports a tslang shared library (`import './lib'` resolving to a DLL).
// Set once the module is generated, and read when linking: under `-mm=gc` such a program must
// take its collector from gc.dll, like the library does, because two statically linked
// collectors in one process each free what only the other's memory references.
// See docs/single-gc-collector-design.md.
bool importsSharedLibrary = false;

// Whether the Boehm runtime has to be present. Only `gc` needs it: it is the model whose
// reclamation *is* the collector. `rc` frees through the reference counts it maintains and
// `none` frees nothing, so both allocate straight from `malloc` and neither links libgc.
Expand Down
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/ObjDumper.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ static std::unique_ptr<Dumper> createDumperT(const ELFObjectFile<ELFT> &elfObjec
namespace Dump
{
void getSymbols(llvm::StringRef, SmallVector<StringRef> &, BumpPtrAllocator &);

// Whether the binary carries a Boehm collector of its own (linked the static gc library),
// rather than importing one from gc.dll / its host or having none. Read from the binary itself,
// so it answers for libraries linked by hand or by an older tslang as well.
bool containsGarbageCollector(llvm::StringRef);
}

std::unique_ptr<Dumper> createCOFFDumper(const COFFObjectFile &);
Expand Down
Loading
Loading