build: Fix various shared library build issues.
Node.js unofficially supports a shared library variant where the main node
executable is a thin wrapper around node.dll
/libnode.so
. The key benefit of this is to support embedding Node.js in other applications, for example the product I work on embeds a Node.js runtime in 3 separate applications (alongside the JVM and CLR in the same processes) in addition to having some standalone Node.js applications.
Since Node.js 12 there have been a number of issues preventing the shared library build from working correctly with the most significant issues being on Windows:
- A number of functions used executables such as
mksnapshot
are not exported fromlibnode.dll
using aNODE_EXTERN
attribute - A dependency on the
Winmm
system library is missing - Incorrect defines on executable targets leads to
node.exe
claiming to export a number of functions that are actually inlibnode.dll
- Because
node.exe
attempts to export symbols,node.lib
gets generated causing native extensions to try to link againstnode.exe
notlibnode.dll
. - Similarly, because
node.dll
was renamed tolibnode.dll
, native extensions don't know to look forlibnode.lib
rather thannode.lib
. - On macOS an RPATH is added to find
libnode.dylib
relative tonode
in the same folder. This works fine from theout/Release
folder but not from an installed prefix, wherenode
will be inbin/
andlibnode.dylib
will be inlib/
. - Similarly on Linux, the only RPATH that is added is
$ORIGIN
so LD_LIBRARY_PATH needs setting correctly forbin/node
to findlib/libnode.so
.
For the libnode.lib
vs node.lib
issue there are two possible options:
- Ensure
node.lib
fromnode.exe
does not get generated, and instead copylibnode.lib
tonode.lib
. This means addons compiled when referencing the correctnode.lib
file will correctly depend onlibnode.dll
. The down side is that native addons compiled with stock Node.js will still try to resolve symbols against node.exe rather than libnode.dll. - After building
libnode.dll
, dump the exports usingdumpbin
, and process this to generate anode.def
file to be linked intonode.exe
with the/DEF:node.def
flag. The export entries innode.def
would all readmy_symbol=libnode.my_symbol
node.exe
will redirect all exported symbols back tolibnode.dll
. This has the benefit that addons compiled with stock Node.js will load correctly intonode.exe
from a shared library build, but means that every embedding executable also needs to perform this same trick.
I went with the first option as it is the cleaner of the two solutions in my opinion. Projects wishing to generate a shared library variant of Node.js can now, for example,
.\vcbuild dll package vs2019
to generate a full node installation including libnode.dll
, Release\node.lib
, and all the necessary headers. Native addons can then be built against the shared library easily by specifying the correct nodedir
option.
For example
>npx node-gyp configure --nodedir C:\Users\User\node\Release\node-v18.0.0-win-x64
...
>npx node-gyp build
...
>dumpbin /dependents build\Release\binding.node
Microsoft (R) COFF/PE Dumper Version 14.29.30136.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file build\Release\binding.node
File Type: DLL
Image has the following dependencies:
KERNEL32.dll
libnode.dll
VCRUNTIME140.dll
api-ms-win-crt-string-l1-1-0.dll
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-runtime-l1-1-0.dll
...
I have tested my changes on Linux/x86_64, Linux/s390x, Linux/ppc64le (all on RHEL 7.9 devtoolset-10), Windows/x86_64 (VS2019 on Windows Server 2019 Datacenter Edition), and macOS/x86_64 (macOS 12.1 with Apple Clang 13.0.0). I have tried to test on AIX but the GCC 8 installation on the machine I have access to is currently broken...
Fixes #34539 (closed) Fixes #41559 (closed)
This PR is essentially a set of patches that I have been maintaining for the Node.js 14.x branch internally in my day job. Ideally I would love to see these changes get back ported but I can understand if this is not desireable.
Short of making shared library builds of Node.js the default, as is common for other embeddable runtimes such as the JVM or the CLR, or by building both the standard build and the shared library build together in the CI, I don't see a way to prevent changes that break the shared library from being merged in the future.