c++node.jsnode-gypnode.js-addon

node-gyp not linking libraries correctly on macos


I'm working on a node addon but there is an error on finding libraries after building with node-gyp.

here is my binging.gyp file:

{
"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' ]
}]
}

This builds correctly and if I put so file in the root folder of the project, everything works correctly. But I want to run a project with files in lib folder.

This is the error when I try to run the project so files in the lib folder instead of the root folder.

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

Solution

  • The thing with linked 3rd-party libraries is that you'll have to provide locations to search for them. node addons are dynamically linked, shared libraries, so whenever you require an addon, the respective dynamic loader will have to load all required libraries.

    How can we check which libraries we require?

    In order to load these requires libraries, our loader is going to search in several places, e.g. in all paths listed in LD_LIBRARY_PATH on Linux.

    But we're shipping our own lib, which is not in one of the standard search paths?

    It is possible for a lib or executable (ELF format on Linux or Mach-O format e.g. macOS) to specify a runtime loader path. These paths are hardcoded into the binary and may specify additional paths to search for libraries.

    How can we retrieve a binaries RPATH?

    Ok, rpath it is!

    We can configure our rpath via linker flags:

    "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? What's this?

    rpath entries are hardcoded, so having it fixed to e.g. /home/youruser/libs/foo/bar/ will break as soon as you try to use your addon on a different machine.

    Both $ORIGIN and @loader_path are token which our dynamic loader is going to replace with the directory containing our binary. So no matter where our library will be installed, if we specify paths relative to our binaries location, the dynamic loader will be able to find it. ('$$ORIGIN' is just a little workaround required so node-gyp doesn't mess things up when trying to substitute values)

    A good read regarding this topic is this article

    Example

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

    Our build generates the ./build/Release/addon.node file, which is linked against ./lib/my_library.dylib at build time.

    Using @loader_path we can now specify an rpath relative to our binaries location, since @loader_path will be replaced with /whatever/path/to/our/package/build/Release.

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

    At runtime, this will result in /whatever/path/to/our/package/build/Release/../../lib, exactly where our lib is located.

    What about Windows?

    Windows binaries do not have / use an rpath property.

    However, the DLL search order will start searching in the directory from which the application is loaded.

    A cross-platform approach

    My approach to shipping required libraries is as follows:

    1. Link your libraries during build
    2. Copy your libraries to the resulting output directory, e.g. build/Release
    3. Set the rpath to either $ORIGIN or @loader_path on Linux and macOS

    This way, we're instructing the dynamic loader on Linux and macOS to search for our lib in the same folder as our resulting binary, which is the default behaviour on Windows.

    Copying files can be done via an additional target in our gyp file:

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