Home > Mobile >  How do I resolve UnsatisfiedLinkError with JavaCPP native libraries while using JPMS Java modules?
How do I resolve UnsatisfiedLinkError with JavaCPP native libraries while using JPMS Java modules?

Time:03-08

I have a Java 17 project using Gradle with a multitude of JavaCPP libraries. I started with a simple demo project that I cloned from the JavaCPP Github repo. This sample project incorporates several native libs such as OpenCV, ffmpeg, etc., so I thought it'd be a good test. And, no surprise, it worked just fine. It brought up my camera, did the face detection, etc. All very cool.

My aim - I want to modularize my JavaCPP projects to be JPMS compliant.

Not easy. So, to troubleshoot, I figure that I would start with good test code, which is why I'm working with the official JavaCPP Gradle demo program.

So, I did the following to convert it to be JPMS compliant:

  1. Created a module-info.java placed down src/main/java. I added the appropriate requires statements (see below).
  2. Modified build.gradle to add several *-platform dependencies and a few other plugins, including JavaFX.

The TL;DR - I got it to work (though the camera appears at an angle in the app window, which is just weird, but I'm assuming I'm still missing a library in module-info.java). The problem is that it only worked after I not only specified numerous additional *-platform dependencies in build.gradle, but also needed to list the actual native platform libraries in module-info.java. So, for instance, I need to add the following statement:

requires org.bytedeco.opencv.macosx.x86_64;

If I do not do that, then I get the following error:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no jniopencv_core in java.library.path: /Users/brk009/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.

My main question - How can I make a modular JavaCPP project build and execute properly without hard coding the platform dependent native libraries in module-info.java? I thought that just specifying the *-platform libraries in module-info.java would do it, but nope.

If this was just a project for my own system, then fine - I'd live with it. However, I want to pass some of my example code off to my students. It'd be fine if they all ran Macs. However, my students have quite a heterogeneous platform base (i.e. a mix of Mac, Windows, and Linux users.) Ideally, it'd be great to have a platform-independent codebase and let my program build and run regardless of the platform. Heck, I'd even be happy if I only needed to specify the platform as a parameter for gradlew as a command-line argument, such as indicated here, where I could just specify -PjavacppPlatform=linux-x86_64. But that did not work either.

I did verify that Loader.Detector.getPlatform() returns the correct platform string, and Loader.getCacheDir() returns ~/.javacpp/cache as you would expect.

Any help/guidance would be immensely appreciated! Thank you kindly.


module-info.java

module HelloJavaCPP {
    requires java.base;
    requires java.desktop;
    requires org.bytedeco.javacpp;
    requires org.bytedeco.javacpp.macosx.x86_64;  // I do NOT WANT to hard code any platform!
    requires org.bytedeco.javacv;
    requires org.bytedeco.opencv;
    requires org.bytedeco.opencv.macosx.x86_64;
    requires org.bytedeco.ffmpeg;
    requires org.bytedeco.ffmpeg.macosx.x86_64;
    requires org.bytedeco.openblas;
    requires org.bytedeco.openblas.macosx.x86_64;
}

build.gradle

plugins {
    id 'application'
    id 'java'
    id 'java-library'
    id 'org.openjfx.javafxplugin' version '0.0.12'
    id 'org.javamodularity.moduleplugin' version '1.8.10'
    id 'org.bytedeco.gradle-javacpp-platform' version '1.5.7'
}

group = 'org.hello'
version = '1.5.7'

repositories {
    mavenLocal()
    mavenCentral()
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

javafx {
    version = "17.0.2"
    modules = [ 'javafx.graphics','javafx.controls', 'javafx.fxml' ]
}

dependencies {
    api "org.bytedeco:javacv-platform:1.5.7"
    api 'org.bytedeco:opencv-platform:4.5.5-1.5.7'
//    api "org.bytedeco:opencv-platform-gpu:4.5.5-$version"
    api "org.bytedeco:ffmpeg-platform-gpl:5.0-$version"
    api 'org.bytedeco:openblas-platform:0.3.19-1.5.7'
    testImplementation 'junit:junit:4.13.2'
}

application {
    mainModule = "$moduleName"
    mainClass = "org.hello.Demo"
}


settings.gradle I'm including this just incase.

pluginManagement {
    repositories {
        mavenLocal()
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
        gradlePluginPortal()
    }
}

rootProject.name = 'HelloJavaCPP'

gradle.rootProject { ext.javacppVersion = '1.5.7' }

CodePudding user response:

Those links posted by Samuel above were immensely helpful. It turns out there are some modularity peculiarities with JavaFX that can wreck havoc when using JavaFX with non-JavaFX modules such as those in JavaCPP. See here.

The key part that was important:

We can either use the run goal of the JavaFX Maven plugin, the java goal of the Exec Maven plugin, or manually launch java with a module path computed from Maven dependencies and option --add-modules ALL-MODULE-PATH.

Once I figured out how to add a JVM argument in Gradle, I was able to remove all hard-coded architecture requires statements, and I can now use gradlew run, let JavaCPP's Loader class do all the work of discovering the architecture itself and loading the appropriate native libraries!

The two most important files that need to change:


module-info.java

Notice how much simpler this becomes, and it has NO hard-coded platform system architecture information, which is exactly what we want:

module HelloJavaCPP {
    requires java.base;
    requires java.desktop;
    requires org.bytedeco.javacpp;
    requires org.bytedeco.javacv;
    requires org.bytedeco.opencv;
    requires org.bytedeco.ffmpeg;
    requires org.bytedeco.openblas;
}

build.gradle

The most important change I needed to make (which I got from information posted here was to add a run configuration, and specify the JVM argument `--add-modules

plugins {
    id 'application'
    id 'java'
    id 'java-library'
    id 'org.openjfx.javafxplugin' version '0.0.12'
    id 'org.javamodularity.moduleplugin' version '1.8.10'
    id 'org.bytedeco.gradle-javacpp-platform' version "$javacppVersion"
}

group = 'org.hello'
version = '1.5.7'

repositories {
    mavenCentral()
}

ext {
    // javacppPlatform - should be autodetected, but can also specify on cmd line
    // as -PjavacppPlatform=macosx-x86_64
//    javacppPlatform = 'linux-x86_64,macosx-x86_64,windows-x86_64,etc' // defaults to Loader.getPlatform()
    javacppPlatform = 'macosx-x86_64' // defaults to Loader.getPlatform()
}

javafx {
    version = "17.0.2"
    modules = [ 'javafx.graphics','javafx.controls', 'javafx.fxml' ]
}

dependencies {
    api "org.bytedeco:javacpp-platform:$javacppVersion"
    api "org.bytedeco:javacv-platform:$javacppVersion"
    api "org.bytedeco:opencv-platform:4.5.5-1.5.7"
    api "org.bytedeco:ffmpeg-platform-gpl:5.0-1.5.7"
    api "org.bytedeco:openblas-platform:0.3.19-1.5.7"
    testImplementation 'junit:junit:4.13.2'
}

application {
    mainModule = "$moduleName"
    mainClass = "org.hello.Demo"
}

// THIS WAS THE PRIMARY CHANGE: 
run {
    jvmArgs = ['--add-modules', 'ALL-MODULE-PATH']
}

It's worth noting that I was able to remove the ext configuration in build.gradle, which allows Loader.getPlatform() to do the work of determining the platform at runtime, and it worked just fine! (I left it in place just for reference purposes.)

I hope this helps others. I did NOT test out building an image, as judging from what I read, that is quite an additional level of complexity. We'll tackle that another time.

Thank you again.

CodePudding user response:

I'm glad that the linked wiki helped you. You problem was not related to JavaFX (as long as you don't build a JLink image) though. You may have found more specific help in this wiki instead.

Thank you for posting your detailed solution. It may probably be still simplified by dropping some dependencies or requires like openblas-platform (dependency already implied by opencv-platform), java.base etc...

  • Related