Search code examples
javascalajava-native-interfacesbtclassloader

How to properly load a native library for sbt tests?


I have a sbt project and a java class that statically loads a native library and contains the native methods. It looks like this:

public class NativeContainer {
  static {
    System.load("/path-to-lib");
  }

  public static native void nativeFunc(int n);
}

I also have a Scala test that calls the native function like this:

class TestJni extends FunSpec { 
  describe("JNI test") {
    NativeContainer.nativeFunc(5);
  }
}

When I run the test via sbt once, everything works fine. However, at every next run I get:

[error] Could not run test intrinsics.TestJni: java.lang.UnsatisfiedLinkError: Native Library /path-to-lib already loaded in another classloader

What would be a proper way to load the library to avoid that? Restarting sbt works, but I was looking for a more flexible solution.

I don't use any libraries or plugins for glueing sbt with JNI.

This is the full stack trace:

[debug] Running TaskDef(TestJni, org.scalatest.tools.Framework$$anon$1@1d29c60d, false, [SuiteSelector]) java.lang.UnsatisfiedLinkError: Native Library /path-to-lib/libNativeContainer.dylib already loaded in another classloader
        at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1907)
        at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
        at java.lang.Runtime.load0(Runtime.java:809)
        at java.lang.System.load(System.java:1086)
        at NativeContainer.<clinit>(NativeContainer.java:5)
        at TestJni$$anonfun$1.apply$mcV$sp(TestJni.scala:16)
        at org.scalatest.SuperEngine.registerNestedBranch(Engine.scala:613)
        at org.scalatest.FunSpecLike$class.describe(FunSpecLike.scala:357)
        at org.scalatest.FunSpec.describe(FunSpec.scala:1626)
        at TestJni.<init>(TestJni.scala:7)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at java.lang.Class.newInstance(Class.java:442)
        at org.scalatest.tools.Framework$ScalaTestTask.execute(Framework.scala:646)
        at sbt.TestRunner.runTest$1(TestFramework.scala:76)
        at sbt.TestRunner.run(TestFramework.scala:85)
        at sbt.TestFramework$$anon$2$$anonfun$$init$$1$$anonfun$apply$8.apply(TestFramework.scala:197)
        at sbt.TestFramework$$anon$2$$anonfun$$init$$1$$anonfun$apply$8.apply(TestFramework.scala:197)
        at sbt.TestFramework$.sbt$TestFramework$$withContextLoader(TestFramework.scala:185)
        at sbt.TestFramework$$anon$2$$anonfun$$init$$1.apply(TestFramework.scala:197)
        at sbt.TestFramework$$anon$2$$anonfun$$init$$1.apply(TestFramework.scala:197)
        at sbt.TestFunction.apply(TestFramework.scala:202)
        at sbt.Tests$.sbt$Tests$$processRunnable$1(Tests.scala:239)
        at sbt.Tests$$anonfun$makeSerial$1.apply(Tests.scala:245)
        at sbt.Tests$$anonfun$makeSerial$1.apply(Tests.scala:245)
        at sbt.std.Transform$$anon$3$$anonfun$apply$2.apply(System.scala:44)
        at sbt.std.Transform$$anon$3$$anonfun$apply$2.apply(System.scala:44)
        at sbt.std.Transform$$anon$4.work(System.scala:63)
        at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226)
        at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226)
        at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
        at sbt.Execute.work(Execute.scala:235)
        at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226)
        at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226)
        at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:159)
        at sbt.CompletionService$$anon$2.call(CompletionService.scala:28)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745) [error] Could not run test TestJni: java.lang.UnsatisfiedLinkError: Native Library /path-to-lib/libNativeContainer.dylib already loaded in another classloader

Solution

  • I guess it looks like what the issue is is that, by default, sbt runs each round of tests in the same JVM but a different ClassLoader, but JNI libraries can only be linked once, not multiple times in multiple ClassLoaders.

    sbt has a setting...

    fork := true
    

    ...that causes tests to be run in a fresh JVM rather than in sbt's JVM under a new ClassLoader. (See the docs.) Under this setting, your classes will be loaded only once per JVM (as they probably would be in production scenarios), without illegal attempts to multiply link JNI libraries through different ClassLoaders. This should resolve your issue.