I don't know about "best", but here is a practice that has worked successfully for me in at least four projects now, three of which were "real", commercial ones. Although I've open-sourced only a specific case for ZeroMQ, I believe the principles are generic and should work for any native libraries. Most of the code could easily be reused, too, and it is licensed under Eclipse, so feel free to take it if you want. I haven't had a need for a more generic version of a native-including library, but I believe it could easily be extracted.
The problem I had with the standard solutions (lein's :native-path, JVM args, etc.) was that I wanted a portable solution for uberjar distribution that does not require the user to install anything else, so instructions such as "download the uberjar, install libzmq-dev from your package manager, and then launch the uberjar" were out of the question.
The principle is pretty simple: I bundle the native libraries within the library jar, for all supported platforms. That way:
- I, as the library author, control specifically which versions are used. No surprises there.
- Including the library from another project can be done without any awareness that the library relies on native libs.
- Similarly, uberjar distribution, whether from leiningen or maven, work seamlessly.
I'm not relying on any of the OS linking strategies either, for I found it hard to make them work reliably. So what I did was that I included all of the required native libraries that are not guaranteed to be present on a running system within the jar, and then, at runtime, the library:
- dynamically finds out what platform it is running on;
- extracts the native libs from its own archive towards the system-specific temp directory if it is not already there (based on a SHA; this was necessary to avoid accumulation on platforms that do not properly clean their temp dirs -- that is, Windows);
- "manually" load the native dependencies in the correct order, so dynamic linking does not try to resolve libraries on the host OS path.
There are of course downsides to that approach:
- As the build process for a single artifact involves three different OSes (I support Linux, Windows and OSX) and 2 architectures (i386 & x86_64), the build process is hard to automate and requires access to the relevant machines;
- Because specific versions of the native libs are bundled, library users cannot update them; in security-sensitive environments, this may be unacceptable;
- As a user of this library, you have to trust me that the bundled native libs are indeed what I claim they are, and have indeed been built through the process I describe in the source code, without any more tampering;
- As I know practically nothing about linking dynamic libraries, there may be other problems with this approach, though they haven't bitten me yet.