node-gyp 没有在 macOS 上正确链接库

node-gyp not linking libraries correctly on macos

我正在开发一个节点插件,但在使用 node-gyp 构建后查找库时出错。

这是我的 binging.gyp 文件:

{
"targets": [{
    "target_name": "xaddon",
    "cflags!": [ "-fno-exceptions" ],
    "cflags_cc!": [ "-fno-exceptions" ],
    "sources": [
        "cppsrc/main.cc"
    ],
    'include_dirs': [
        "<!@(node -p \"require('node-addon-api').include\")", "lib"
    ],
    "libraries": ["<(module_root_dir)/lib/xaddon.so"],
    'dependencies': [
        "<!(node -p \"require('node-addon-api').gyp\")"
    ],
    'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}]
}

这构建正确,如果我将 so 文件放在项目的根文件夹中,一切正常。但我想 运行 一个包含 lib 文件夹中文件的项目。

这是我尝试 运行 lib 文件夹而不是根文件夹中的项目 so 文件时出现的错误。

Error: dlopen(#PATH_TO_PROJECT#/build/Release/xaddon.node, 1): Library not loaded: xaddon.so
Referenced from: #PATH_TO_PROJECT#/build/Release/xaddon.node
Reason: image not found

链接的第 3 方库的问题是您必须提供位置才能搜索它们。 节点插件是动态链接的共享库,因此无论何时您需要插件,相应的动态加载器都必须加载所有必需的库。

我们如何检查我们需要哪些库?

  • macOS:otool -L your_addon.node
  • Linux: ldd you_addon.node

为了加载这些需要的库,我们的加载器将在几个地方进行搜索,例如在 Linux 上 LD_LIBRARY_PATH 中列出的所有路径中。

但是我们正在运送我们自己的库,它不在标准搜索路径之一中?

lib 或可执行文件(Linux 上的 ELF 格式或 Mach-O 格式,例如 macOS)可以指定 runtime loader path。这些路径被硬编码到二进制文件中,并且可以指定其他路径来搜索库。

我们如何检索二进制文件 RPATH

  • macOS:otool -l your_addon.node | grep RPATH -A2
  • Linux: objdump -x your_addon.node | grep RPATH

好的,rpath是!

我们可以通过链接器标志配置 rpath

"conditions": [
            ["OS==\"mac\"",
              {
                "link_settings": {
                  "libraries": [
                    "-Wl,-rpath,@loader_path",
                    "-Wl,-rpath,@loader_path/..",
                  ],
                }
              }
            ],
            ["OS==\"linux\"",
              {
                "link_settings": {
                  "libraries": [
                    "-Wl,-rpath,'$$ORIGIN'",
                    "-Wl,-rpath,'$$ORIGIN'/.."
                  ],
                }
              }
            ]
        ],

$$ORIGIN@loader_path?这是什么?

rpath 条目是硬编码的,因此将其固定为例如/home/youruser/libs/foo/bar/ 一旦您尝试在另一台机器上使用您的插件就会崩溃。

$ORIGIN@loader_path 都是令牌,我们的动态加载器将用包含二进制文件的目录替换它们。所以无论我们的库安装在哪里,如果我们指定相对于我们的二进制文件位置的路径,动态加载器就能够找到它。 ('$$ORIGIN' 只是需要一个小的解决方法,因此 node-gyp 在尝试替换值时不会搞砸)

关于这个主题的好书是 this article

例子

.
├── build
│   └── Release
│       └── addon.node
├── index.js
├── lib
│   └── my_library.dylib
├── package-lock.json
└── package.json

我们的构建生成 ./build/Release/addon.node 文件,该文件在构建时链接到 ./lib/my_library.dylib

使用 @loader_path 我们现在可以指定相对于二进制文件位置的 rpath,因为 @loader_path 将替换为 /whatever/path/to/our/package/build/Release

              {
                "link_settings": {
                  "libraries": [
                    "-Wl,-rpath,@loader_path/../../lib",
                  ],
                }
              }

在运行时,这将导致 /whatever/path/to/our/package/build/Release/../../lib,正是我们的库所在的位置。

Windows呢?

Windows 二进制文件没有/使用 rpath 属性.

但是,DLL search order 将在加载应用程序的目录中开始搜索。

跨平台方法

My approach 运送所需的库如下:

  1. Link 构建期间的库
  2. 将您的库复制到生成的输出目录,例如build/Release
  3. 在 Linux 和 macOS
  4. 上将 rpath 设置为 $ORIGIN@loader_path

这样,我们指示 Linux 和 macOS 上的动态加载程序在与生成的二进制文件相同的文件夹中搜索我们的库,这是 Windows 上的默认行为。

复制文件可以通过我们的 gyp 文件中的附加目标完成:

{
    "target_name": "action_before_build",
    "type": "none",
    "copies": [{
        "files": [ "/your/lib.dylib" ],
        "destination": "<(PRODUCT_DIR)"
    }]
}