From 5606d68447c721fe3a9134106e8cb4de54b791af Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 20 Oct 2026 23:14:50 +0100 Subject: [PATCH] --jit-native: debug JIT runs in WinDbg and Visual Studio `--emit=jit --jit-native` (Windows) links the JIT cache's objects into one DLL with a PDB and loads it with the system loader instead of RTDyld, so a debugger sees the program as a module with symbols and source lines. The DLL and PDB live in __jit, are reused while the objects and options are the same, and earlier ones are removed. - exe.cpp: the link line is linkBinary (link.h), taking its objects, output, "shared" and extra arguments as parameters; buildExe calls it. A quiet link shows the linker's output only if it fails. - jitnative.cpp: the link, with a small object of host functions: init sends assert/abort reports to stderr (no dialogs) and flushes on SIGABRT; flush is called after the program, before TerminateProcess - the DLL's static C runtime owns the program's stdout buffer. - The DLL links the static default library: through its DLL's import library a program reading the library's data (Map reads PrimeHelpers.hashCollisionThreshold) does not link. - jit.cpp: one runner for both loaders; in this mode neither the default library's DLL nor TypeScriptRuntime.dll is loaded. - test-jit-native-dll (jit-native.cmake): reuse, relink after a change, exit code, uncaught exception, failing assert, no-cache error, and a cdb breakpoint where cdb is installed. See docs/jit-native-dll-debugging-design.md. Co-Authored-By: Claude Opus 5.5 --- .../docs/jit-native-dll-debugging-design.md | 174 +++++++++ tslang/test/tester/CMakeLists.txt | 28 ++ tslang/test/tester/jit-native.cmake | 131 +++++++ tslang/tslang/CMakeLists.txt | 1 + tslang/tslang/exe.cpp | 95 +++-- tslang/tslang/jit.cpp | 186 +++++++--- tslang/tslang/jitcache.cpp | 43 ++- tslang/tslang/jitcache.h | 25 ++ tslang/tslang/jitnative.cpp | 344 ++++++++++++++++++ tslang/tslang/link.h | 32 ++ tslang/tslang/tslang.cpp | 26 ++ 11 files changed, 1000 insertions(+), 85 deletions(-) create mode 100644 tslang/docs/jit-native-dll-debugging-design.md create mode 100644 tslang/test/tester/jit-native.cmake create mode 100644 tslang/tslang/jitnative.cpp create mode 100644 tslang/tslang/link.h diff --git a/tslang/docs/jit-native-dll-debugging-design.md b/tslang/docs/jit-native-dll-debugging-design.md new file mode 100644 index 000000000..d422c8df3 --- /dev/null +++ b/tslang/docs/jit-native-dll-debugging-design.md @@ -0,0 +1,174 @@ +# Debugging `--emit=jit` in WinDbg and Visual Studio: design proposal + +Status: **implemented** on branch `jit-native-dll` (2026-10-20) as `--emit=jit --jit-native`. The +approach was first proven by hand on a two-module program; see +[Feasibility](#feasibility-2026-10-20). Where the implementation differs from the plan below, see +[As built](#as-built). + +## Problem + +On Linux a JIT run can be debugged in gdb or lldb. On Windows, WinDbg and Visual Studio see +nothing of the program: no function names, no source lines, no locals. + +The JIT's only debugger hook is the GDB JIT interface (`createGDBRegistrationListener`, +jit.cpp `createJit`, and only without `--opt`). gdb and lldb read `__jit_debug_descriptor`. +WinDbg and Visual Studio do not. They learn about code from a module load event and a PDB, and +RTDyld code has neither: it sits in plain `VirtualAlloc` memory. + +The objects are no help either. On windows-msvc the module carries `!"CodeView", i32 1`, so the +objects hold CodeView. No debugger reads CodeView out of a JIT registration. `--lldb`, which +switches to DWARF, is applied only in `dumpLLVMIR` and never on the JIT path. + +Stack walking already works (`k` goes through JIT frames), because `JitSectionMemoryManager` +registers `.pdata` with `RtlAddFunctionTable`. What's missing is symbols and source. + +## Proposal + +When asked to, the JIT on Windows links the objects the JIT cache already keeps in `__jit` into +**one DLL with a PDB** and loads it with `LoadLibrary` instead of RTDyld. Then: + +- the debugger gets a real module load event and finds the PDB beside the DLL. Breakpoints by + name or by line, source stepping, locals and stacks work in WinDbg, cdb and Visual Studio; +- the loader registers `.pdata`, and `RtlPcToFileHeader` finds the image. That makes the + `_CxxThrowException` shim, the RVA-0 image-base workaround and the read-only-as-code mapping + unnecessary in this mode; +- the program runs exactly as it would linked AOT as `--emit=dll`. It uses the same link line + and the same runtime libraries. + +### One DLL, not one per unit + +All units go into a single image, the way AOT links a program and its imported modules into one +exe. A DLL per unit would need every cross-module **data** reference to go through `__imp_` +indirection: globals, vtables, RTTI, a class's `.size`. Codegen emits no `dllimport` for those. +One image resolves them at link time, as AOT does. + +### Trigger + +The first version uses an explicit option, e.g. `--jit-native` (name to be decided): +`--emit=jit` + Windows + the option → DLL mode. It implies `--di` unless `--opt` is given. + +Possible follow-up: switch on automatically when `IsDebuggerPresent()` and `--di`. That doesn't +cover attaching a debugger later, so the explicit option stays. + +## Work items + +1. **A reentrant link function.** `buildExe` (exe.cpp) runs `llvm::InitLLVM` and reads the + globals `emitAction` and `outputFilename`. Split the link-line builder out of it so it takes + the object list, the output path, "shared" and extra linker arguments as parameters. + `buildExe` keeps its behaviour and calls the new function. The JIT calls it mid-run without a + second `InitLLVM`. +2. **Exports.** `-Wl,/EXPORT:` (or a generated `.def`) for `__tslang_jit_main`, every unit's + `__tslang_jit_init_*`, `__mlir_gctors` when the program has it, and the hook from item 3. +3. **A host hook in the image.** Add one more function to the entry-thunk module, e.g. + `__tslang_jit_host_hook(int what)`. It is compiled into the DLL, so it reaches **the DLL's** + CRT: + - *flush*: `fflush(NULL)` before `runJitProgram`'s `TerminateProcess`. The DLL links the + static CRT, so it owns the program's stdout buffer. The existing flush reaches only + ucrtbase, and only under `_DEBUG`. Without this hook the output is **lost**. This was + measured: the hand-built DLL printed only when it was unloaded. + - *assert mode*: `_CrtSetReportMode` / `_set_error_mode`, so a failing `assert` in a debug-CRT + DLL doesn't open a modal box. This is the same hole PR #276 closed for AOT; in the JIT today + `_assert` is bound to `jitAssertFailed`. +4. **The run path.** In `runJitCached`: after `buildJitUnits`, collect the units' object paths + and link them. Then `LoadLibraryExW` with `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS`, plus `AddDllDirectory` for the default library's folder: + the DLL imports `TypeScriptDefaultLib.dll`, and under gc it imports `gc.dll` (the link step + already copies `gc.dll` beside the output). Then call the same sequence `runJitProgram` + calls (init functions in order, `__mlir_gctors`, the entry thunk) through `GetProcAddress`. + `runJitProgram` is factored so the two loaders share argv building, `callEntryThunk` and the + teardown. +5. **Caching the DLL.** Name it from a hash of the unit object hashes plus the link options, + e.g. `__jit/
..dll` with its `.pdb`. Reuse it when it already exists. Link to a + temporary name and rename, as `writeFileAtomically` does. A loaded DLL can't be replaced, + which is fine: a new hash is a new name. Link with `/INCREMENTAL:NO`, or link.exe writes a + 14 MB `.ilk` every time. +6. **What isn't loaded.** In DLL mode the image is self-contained, like `--emit=dll`: it links + `TypeScriptAsyncRuntime.lib` itself. So `loadJitSharedLibraries` skips + `TypeScriptRuntime.dll` and the default library. Otherwise the process runs two async + runtimes, and under gc possibly a second collector. `--shared-libs` still load: a program + resolves those at run time through `tslang_load_library_permanently`. +7. **Paths that can't do it.** `--jit-cache=false` and stdin (`-`) compile in memory and have no + objects on disk. Report an error ("--jit-native needs the JIT cache"). Don't write temporary + objects. Off Windows the option is ignored, with a warning: gdb and lldb already work there. +8. **Tests.** Two of them: + - a cmake `-P` script in the style of `jit-throw-without-runtime.cmake`, running + `tslang --emit=jit --jit-native` on a two-module program. It checks the output and its + order, the exit code, an uncaught throw, and a second run that reuses the DLL. test-runner + can't cover this: it always adds `--shared-libs=TypeScriptRuntime.dll` and never sees + tslang's own link line. + - a cdb check where cdb is installed: `bp !`, `kn`, `lsa .`. Skip otherwise. + +## Behaviour that changes in this mode (documented, not fixed) + +- **A linker is needed**: link.exe from Visual Studio, or lld-link. The in-memory JIT needs none. +- **Symbols resolve at link time.** A missing symbol is a link error, not a JIT materialization + error. Today `--shared-libs` also feed symbol resolution, through DLL export tables; in this + mode the AOT rules apply. +- **The CRT follows `--opt`, not the tslang build.** With `--opt` it links `libcmt`, without it + `libcmtd`, as `buildExe` does today. A release tslang with `--di` gets the debug CRT. +- **Size and time.** Measured on the probe: a 0.9 MB DLL and 5.5 MB PDB, linked in 138 ms. A + rebuilt unit costs one link; an unchanged run costs none. + +## Feasibility (2026-10-20) + +Done by hand with the 2026-10-20 release build (`windows-msbuild-2026-release`) under `-mm=rc`. +`m.ts` exports a class with a method, an exported `let`, and has a top-level `print`. `p.ts` +imports it, throws and catches a string, and prints. + +1. `tslang --emit=jit --di` put `m.ts..obj` (init `__tslang_jit_init_65a6b283138c1d5b`) + and `p.ts..obj` in `__jit`. +2. The `--emit=dll --verbose` link line (link.exe), with those two objects instead of its own, + plus `-incremental:no -export:__tslang_jit_main -export:__tslang_jit_init_…`, **linked + cleanly in 138 ms**. The JIT-flavoured main object links as a DLL, and the weakened COMDAT + duplicates cause no conflicts. +3. A host process that called `LoadLibraryW` → init → `__tslang_jit_main(1, argv)` printed + `m init`, `caught`, `42` with exit code 0. The C++ exception was caught with no shim. The + output appeared only when the DLL was unloaded (see item 3). +4. Under cdb, `bp prog!Counter.inc` stopped in the **imported** module. `kn` showed + `prog!Counter.inc [m.ts @ 3]` ← `prog!main [p.ts @ 4]` ← `prog!_tslang_jit_main`, and `lsa .` + listed `m.ts` with the current line marked. + +Locals showed as `` at the method's first instruction. That is debug-info +quality for TS code, which AOT shares, not something this mode adds. + +## As built + +- **The link** is `linkBinary` (exe.cpp, declared in link.h): the link line `buildExe` built, + taking its objects, output, "shared", extra arguments and a quiet flag as parameters. `buildExe` + keeps `InitLLVM` and the command-line globals and calls it. With `quiet`, the linker's output + goes to a temporary file and is shown only if the link fails. +- **The host functions** (`__tslang_jit_native_init`, `__tslang_jit_native_flush`) are in an object + of their own, generated at link time (jitnative.cpp), not in the entry-thunk module. The cached + objects are also the ones the in-memory JIT loads, and a call to the debug C runtime's + `_CrtSetReportMode` would leave the in-memory JIT an unresolved symbol. Init calls + `_set_error_mode(_OUT_TO_STDERR)` and `_set_abort_behavior(0, _WRITE_ABORT_MSG | + _CALL_REPORTFAULT)`, sets a SIGABRT handler that flushes (a failing assert ends in `abort()`, + which flushes nothing), and with the debug C runtime sends `_CRT_ASSERT` and `_CRT_ERROR` + reports to stderr. +- **The static default library**, not the DLL's import library (`LinkRequest::staticDefaultLib`). + With the import library the link fails as soon as the program reads data the default library + defines. A `Map` reads `PrimeHelpers.hashCollisionThreshold` by its own name, and only an + `__imp_` reference resolves a DLL's data export. The in-memory JIT binds it from the export + table. `--emit=dll` has the same failure today (a separate bug). So in this mode tslang loads + neither the default library's DLL nor `TypeScriptRuntime.dll`. +- **The DLL and its PDB** are `__jit/..dll` and `.pdb`, linked under a temporary name, + `/PDB:` the final name with `/PDBALTPATH:%_PDB%`, then renamed. `.lib` and `.exp` are removed. + The DLLs of earlier runs of the same program are removed, and so are temporary files more than an + hour old, which a run killed mid-link leaves behind. +- **In WinDbg** the module is named after the file with its dots made underscores: + `p_ts_`. Break by function (`bm *!Counter.inc`, `bu !name`) or by source line + (`` bu `m.ts:6` ``). A source line that maps to more than one address, such as a module's line 1 + when its initializer is there too, is an "ambiguous symbol" error. +- **Assert output** is the C runtime's (`Assertion failed: , file , line `), not + the in-memory JIT's `:: assertion failed: `. The exit code (3) is the same. +- **Test**: `test-jit-native-dll` (test/tester/jit-native.cmake), plus the cdb step where cdb is + installed. It covers rc only. gc needs gc.dll's import library (`GC_SHARED_LIB_PATH`) and a + runtime library built with the C runtime the configuration links. It works by hand with + `--opt`, and with `--di` against a debug runtime library. + +## Estimate + +About **3–4 days**: item 1 is a careful refactor of a 600-line function, items 2–7 are plumbing +in jit.cpp and jitcache.cpp, and item 8 is the cmake test plus a cdb probe. The risk is in +item 1 (the link line has many target branches) and item 6 (making sure nothing else loads the +runtime DLL). diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 83825b714..a687881c2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3906,6 +3906,34 @@ add_test(NAME test-jit-cache-embedded-types "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/jit-cache-embedded-types" "-DEXPECTED=done." -P "${CMAKE_CURRENT_SOURCE_DIR}/jit-cache-empty.cmake") +# --jit-native: the cache's objects linked into a DLL with a PDB and loaded by the system loader, +# for WinDbg and Visual Studio - see jit-native.cmake. Windows only. With debug info under either +# configuration, and the C runtime of the configuration's own libraries. cdb, if it is installed, +# checks that a breakpoint on a source line is hit. +if (WIN32) + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") + set(jit_native_opt "--opt;--di") + else() + set(jit_native_opt "--di;--opt_level=0") + endif() + find_program(TSLANG_TEST_CDB NAMES cdb cdbX64 + PATHS "C:/Program Files (x86)/Windows Kits/10/Debuggers/x64" "$ENV{LOCALAPPDATA}/Microsoft/WindowsApps") + if (TSLANG_TEST_CDB) + set(jit_native_cdb "-DCDB=${TSLANG_TEST_CDB}") + else() + set(jit_native_cdb "") + endif() + add_test(NAME test-jit-native-dll + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DSOURCE_DIR=${jit_cache_dir}" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/jit-native" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + "-DOPT=${jit_native_opt}" + ${jit_native_cdb} + -P "${CMAKE_CURRENT_SOURCE_DIR}/jit-native.cmake") +endif() # `import { a as b }` and `import * as M` - each used to be "can't resolve name". set(import_bindings_dir "${PROJECT_SOURCE_DIR}/test/tester/import-bindings") diff --git a/tslang/test/tester/jit-native.cmake b/tslang/test/tester/jit-native.cmake new file mode 100644 index 000000000..37eab813e --- /dev/null +++ b/tslang/test/tester/jit-native.cmake @@ -0,0 +1,131 @@ +# --jit-native (Windows): `tslang --emit=jit` links the JIT cache's objects into one DLL with its PDB +# and loads it with the system loader, so a debugger sees the program as a module with symbols and +# source. See docs/jit-native-dll-debugging-design.md. +# +# 1. A program importing a module runs, and leaves one DLL and its PDB in __jit. +# 2. The second run uses that DLL: it is not linked again. +# 3. The imported module changes: the program runs the new code, from a new DLL, and the old one is +# removed. +# 4. The program's exit code, an uncaught exception and a failing assert come out as they do from +# the in-memory JIT: the DLL has a C runtime of its own, whose output tslang has to flush and +# whose assert would otherwise put up a dialog. +# 5. Without the JIT cache there is nothing to link: an error, not a run. +# 6. If cdb is there (CDB), a breakpoint in the imported module is hit, at its source line. +# +# This drives `tslang` itself, which `test-runner` does not: it links nothing for the JIT. -mm=rc and +# --no-default-lib keep the link to the compiler's own libraries. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG SOURCE_DIR WORK_DIR LLVM_LIB TSLANG_LIB OPT) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +file(COPY "${SOURCE_DIR}/greeting.ts" "${SOURCE_DIR}/greeting_module.ts" DESTINATION "${WORK_DIR}") +file(WRITE "${WORK_DIR}/exit_code.ts" "function main() {\n print(\"before\");\n return 7;\n}\n") +file(WRITE "${WORK_DIR}/uncaught.ts" "function main() {\n print(\"before\");\n throw \"boom\";\n}\n") +file(WRITE "${WORK_DIR}/assert_fails.ts" "function main() {\n print(\"before\");\n assert(1 == 2, \"one is not two\");\n print(\"after\");\n}\n") + +set(tslang_args --emit=jit --jit-native -mm=rc --no-default-lib ${OPT} + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}") + +# run( ) - fails the test unless the program ends with that +# exit code, prints exactly that, and its diagnostics match (an empty regex matches anything) +function(run source expected_status expected_out expected_err) + execute_process(COMMAND "${TSLANG}" ${tslang_args} ${source} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + string(STRIP "${out}" out) + if(NOT status EQUAL expected_status OR NOT out STREQUAL expected_out OR NOT err MATCHES "${expected_err}") + message(FATAL_ERROR "${source}: expected exit ${expected_status} and '${expected_out}', got exit ${status}\n" + "stdout:\n${out}\nstderr:\n${err}") + endif() +endfunction() + +# the program's DLLs in the cache, and when each was written; each must have its PDB +function(native_dlls result source) + file(GLOB dlls "${WORK_DIR}/__jit/${source}.*.dll") + list(SORT dlls) + set(stamps "") + foreach(dll ${dlls}) + string(REGEX REPLACE "\\.dll$" ".pdb" pdb "${dll}") + if(NOT EXISTS "${pdb}") + message(FATAL_ERROR "${dll} has no PDB beside it") + endif() + file(TIMESTAMP "${dll}" stamp "%Y%m%d%H%M%S") + list(APPEND stamps "${dll}@${stamp}") + endforeach() + set(${result} "${stamps}" PARENT_SCOPE) +endfunction() + +run(greeting.ts 0 "one" "") +native_dlls(first greeting.ts) +list(LENGTH first count) +if(NOT count EQUAL 1) + message(FATAL_ERROR "expected one DLL for greeting.ts in __jit, found: ${first}") +endif() + +# a second apart, so a DLL linked again has another time +execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep 1.1) + +run(greeting.ts 0 "one" "") +native_dlls(second greeting.ts) +if(NOT first STREQUAL second) + message(FATAL_ERROR "the second run linked again instead of using the DLL:\n${first}\n${second}") +endif() + +file(WRITE "${WORK_DIR}/greeting_module.ts" "export function greeting() {\n return \"two\";\n}\n") + +run(greeting.ts 0 "two" "") +native_dlls(third greeting.ts) +list(LENGTH third count) +if(NOT count EQUAL 1 OR third STREQUAL second) + message(FATAL_ERROR "expected the old DLL replaced by a new one after greeting_module.ts changed:\n${second}\n${third}") +endif() + +run(exit_code.ts 7 "before" "") +run(uncaught.ts 1 "before" "uncaught exception") +run(assert_fails.ts 3 "before" "Assertion failed: one is not two") + +execute_process(COMMAND "${TSLANG}" ${tslang_args} --jit-cache=false greeting.ts + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(status EQUAL 0 OR NOT err MATCHES "--jit-native links the objects of the JIT cache") + message(FATAL_ERROR "--jit-native --jit-cache=false: expected an error, got exit ${status}\n${out}\n${err}") +endif() + +if(DEFINED CDB AND EXISTS "${CDB}") + # The DLL linked above, by name: the debugger stops when it loads, loads its PDB, and then stops + # in the imported module's `greeting`, whose frame the PDB places on line 1 of + # greeting_module.ts. A breakpoint on the line itself would be ambiguous - the module's + # initializer is on line 1 too - and optimized, `greeting` has no code on any other. dbgeng + # names the module after the file with its dots made underscores. Symbols only from __jit, so + # nothing is fetched from a symbol server. `q` ends tslang too. + list(GET third 0 dll) + string(REGEX REPLACE "@.*$" "" dll "${dll}") + get_filename_component(dll_name "${dll}" NAME) + string(REGEX REPLACE "\\.dll$" "" module "${dll_name}") + string(REPLACE "." "_" module "${module}") + execute_process(COMMAND "${CDB}" -y "${WORK_DIR}/__jit" + -c "sxe ld:${dll_name}; g; .lines -e; ld ${module}; bu ${module}!greeting; g; kn 3; q" + "${TSLANG}" ${tslang_args} greeting.ts + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status + TIMEOUT 120) + if(NOT out MATCHES "Breakpoint 0 hit" OR NOT out MATCHES "${module}!greeting [^\n]*greeting_module\\.ts @ 1") + message(FATAL_ERROR "cdb did not stop in ${module}!greeting at greeting_module.ts line 1:\n${out}\n${err}") + endif() + message(STATUS "cdb stops in a function of the imported module, at its source line") +endif() + +message(STATUS "--jit-native links the JIT cache into a DLL with a PDB, reuses it, and runs the program as the JIT does") diff --git a/tslang/tslang/CMakeLists.txt b/tslang/tslang/CMakeLists.txt index ca7542df1..65c4e5580 100644 --- a/tslang/tslang/CMakeLists.txt +++ b/tslang/tslang/CMakeLists.txt @@ -59,6 +59,7 @@ add_llvm_executable(tslang dump.cpp jit.cpp jitcache.cpp + jitnative.cpp obj.cpp exe.cpp TextDiagnostic.cpp diff --git a/tslang/tslang/exe.cpp b/tslang/tslang/exe.cpp index 4b20b6558..3e5166927 100644 --- a/tslang/tslang/exe.cpp +++ b/tslang/tslang/exe.cpp @@ -27,6 +27,8 @@ #include "TypeScript/TypeScriptCompiler/Defines.h" #include "TypeScript/Defines.h" +#include "link.h" + namespace cl = llvm::cl; extern cl::opt emitAction; @@ -643,8 +645,32 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio { // Initialize variables to call the driver llvm::InitLLVM x(argc, argv); - //llvm::SmallVector args(argv, argv + argc); - llvm::SmallVector args(argv, argv + 1); + + if (outputFilename.empty()) + { + outputFilename = getDefaultOutputFileName(emitAction); + } + + LinkRequest request; + request.objects.push_back(objFileName); + for (auto &obj : objs) + { + request.objects.push_back(obj); + } + + if (!additionalObjFileName.empty()) + { + request.objects.push_back(additionalObjFileName); + } + + request.output = outputFilename; + request.shared = emitAction == BuildDll; + return linkBinary(argv[0], request, compileOptions); +} + +int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &compileOptions) +{ + llvm::SmallVector args{argv0}; clang::driver::ParsedClangName targetAndMode("tslang", "--driver-mode=tslang"); std::string driverPath = getExecutablePath(args[0]); @@ -702,7 +728,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio auto win = os == llvm::Triple::Win32; auto wasm = arch == llvm::Triple::wasm32 || arch == llvm::Triple::wasm64; auto emscripten = os == llvm::Triple::Emscripten; - auto shared = emitAction == BuildDll; + auto shared = request.shared; // Windows x86 and x64 link libraries whose machine tslang checks; see resolveWindowsLibPath. auto winX86OrX64 = win && (arch == llvm::Triple::x86 || arch == llvm::Triple::x86_64); // the same debug/release choice as the CRT below @@ -750,29 +776,12 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio } } - args.push_back(objFileName.c_str()); - llvm::SmallVector objOpts; - for (auto obj : objs) - { - objOpts.push_back(obj); - } - - for (auto &obj : objOpts) + for (auto &obj : request.objects) { args.push_back(obj.c_str()); } - if (!additionalObjFileName.empty()) - { - args.push_back(additionalObjFileName.c_str()); - } - - if (outputFilename.empty()) - { - outputFilename = getDefaultOutputFileName(emitAction); - } - - std::string resultFile = "-o" + outputFilename; + std::string resultFile = "-o" + request.output; args.push_back(resultFile.c_str()); if (shared) { @@ -823,7 +832,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // Android: a shared library links the static default library too, so the app ships one // self-contained .so. Its shared default library leaves GC_* to the process, and an app's // process (the Java VM) has no collector to give it. - auto sharedDefaultLib = shared && !android; + auto sharedDefaultLib = shared && !android && !request.staticDefaultLib; auto defaultLibDir = llvm::sys::path::parent_path(getDefaultLibBinary(sharedDefaultLib, compileOptions, TheTriple)).str(); @@ -859,7 +868,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // DLL predates linking gc.dll it brings a second collector, which frees what the library // and its host hold - the case step 4's import check cannot see, since the library does // not import it by name. See docs/single-gc-collector-design.md. - if (win && shared && compileOptions.needsGCRuntime() && !defaultLibDir.empty()) + if (win && sharedDefaultLib && compileOptions.needsGCRuntime() && !defaultLibDir.empty()) { llvm::SmallString<256> defaultLibDll(defaultLibDir); llvm::sys::path::append(defaultLibDll, DEFAULT_LIB_NAME ".dll"); @@ -1009,7 +1018,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio if (auto error = writeAsInvokerManifestObj(llvm::COFF::IMAGE_FILE_MACHINE_I386, manifestObjFileName)) { llvm::WithColor::error(llvm::errs(), "tslang") - << "could not write the manifest object for " << outputFilename.getValue() << ": " + << "could not write the manifest object for " << request.output << ": " << llvm::toString(std::move(error)) << "\n"; return 1; } @@ -1149,6 +1158,11 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio args.push_back("-g"); } + for (auto &extraArg : request.extraArgs) + { + args.push_back(extraArg.c_str()); + } + // Create DiagnosticsEngine for the compiler driver auto diagOpts = createAndPopulateDiagOpts(args); llvm::IntrusiveRefCntPtr diagID(new clang::DiagnosticIDs()); @@ -1186,11 +1200,36 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio if (verbose.getValue()) c->getJobs().Print(llvm::errs(), "\n", /*Quote=*/false); + // A quiet link writes the linker's output to a file, shown only when the link fails: link.exe + // says "Creating library ..." on stdout, which would land in the middle of a program's output. + llvm::SmallString<256> linkerOutputFile; + llvm::FileRemover linkerOutputRemover; + if (request.quiet) + { + if (auto error = llvm::sys::fs::createTemporaryFile("tslang-link", "txt", linkerOutputFile)) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "could not create a file for the linker's output: " << error.message() << "\n"; + return 1; + } + + linkerOutputRemover.setFile(linkerOutputFile); + std::optional redirects[] = {std::nullopt, llvm::StringRef(linkerOutputFile), llvm::StringRef(linkerOutputFile)}; + c->Redirect(redirects); + } + // Run the driver int res = 1; bool isCrash = false; res = theDriver.ExecuteCompilation(*c, failingCommands); + if (request.quiet && (res != 0 || !failingCommands.empty())) + { + if (auto linkerOutput = llvm::MemoryBuffer::getFile(linkerOutputFile, /*IsText=*/true)) + { + llvm::errs() << (*linkerOutput)->getBuffer(); + } + } + for (const auto &p : failingCommands) { int commandRes = p.first; @@ -1218,7 +1257,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // A binary linked against gc.dll does not start without it, so put it beside the output. if (res == 0 && useSharedGC) { - auto outputDir = llvm::sys::path::parent_path(outputFilename.getValue()); + auto outputDir = llvm::sys::path::parent_path(request.output); llvm::SmallString<256> destination(outputDir.empty() ? "." : outputDir); llvm::sys::path::append(destination, "gc.dll"); @@ -1227,7 +1266,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio { llvm::WithColor::warning(llvm::errs(), "tslang") << "linked against gc.dll, but no gc.dll was found next to '" << gcSharedLibPath - << "' or in its '../bin'; ship gc.dll beside '" << outputFilename.getValue() << "'\n"; + << "' or in its '../bin'; ship gc.dll beside '" << request.output << "'\n"; } else if (llvm::sys::fs::equivalent(gcDllPath, destination, sameFile) || !sameFile) { @@ -1235,7 +1274,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio { llvm::WithColor::warning(llvm::errs(), "tslang") << "could not copy '" << gcDllPath << "' to '" << destination << "': " << error.message() - << "; ship gc.dll beside '" << outputFilename.getValue() << "'\n"; + << "; ship gc.dll beside '" << request.output << "'\n"; } } } diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 3ca14a093..4ecb0eb6e 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -14,6 +14,7 @@ #include "llvm/IR/IRBuilder.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IR/Module.h" +#include "llvm/Support/ConvertUTF.h" #include "llvm/Support/DynamicLibrary.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Memory.h" @@ -62,6 +63,7 @@ extern cl::opt objectFilename; extern cl::opt mainFuncName; extern cl::opt inputFilename; extern cl::list programArgs; +extern cl::opt jitNative; // obj extern cl::opt TargetTriple; @@ -449,7 +451,6 @@ static void *jitTypeInfoVftable() } #endif -#define JIT_ENTRY_THUNK_NAME "__tslang_jit_main" using JitEntryThunkFn = int (*)(int, char **); // Calls the program's entry point; false when an exception nothing in the program catches came @@ -584,13 +585,15 @@ static bool checkInProcessJit(CompileOptions &compileOptions) } // The libraries the program runs against - the default library, the GC runtime - added to -// --shared-libs, and on Windows the CRT entry points bound to this process's own CRT. -static int prepareJitProcess(CompileOptions &compileOptions) +// --shared-libs, and on Windows the CRT entry points bound to this process's own CRT. A +// `nativeImage` (--jit-native) links both in itself, the static default library and the async +// runtime, so neither DLL is loaded: they would be a second copy of each in the process. +static int prepareJitProcess(CompileOptions &compileOptions, bool nativeImage) { // If shared library implements custom mlir-runner library init and destroy // functions, we'll use them to register the library with the execution // engine. Otherwise we'll pass library directly to the execution engine. - if (!compileOptions.noDefaultLib) + if (!compileOptions.noDefaultLib && !nativeImage) { // per-build subfolder (debug/release) must match the JIT compilation mode. // Keyed on --di (generate debug info): with debug info use the debug lib. @@ -648,7 +651,7 @@ static int prepareJitProcess(CompileOptions &compileOptions) }); std::string pathTypeScriptLib("../lib/" LIB_NAME "TypeScriptRuntime." LIB_EXT); - if (compileOptions.needsGCRuntime() && !hasTypeScriptRuntime) + if (compileOptions.needsGCRuntime() && !hasTypeScriptRuntime && !nativeImage) { auto absPath3 = makeAbsolutePath(mergeWithDefaultLibPath(getTslangLibPath(), LIB_NAME "TypeScriptRuntime." LIB_EXT)); if (absPath3.empty()) @@ -922,34 +925,24 @@ static std::unique_ptr createJit(llvm::orc::JITTargetMachineBu return jit; } -// Runs what was added to the JIT: the platform initializers, the objects' own initializers -// (`initFunctions`, in the order given - an imported module's before its importer's), the -// program's `__mlir_gctors` if it has one, then the entry point. -static int runJitProgram(llvm::orc::LLJIT &jit, llvm::ArrayRef initFunctions, bool hasGCtorsMethod) -{ - // run platform initializers (llvm.global_ctors etc.) - if (auto err = jit.initialize(jit.getMainJITDylib())) - { - llvm::WithColor::error(llvm::errs(), "tslang") << "JIT initialization failed, error: " << err << "\n"; - llvm::consumeError(std::move(err)); - return -1; - } +// The address of a symbol of the program, or null once it has said why there is none. +using ProgramSymbolLookup = llvm::function_ref; +// Runs the program: the objects' own initializers (`initFunctions`, in the order given - an +// imported module's before its importer's), the program's `__mlir_gctors` if it has one, then the +// entry point. `flushProgram`, if given, flushes a C runtime the program has to itself +// (--jit-native), before anything after the program writes. +static int runProgram(ProgramSymbolLookup lookup, llvm::ArrayRef initFunctions, bool hasGCtorsMethod, + void (*flushProgram)()) +{ auto invoke = [&](llvm::StringRef name) { - auto sym = jit.lookup(name); - if (!sym) + auto *address = lookup(name); + if (!address) { - // Streaming an Error only logs it - the payload survives, and ~Error then trips - // fatalUncheckedError, turning a plain "no such symbol" into an abort with a crash - // backtrace wherever LLVM_ENABLE_ABI_BREAKING_CHECKS is on (i.e. debug builds). - // consumeError takes the payload; the message itself is unchanged. - auto err = sym.takeError(); - llvm::WithColor::error(llvm::errs(), "tslang") << "JIT invocation failed, error: " << err << "\n"; - llvm::consumeError(std::move(err)); return -1; } - sym->toPtr()(); + reinterpret_cast(address)(); return 0; }; @@ -966,12 +959,9 @@ static int runJitProgram(llvm::orc::LLJIT &jit, llvm::ArrayRef init return -1; } - auto entryThunk = jit.lookup(JIT_ENTRY_THUNK_NAME); + auto *entryThunk = lookup(JIT_ENTRY_THUNK_NAME); if (!entryThunk) { - auto err = entryThunk.takeError(); - llvm::WithColor::error(llvm::errs(), "tslang") << "JIT invocation failed, error: " << err << "\n"; - llvm::consumeError(std::move(err)); return -1; } @@ -987,7 +977,13 @@ static int runJitProgram(llvm::orc::LLJIT &jit, llvm::ArrayRef init programArgv.push_back(nullptr); auto exitCode = 0; - if (!callEntryThunk(entryThunk->toPtr(), programArgc, programArgv.data(), exitCode)) + auto finished = callEntryThunk(reinterpret_cast(entryThunk), programArgc, programArgv.data(), exitCode); + if (flushProgram) + { + flushProgram(); + } + + if (!finished) { fflush(stdout); llvm::WithColor::error(llvm::errs(), "tslang") << "uncaught exception\n"; @@ -1028,6 +1024,85 @@ static int runJitProgram(llvm::orc::LLJIT &jit, llvm::ArrayRef init return exitCode; } +// Runs what was added to the JIT: the platform initializers, then the program (runProgram). +static int runJitProgram(llvm::orc::LLJIT &jit, llvm::ArrayRef initFunctions, bool hasGCtorsMethod) +{ + // run platform initializers (llvm.global_ctors etc.) + if (auto err = jit.initialize(jit.getMainJITDylib())) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "JIT initialization failed, error: " << err << "\n"; + llvm::consumeError(std::move(err)); + return -1; + } + + auto lookup = [&](llvm::StringRef name) -> void * { + auto sym = jit.lookup(name); + if (!sym) + { + // Streaming an Error only logs it - the payload survives, and ~Error then trips + // fatalUncheckedError, turning a plain "no such symbol" into an abort with a crash + // backtrace wherever LLVM_ENABLE_ABI_BREAKING_CHECKS is on (i.e. debug builds). + // consumeError takes the payload; the message itself is unchanged. + auto err = sym.takeError(); + llvm::WithColor::error(llvm::errs(), "tslang") << "JIT invocation failed, error: " << err << "\n"; + llvm::consumeError(std::move(err)); + return nullptr; + } + + return sym->toPtr(); + }; + + return runProgram(lookup, initFunctions, hasGCtorsMethod, nullptr); +} + +#ifdef _WIN32 +// --jit-native: the DLL linkJitUnits linked, loaded by the system loader - which registers its +// unwind info and tells a debugger about it, PDB and all - then the program run as runJitProgram +// runs it. The DLL's own dependencies are found beside it (gc.dll, which the link copies there) or +// are loaded already (--shared-libs, by loadJitSharedLibraries): a module of that name already +// loaded is the one the loader binds to. +static int runNativeProgram(const std::string &dllPath, llvm::ArrayRef initFunctions, bool hasGCtorsMethod) +{ + // a full path, as LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR asks for: the cache folder is made absolute + llvm::SmallVector widePath; + if (!llvm::convertUTF8ToUTF16String(dllPath, widePath)) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: '" << dllPath << "' is not a path Windows can load\n"; + return -1; + } + + widePath.push_back(0); + auto module = LoadLibraryExW(reinterpret_cast(widePath.data()), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!module) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--jit-native: could not load '" << dllPath << "' (Windows error " << GetLastError() << ")\n"; + return -1; + } + + auto lookup = [&](llvm::StringRef name) -> void * { + auto *address = reinterpret_cast(GetProcAddress(module, name.str().c_str())); + if (!address) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: '" << dllPath << "' does not export '" << name << "'\n"; + } + + return address; + }; + + auto *init = lookup(JIT_NATIVE_INIT_NAME); + auto *flush = lookup(JIT_NATIVE_FLUSH_NAME); + if (!init || !flush) + { + return -1; + } + + reinterpret_cast(init)(); + return runProgram(lookup, initFunctions, hasGCtorsMethod, reinterpret_cast(flush)); +} +#endif + int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compileOptions) { if (!checkInProcessJit(compileOptions)) @@ -1054,7 +1129,7 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile auto optPipeline = getTransformer(enableOpt, optLevel, sizeLevel, compileOptions); - if (auto result = prepareJitProcess(compileOptions)) + if (auto result = prepareJitProcess(compileOptions, /*nativeImage=*/false)) { return result; } @@ -1197,7 +1272,7 @@ int runJitCached(int argc, char **argv, CompileOptions &compileOptions) llvm::InitializeNativeTarget(); llvm::InitializeNativeTargetAsmPrinter(); - if (auto result = prepareJitProcess(compileOptions)) + if (auto result = prepareJitProcess(compileOptions, /*nativeImage=*/jitNative)) { return result; } @@ -1223,14 +1298,46 @@ int runJitCached(int argc, char **argv, CompileOptions &compileOptions) return result; } + std::vector initFunctions; + auto hasGCtorsMethod = false; + for (auto &unit : units) + { + if (!unit.initFunction.empty()) + { + initFunctions.push_back(unit.initFunction); + } + + hasGCtorsMethod |= unit.isMain && unit.hasGCtorsMethod; + } + +#ifdef _WIN32 + if (jitNative) + { + auto tmOrError = tmBuilderOrError->createTargetMachine(); + if (!tmOrError) + { + auto err = tmOrError.takeError(); + llvm::WithColor::error(llvm::errs(), "tslang") << "failed to create a TargetMachine for the host, error: " << err << "\n"; + llvm::consumeError(std::move(err)); + return -1; + } + + std::string dllPath; + if (auto result = linkJitUnits(argv[0], units, compileOptions, **tmOrError, dllPath)) + { + return result; + } + + return runNativeProgram(dllPath, initFunctions, hasGCtorsMethod); + } +#endif + auto jit = createJit(std::move(*tmBuilderOrError), needsGCEnableThreadsStandIn); if (!jit) { return -1; } - std::vector initFunctions; - auto hasGCtorsMethod = false; for (auto &unit : units) { if (auto err = jit->addObjectFile(std::move(unit.object))) @@ -1239,13 +1346,6 @@ int runJitCached(int argc, char **argv, CompileOptions &compileOptions) llvm::consumeError(std::move(err)); return -1; } - - if (!unit.initFunction.empty()) - { - initFunctions.push_back(unit.initFunction); - } - - hasGCtorsMethod |= unit.isMain && unit.hasGCtorsMethod; } return runJitProgram(*jit, initFunctions, hasGCtorsMethod); diff --git a/tslang/tslang/jitcache.cpp b/tslang/tslang/jitcache.cpp index 849f096d6..8e21c7ab6 100644 --- a/tslang/tslang/jitcache.cpp +++ b/tslang/tslang/jitcache.cpp @@ -285,16 +285,8 @@ class JitUnitBuilder public: JitUnitBuilder(const char *argv0, const CompileOptions &compileOptions, llvm::orc::JITTargetMachineBuilder &tmBuilder, std::vector &units) - : compileOptions(compileOptions), tmBuilder(tmBuilder), units(units) + : compileOptions(compileOptions), tmBuilder(tmBuilder), units(units), compilerId(jitCompilerId(argv0)) { - // a rebuilt compiler compiles differently: its objects are not these - auto executable = llvm::sys::fs::getMainExecutable(argv0, reinterpret_cast(&canonicalPath)); - llvm::sys::fs::file_status status; - if (!executable.empty() && !llvm::sys::fs::status(executable, status)) - { - compilerId = llvm::utohexstr(status.getSize(), true) + "-" + - llvm::utohexstr(status.getLastModificationTime().time_since_epoch().count(), true); - } } int add(llvm::StringRef sourcePath, bool isMain) @@ -327,16 +319,23 @@ class JitUnitBuilder auto [objectPath, manifestPath] = cachePaths(path, isMain); Manifest manifest; - if (!loadFromCache(objectPath, manifestPath, manifest, unit)) + auto onDisk = loadFromCache(objectPath, manifestPath, manifest, unit); + if (!onDisk) { if (auto result = compile(path, isMain, manifest, unit)) { return result; } - storeInCache(objectPath, manifestPath, manifest, *unit.object); + onDisk = storeInCache(objectPath, manifestPath, manifest, *unit.object); + } + + if (onDisk) + { + unit.objectPath = objectPath; } + unit.objectHash = manifest.objectHash; unit.initFunction = manifest.initFunction; unit.hasGCtorsMethod = manifest.hasGCtorsMethod; @@ -462,14 +461,15 @@ class JitUnitBuilder return true; } - void storeInCache(llvm::StringRef objectPath, llvm::StringRef manifestPath, const Manifest &manifest, + // true if the object is now in the cache folder + bool storeInCache(llvm::StringRef objectPath, llvm::StringRef manifestPath, const Manifest &manifest, const llvm::MemoryBuffer &object) { // Without a compiler to tell one build from another, the object could outlive the build it // belongs to: better compiled again every time. if (compilerId.empty()) { - return; + return false; } // Nowhere to write it (a read-only folder, say) is not an error: the program still runs, @@ -478,10 +478,11 @@ class JitUnitBuilder !writeFileAtomically(objectPath, object.getBuffer()) || !writeFileAtomically(manifestPath, manifest.str())) { LLVM_DEBUG(llvm::dbgs() << "JIT cache: could not write " << objectPath << "\n";); - return; + return false; } LLVM_DEBUG(llvm::dbgs() << "JIT cache: wrote " << objectPath << "\n";); + return true; } int compile(llvm::StringRef path, bool isMain, Manifest &manifest, JitUnit &unit) @@ -641,6 +642,20 @@ class JitUnitBuilder } // namespace +std::string jitCompilerId(const char *argv0) +{ + // a rebuilt compiler compiles differently: its objects are not these + auto executable = llvm::sys::fs::getMainExecutable(argv0, reinterpret_cast(&canonicalPath)); + llvm::sys::fs::file_status status; + if (executable.empty() || llvm::sys::fs::status(executable, status)) + { + return ""; + } + + return llvm::utohexstr(status.getSize(), true) + "-" + + llvm::utohexstr(status.getLastModificationTime().time_since_epoch().count(), true); +} + int buildJitUnits(const char *argv0, llvm::StringRef mainFile, const CompileOptions &compileOptions, llvm::orc::JITTargetMachineBuilder &tmBuilder, std::vector &units) { diff --git a/tslang/tslang/jitcache.h b/tslang/tslang/jitcache.h index f6e8e1080..670d1c448 100644 --- a/tslang/tslang/jitcache.h +++ b/tslang/tslang/jitcache.h @@ -3,9 +3,11 @@ #include "TypeScript/DataStructs.h" +#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringRef.h" #include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h" #include "llvm/Support/MemoryBuffer.h" +#include "llvm/Target/TargetMachine.h" #include #include @@ -17,6 +19,10 @@ struct JitUnit std::string sourcePath; bool isMain = false; std::unique_ptr object; + // where the object is in the cache folder; empty if it could not be written there + std::string objectPath; + // the hash of the object, as its manifest has it + std::string objectHash; // what the file's global constructors became: the JIT runs no object's llvm.global_ctors, so // they are called from one function, which the JIT calls before the program starts std::string initFunction; @@ -31,4 +37,23 @@ struct JitUnit int buildJitUnits(const char *argv0, llvm::StringRef mainFile, const CompileOptions &compileOptions, llvm::orc::JITTargetMachineBuilder &tmBuilder, std::vector &units); +// Which build of tslang this is (its executable's size and time), or empty if that cannot be told: +// what the cache keys its objects on, so that another build compiles them again. +std::string jitCompilerId(const char *argv0); + +// --jit-native (jitnative.cpp): the units' objects linked into one DLL, with its PDB, in the cache +// folder beside the program's object; reused while the objects and options are the same. `dllPath` +// is set to the DLL. Non-zero if an object is not on disk or the link fails. +int linkJitUnits(const char *argv0, llvm::ArrayRef units, CompileOptions &compileOptions, + llvm::TargetMachine &tm, std::string &dllPath); + +// The entry point the JIT calls, `int (int argc, char **argv)`, whatever `main` lowered to (see +// addEntryThunk in jit.cpp) +#define JIT_ENTRY_THUNK_NAME "__tslang_jit_main" + +// The functions the DLL linkJitUnits makes exports beside the program's own: called before the +// program's initializers and after the program, they reach the C runtime linked into the DLL. +#define JIT_NATIVE_INIT_NAME "__tslang_jit_native_init" +#define JIT_NATIVE_FLUSH_NAME "__tslang_jit_native_flush" + #endif // TSLANG_JITCACHE_H_ diff --git a/tslang/tslang/jitnative.cpp b/tslang/tslang/jitnative.cpp new file mode 100644 index 000000000..e1614b02f --- /dev/null +++ b/tslang/tslang/jitnative.cpp @@ -0,0 +1,344 @@ +// --jit-native: the objects of the JIT cache linked into one DLL with its PDB and loaded by the +// system loader instead of RTDyld, so that WinDbg and Visual Studio see the program as a module +// with symbols and source lines. One DLL for all of them, as `--emit=exe` links one program: an +// import of another module's data (a global, a vtable, a class's `.size`) is resolved at link time, +// where a DLL per module would need `__imp_` references nothing emits. See +// docs/jit-native-dll-debugging-design.md. + +#include "TypeScript/DataStructs.h" +#include "TypeScript/Defines.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ExecutionEngine/Orc/CompileUtils.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/FileUtilities.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/WithColor.h" +#include "llvm/Support/xxhash.h" +#include "llvm/Support/raw_ostream.h" + +#include "jitcache.h" +#include "link.h" + +#include + +#define DEBUG_TYPE "tslang" + +namespace cl = llvm::cl; + +extern cl::opt enableOpt; + +std::string getDefaultLibBinary(bool, const CompileOptions &, const llvm::Triple &); + +namespace +{ + +std::string hashString(llvm::StringRef data) +{ + return llvm::utohexstr(llvm::xxh3_64bits(data), /*LowerCase=*/true, /*Width=*/16); +} + +// The functions the DLL exports for tslang beside the program's own, in an object of their own: the +// cached objects are also the ones the in-memory JIT loads, where none of this applies. +// +// The DLL links the C runtime statically, so it has its own: its own stdout buffer, which nothing in +// tslang can flush, and its own error modes, which no executable's startup has set - a DLL's C +// runtime does not know it runs in a console program, and reports a failing assert or an abort() +// with a modal box, a hang in an unattended run. +// - JIT_NATIVE_FLUSH_NAME flushes every stream of it; tslang calls it after the program, before +// TerminateProcess, which flushes nothing. +// - JIT_NATIVE_INIT_NAME sends assert and abort reports to stderr and takes the dialogs away +// (the debug C runtime's report modes too, if that is the one linked), and has SIGABRT flush +// the program's output first: a failing assert ends in abort(), which flushes nothing either. +llvm::Expected> compileHostFunctions(llvm::TargetMachine &tm, bool debugCRT) +{ + llvm::LLVMContext context; + llvm::Module module("tslang_jit_native", context); + auto dataLayout = tm.createDataLayout(); + module.setDataLayout(dataLayout); + module.setTargetTriple(tm.getTargetTriple()); + + auto *voidType = llvm::Type::getVoidTy(context); + auto *i32Type = llvm::Type::getInt32Ty(context); + auto *ptrType = llvm::PointerType::getUnqual(context); + auto *nullPtr = llvm::ConstantPointerNull::get(ptrType); + auto fflushFn = module.getOrInsertFunction("fflush", i32Type, ptrType); + + auto define = [&](llvm::StringRef name, llvm::FunctionType *type, llvm::GlobalValue::LinkageTypes linkage) { + auto *function = llvm::Function::Create(type, linkage, name, module); + function->setUWTableKind(llvm::UWTableKind::Async); + return function; + }; + + auto *flush = define(JIT_NATIVE_FLUSH_NAME, llvm::FunctionType::get(voidType, false), llvm::GlobalValue::ExternalLinkage); + { + llvm::IRBuilder<> builder(llvm::BasicBlock::Create(context, "entry", flush)); + builder.CreateCall(fflushFn, {nullPtr}); + builder.CreateRetVoid(); + } + + auto *onAbort = define("__tslang_jit_native_on_abort", llvm::FunctionType::get(voidType, {i32Type}, false), + llvm::GlobalValue::InternalLinkage); + { + llvm::IRBuilder<> builder(llvm::BasicBlock::Create(context, "entry", onAbort)); + builder.CreateCall(fflushFn, {nullPtr}); + builder.CreateRetVoid(); + } + + auto *init = define(JIT_NATIVE_INIT_NAME, llvm::FunctionType::get(voidType, false), llvm::GlobalValue::ExternalLinkage); + { + llvm::IRBuilder<> builder(llvm::BasicBlock::Create(context, "entry", init)); + // _set_error_mode(_OUT_TO_STDERR) + builder.CreateCall(module.getOrInsertFunction("_set_error_mode", i32Type, i32Type), {builder.getInt32(1)}); + // _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT) + builder.CreateCall(module.getOrInsertFunction("_set_abort_behavior", i32Type, i32Type, i32Type), + {builder.getInt32(0), builder.getInt32(3)}); + // signal(SIGABRT, onAbort) + builder.CreateCall(module.getOrInsertFunction("signal", ptrType, i32Type, ptrType), {builder.getInt32(22), onAbort}); + if (debugCRT) + { + auto setReportMode = module.getOrInsertFunction("_CrtSetReportMode", i32Type, i32Type, i32Type); + auto setReportFile = module.getOrInsertFunction("_CrtSetReportFile", ptrType, i32Type, ptrType); + // _CRTDBG_FILE_STDERR + auto *stderrFile = llvm::ConstantExpr::getIntToPtr( + llvm::ConstantInt::get(dataLayout.getIntPtrType(context), -5, /*IsSigned=*/true), ptrType); + // _CRT_ERROR (abort's report), _CRT_ASSERT + for (auto reportType : {1, 2}) + { + // _CRTDBG_MODE_FILE + builder.CreateCall(setReportMode, {builder.getInt32(reportType), builder.getInt32(1)}); + builder.CreateCall(setReportFile, {builder.getInt32(reportType), stderrFile}); + } + } + + builder.CreateRetVoid(); + } + + llvm::orc::SimpleCompiler compiler(tm); + return compiler(module); +} + +// The DLLs of earlier runs of the program, linked from objects or options since changed: removed, +// but for any a run still has loaded, which cannot be. Not another run's DLL still being linked - +// unless it is an hour old: what a run killed while it linked (by a debugger, say) leaves behind. +void removeOtherDlls(llvm::StringRef dllPath, llvm::StringRef prefix) +{ + auto staleBefore = std::chrono::system_clock::now() - std::chrono::hours(1); + std::error_code ec; + for (llvm::sys::fs::directory_iterator it(llvm::sys::path::parent_path(dllPath), ec), end; it != end && !ec; + it.increment(ec)) + { + auto path = it->path(); + auto name = llvm::sys::path::filename(path); + auto extension = llvm::sys::path::extension(path); + if (path == dllPath || !name.starts_with(prefix)) + { + continue; + } + + if (name.contains(".tmp.") || name.ends_with(".host.obj")) + { + llvm::sys::fs::file_status status; + if (!llvm::sys::fs::status(path, status) && status.getLastModificationTime() < staleBefore) + { + llvm::sys::fs::remove(path); + } + + continue; + } + + if (extension != ".dll" && extension != ".pdb") + { + continue; + } + + llvm::SmallString<256> dllOfPath(path); + llvm::sys::path::replace_extension(dllOfPath, "dll"); + if (dllOfPath != dllPath) + { + llvm::sys::fs::remove(path); + } + } +} + +} // namespace + +int linkJitUnits(const char *argv0, llvm::ArrayRef units, CompileOptions &compileOptions, + llvm::TargetMachine &tm, std::string &dllPath) +{ + for (auto &unit : units) + { + if (unit.objectPath.empty()) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--jit-native links the objects in the JIT cache, and '" << unit.sourcePath + << "' has none: its cache folder could not be written (see --jit-cache-dir)\n"; + return -1; + } + } + + auto &program = units.back(); + + llvm::SmallVector exports{JIT_ENTRY_THUNK_NAME, JIT_NATIVE_INIT_NAME, JIT_NATIVE_FLUSH_NAME}; + for (auto &unit : units) + { + if (!unit.initFunction.empty()) + { + exports.push_back(unit.initFunction); + } + + if (unit.isMain && unit.hasGCtorsMethod) + { + exports.push_back(MLIR_GCTORS); + } + } + + // the C runtime linkBinary links + auto debugCRT = !enableOpt.getValue(); + + // Everything the DLL is linked from: the objects (by their hashes), what it exports, and the + // options linkBinary picks the libraries by. A rebuilt compiler may link other libraries. + std::string key; + { + llvm::raw_string_ostream os(key); + os << "tslang-jit-native 1\n"; + os << "compiler " << jitCompilerId(argv0) << "\n"; + for (auto &unit : units) + { + os << "object " << unit.objectHash << "\n"; + } + + for (auto &name : exports) + { + os << "export " << name << "\n"; + } + + os << "crt=" << (debugCRT ? "debug" : "release") << " di=" << compileOptions.generateDebugInfo + << " mm=" << compileOptions.memoryModel << " no-default-lib=" << compileOptions.noDefaultLib << "\n"; + os << "default-lib=" + << (compileOptions.noDefaultLib ? std::string() : getDefaultLibBinary(/*shared=*/false, compileOptions, tm.getTargetTriple())) + << "\n"; + } + + auto prefix = llvm::sys::path::filename(program.sourcePath).str() + "."; + llvm::SmallString<256> path(llvm::sys::path::parent_path(program.objectPath)); + llvm::sys::path::append(path, prefix + hashString(key) + ".dll"); + dllPath = path.str().str(); + + llvm::SmallString<256> pdbPath(dllPath); + llvm::sys::path::replace_extension(pdbPath, "pdb"); + + if (llvm::sys::fs::exists(dllPath) && (!compileOptions.generateDebugInfo || llvm::sys::fs::exists(pdbPath))) + { + LLVM_DEBUG(llvm::dbgs() << "JIT native: using " << dllPath << "\n";); + return 0; + } + + auto hostFunctions = compileHostFunctions(tm, debugCRT); + if (!hostFunctions) + { + auto err = hostFunctions.takeError(); + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: failed to compile the host functions, error: " << err << "\n"; + llvm::consumeError(std::move(err)); + return -1; + } + + // Written and linked under names of their own, then the DLL renamed into place: another run + // linking the same program at the same time never loads half a file. + llvm::SmallString<256> hostObjectPath; + llvm::SmallString<256> tempDllPath; + if (llvm::sys::fs::createUniqueFile(dllPath + ".%%%%%%%%.host.obj", hostObjectPath) || + llvm::sys::fs::createUniqueFile(dllPath + ".%%%%%%%%.tmp.dll", tempDllPath)) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: could not create a file beside " << dllPath << "\n"; + return -1; + } + + llvm::FileRemover hostObjectRemover(hostObjectPath); + llvm::FileRemover tempDllRemover(tempDllPath); + // the import library and its exports file the link writes beside the DLL, which nothing uses + llvm::SmallString<256> tempLibPath(tempDllPath); + llvm::sys::path::replace_extension(tempLibPath, "lib"); + llvm::FileRemover tempLibRemover(tempLibPath); + llvm::SmallString<256> tempExpPath(tempDllPath); + llvm::sys::path::replace_extension(tempExpPath, "exp"); + llvm::FileRemover tempExpRemover(tempExpPath); + + { + std::error_code ec; + llvm::raw_fd_ostream os(hostObjectPath, ec); + if (!ec) + { + os << (*hostFunctions)->getBuffer(); + os.close(); + } + + if (ec || os.has_error()) + { + os.clear_error(); + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: could not write " << hostObjectPath << "\n"; + return -1; + } + } + + LinkRequest request; + for (auto &unit : units) + { + request.objects.push_back(unit.objectPath); + } + + request.objects.push_back(hostObjectPath.str().str()); + request.output = tempDllPath.str().str(); + request.shared = true; + // The static default library, linked in as a program links it. Its DLL would do for functions, + // but not for its data: a static field the program reads (a generic's instantiation reading + // PrimeHelpers.hashCollisionThreshold, say) is referenced by its own name, which only an + // `__imp_` reference resolves to an import. The in-memory JIT binds it from the export table. + request.staticDefaultLib = true; + request.quiet = true; + request.extraArgs.push_back("-Wl,/INCREMENTAL:NO"); + for (auto &name : exports) + { + request.extraArgs.push_back("-Wl,/EXPORT:" + name); + } + + if (compileOptions.generateDebugInfo) + { + // the PDB by its final name, and recorded in the DLL by its name alone, so the debugger + // finds it beside the DLL however the DLL was renamed or moved + request.extraArgs.push_back("-Wl,/PDB:" + pdbPath.str().str()); + request.extraArgs.push_back("-Wl,/PDBALTPATH:%_PDB%"); + } + + LLVM_DEBUG(llvm::dbgs() << "JIT native: linking " << dllPath << "\n";); + if (auto result = linkBinary(argv0, request, compileOptions)) + { + llvm::WithColor::error(llvm::errs(), "tslang") << "--jit-native: failed to link " << dllPath << "\n"; + return result; + } + + if (auto error = llvm::sys::fs::rename(tempDllPath, dllPath)) + { + // another run linked it first, and has it loaded + if (!llvm::sys::fs::exists(dllPath)) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--jit-native: could not rename " << tempDllPath << " to " << dllPath << ": " << error.message() << "\n"; + return -1; + } + } + else + { + tempDllRemover.releaseFile(); + } + + removeOtherDlls(dllPath, prefix); + return 0; +} diff --git a/tslang/tslang/link.h b/tslang/tslang/link.h new file mode 100644 index 000000000..e577c85d5 --- /dev/null +++ b/tslang/tslang/link.h @@ -0,0 +1,32 @@ +#ifndef TSLANG_LINK_H_ +#define TSLANG_LINK_H_ + +#include "TypeScript/DataStructs.h" + +#include "llvm/ADT/SmallVector.h" + +#include + +// What linkBinary links: the objects, in the order given, into `output`, with the libraries the +// compile options call for - the link `--emit=exe` and `--emit=dll` make, without the command line +// they read their objects and output from. +struct LinkRequest +{ + llvm::SmallVector objects; + std::string output; + // a DLL or shared object rather than a program + bool shared = false; + // a shared library that links the static default library, as a program does, rather than + // importing the default library's DLL + bool staticDefaultLib = false; + // driver arguments added after tslang's own, e.g. -Wl,/EXPORT:name + llvm::SmallVector extraArgs; + // the linker's output is shown only if the link fails + bool quiet = false; +}; + +// Links with the clang driver, as buildExe does; callable more than once in a process. Non-zero if +// the link fails. `argv0` is tslang's own path, which the driver resolves its tools against. +int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &compileOptions); + +#endif // TSLANG_LINK_H_ diff --git a/tslang/tslang/tslang.cpp b/tslang/tslang/tslang.cpp index d4e031da8..32718d912 100644 --- a/tslang/tslang/tslang.cpp +++ b/tslang/tslang/tslang.cpp @@ -128,6 +128,7 @@ cl::opt dumpObjectFile{"dump-object-file", cl::Hidden, cl::desc("Dump JITt cl::opt objectFilename{"object-filename", cl::Hidden, cl::desc("Dump JITted-compiled object to file .o"), cl::cat(TypeScriptCompilerDebugCategory)}; cl::opt jitCache{"jit-cache", cl::desc("--emit=jit: compile the program and each .ts module it imports into an object of its own, kept in a cache folder and loaded from there while its sources do not change (on by default; --jit-cache=false compiles everything into one module every run)"), cl::init(true), cl::cat(TypeScriptCompilerCategory)}; +cl::opt jitNative{"jit-native", cl::desc("--emit=jit on Windows: link the JIT cache's objects into a DLL with a PDB and load it with the system loader, so that WinDbg and Visual Studio can debug the program with its symbols and source (implies --di unless --opt is given; needs a linker and the JIT cache)"), cl::cat(TypeScriptCompilerCategory)}; cl::opt jitCacheDir{"jit-cache-dir", cl::desc("--emit=jit: the folder the JIT cache keeps its objects in (default: '__jit' beside each source file)"), cl::value_desc("folder"), cl::cat(TypeScriptCompilerCategory)}; cl::opt verbose{"verbose", cl::Hidden, cl::desc("Verbose output"), cl::init(false), cl::cat(TypeScriptCompilerDebugCategory)}; @@ -444,6 +445,22 @@ int main(int argc, char **argv) return -1; } + // --jit-native exists for the debugger, so it brings the debug info the debugger reads, unless + // the run asks to be optimized. Off Windows gdb and lldb read the JIT's own debug info already. + if (jitNative && emitAction == Action::RunJIT) + { +#ifdef _WIN32 + if (!generateDebugInfo && !(enableOpt.getNumOccurrences() && enableOpt)) + { + generateDebugInfo.setValue(true); + } +#else + llvm::WithColor::warning(llvm::errs(), "tslang") + << "--jit-native is for Windows debuggers and is ignored here: gdb and lldb debug the JIT as it is\n"; + jitNative.setValue(false); +#endif + } + // A JIT run is running the program, not building it, so it optimizes (at --opt_level, 3 unless // given) without being asked. Not when asked otherwise: an explicit --opt either way wins, and // --opt_level=0 or --di mean an unoptimized run - --opt would strip the debug info. @@ -499,6 +516,15 @@ int main(int argc, char **argv) readBorrowFacts(compileOptions); + // what --jit-native links is the objects the cache keeps + if (emitAction == Action::RunJIT && jitNative && !compileOptions.jitCache) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--jit-native links the objects of the JIT cache, which is not used with --jit-cache=false, " + "--dump-object-file or a program read from stdin\n"; + return -1; + } + // The JIT with its cache compiles each file it needs by itself, and only if it has to. if (emitAction == Action::RunJIT && compileOptions.jitCache) {