Shipping node addons with libraries
Tags: node, javascript
Published: 2020-07-13 15:47:48


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 import an addon, the respective dynamic loader will have to load all required libraries.

## How can we check which libraries we require?

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

In order to load these required 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](https://en.wikipedia.org/wiki/Rpath).
These paths are hardcoded into the binary and may specify additional paths to search for libraries.

## How can we retrieve a binaries `RPATH`?

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

## Ok, `rpath` it is!

We can configure our `rpath` via linker flags:

```json
"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](https://www.lekensteyn.nl/rpath.html#what-is-rpath)

## Example

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.

```json
{
    "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](https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications) will start searching in the directory from which the application is loaded.

## A cross-platform approach
[My approach](https://github.com/nut-tree/opencv4nodejs) 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:
```json
{
    "target_name": "action_before_build",
    "type": "none",
    "copies": [{
        "files": [ "<!@(node ./lib/bundle.js)" ],
        "destination": "<(PRODUCT_DIR)"
    }]
}
```