Skip to content

Fix node build on Windows#255

Merged
akoeplinger merged 1 commit intodotnet/mainfrom
fix-node-windows
Dec 1, 2025
Merged

Fix node build on Windows#255
akoeplinger merged 1 commit intodotnet/mainfrom
fix-node-windows

Conversation

@akoeplinger
Copy link
Copy Markdown
Member

@akoeplinger akoeplinger commented Dec 1, 2025

Some recent version of MSVC regressed linking mksnapshot.exe, resulting in lots of LNK2019 errors.

Copilot helped me find a workaround, below is the summary:

Details GitHub Copilot: Here’s a quick roadmap of what’s going on, followed by a deeper dive:
  1. V8 relies on explicit template instantiations for many core data structures (dictionaries, hash tables, factory helpers, etc.). Headers declare those templates with extern template to stop the compiler from instantiating them multiple times everywhere.
  2. The explicit definitions live in .cc files such as factory-base.cc, factory.cc, ordered-hash-table.cc, and shared-function-info.cc. They use helpers from export-template.h to decide whether to decorate the instantiation with __declspec(dllexport) (when building a shared V8) or leave it undecorated (when building static libraries).
  3. Because Node’s Windows builds defined BUILDING_V8_SHARED=1 (via common.gypi) even when producing static libs (*.lib), those instantiation definitions became template class __declspec(dllexport) …;.
  4. Recent MSVC versions (19.40/41+) hit a compiler regression: when a translation unit that lives in a static library contains an explicit instantiation marked __declspec(dllexport), the compiler either suppresses code emission or the objects are later discarded; the static library ends up missing the symbol entirely.
  5. Since the headers have extern template declarations, no translation unit tries to re-instantiate the template implicitly. So when the linker looks for, say, v8::internal::FactoryBase<Factory>::AllocateRawWithImmortalMap or HashTable<NameDictionary>::Shrink, it finds nothing and raises the unresolved externals you saw.
  6. Removing the false BUILDING_V8_SHARED define keeps V8’s template instantiations undecorated. With plain instantiations, MSVC generates the functions correctly and the link succeeds, even on the versions that exhibit the bug.

You can see the mechanics here:

  • macros.h sets V8_EXPORT_PRIVATE to __declspec(dllexport) whenever BUILDING_V8_SHARED is defined.
  • export-template.h decides whether to apply the export attribute on the declaration or the definition:
    extern template class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
        HashTable<...>;
    template class EXPORT_TEMPLATE_DEFINE(V8_EXPORT_PRIVATE)
        HashTable<...>;
    When V8_EXPORT_PRIVATE is __declspec(dllexport), the definition becomes template class __declspec(dllexport) HashTable<...>;.
  • That was fine when we actually built a V8 DLL; it’s fatal when the instantiations are packaged inside a static library and fed to a toolchain version with the regression.

So from a template standpoint: V8 intentionally avoids implicit instantiation by declaring every interesting template extern template ... in headers, then explicitly instantiating them in a handful of source files. If the explicit instantiation vanishes (because the compiler drops it under that misconfigured dllexport scenario), there is literally no code emitted for the template, and the linker fails.

By removing BUILDING_V8_SHARED=1 from the static-library configs (or flipping the logic to USING_V8_SHARED only when you really link against a DLL), those explicit instantiations revert to plain definitions and the bug is bypassed—the compiler emits the code, the library contains the symbols, and the linker resolves them.

After that, I was still hitting LNK2019 errors in LocalFactory::true_value/false_value:

Details GitHub Copilot: Nice progress—only the `LocalFactory::true_value/false_value` pair is still missing now. Those two accessors are declared `inline` in local-factory.h, but in local-factory-inl.h the definitions currently expand to
#define ROOT_ACCESSOR(Type, name, CamelName)  \
  Handle<Type> LocalFactory::name() {         \
    return read_only_roots().name##_handle(); \
  }

Because the definition itself isn’t marked inline, MSC 19.40+ treats it as an out-of-line function that should live in some object file. Before our BUILDING_V8_SHARED cleanup the functions were emitted via the __declspec(dllexport) instantiations in the DLL build path; once those decorations disappeared, the linker started looking for an actual symbol in the static archive and couldn’t find one.

Fix

Edit local-factory-inl.h so both macro expansions add the inline specifier:

#define ROOT_ACCESSOR(Type, name, CamelName)  \
  inline Handle<Type> LocalFactory::name() {  \
    return read_only_roots().name##_handle(); \
  }
READ_ONLY_ROOT_LIST(ROOT_ACCESSOR)
#undef ROOT_ACCESSOR

#define ACCESSOR_INFO_ACCESSOR(Type, name, CamelName)                          \
  inline Handle<Type> LocalFactory::name() {                                   \
    return Handle<Type>(                                                       \
        isolate()->isolate_->root_handle(RootIndex::k##CamelName).location()); \
  }
ACCESSOR_INFO_ROOT_LIST(ACCESSOR_INFO_ACCESSOR)
#undef ACCESSOR_INFO_ACCESSOR

But that still wasn't enough, it needed one more change:

Details

GitHub Copilot: Got it—you’re still seeing the LocalFactory::true_value/false_value unresolved externals even after marking the accessors inline. The missing piece is that the MSVC precompiled header (v8_pch.h, built into v8_pch.obj) includes src/api/api-inl.h, src/objects/objects-inl.h, factory-base-inl.h, etc., but not local-factory-inl.h. When it instantiates FactoryBase<LocalFactory>::ToBoolean, it only sees the declarations for LocalFactory::true_value/false_value, not the inline definitions, so it emits external references that never get resolved.

Fix

Add the inline definitions to the PCH:

--- a/tools/msvs/pch/v8_pch.h
+++ b/tools/msvs/pch/v8_pch.h
@@
 #include "src/objects/js-objects-inl.h"
 #include "src/objects/fixed-array-inl.h"
+#include "src/heap/local-factory-inl.h"
 #include "src/utils/utils.h"

This ensures every translation unit built with the precompiled header (including v8_pch.cpp itself) sees the inline definitions for LocalFactory::true_value() and false_value().

... and then it worked!

Some recent version of MSVC regressed linking mksnapshot.exe, resulting in lots of LNK2019 errors.
@akoeplinger akoeplinger enabled auto-merge (squash) December 1, 2025 18:20
@akoeplinger akoeplinger merged commit ccf4f64 into dotnet/main Dec 1, 2025
10 checks passed
akoeplinger added a commit that referenced this pull request Dec 1, 2025
Some recent version of MSVC regressed linking mksnapshot.exe, resulting in lots of LNK2019 errors.

(cherry picked from commit ccf4f64)
akoeplinger added a commit that referenced this pull request Dec 1, 2025
Some recent version of MSVC regressed linking mksnapshot.exe, resulting in lots of LNK2019 errors.

(cherry picked from commit ccf4f64)
akoeplinger added a commit that referenced this pull request Dec 1, 2025
Some recent version of MSVC regressed linking mksnapshot.exe, resulting in lots of LNK2019 errors.

(cherry picked from commit ccf4f64)
@akoeplinger akoeplinger deleted the fix-node-windows branch December 2, 2025 10:36
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