Loading Multiple Jars on Windows - JNI JNI_CreateJavaVM - java

I'm creating a JVM within my C++ application for windows, and I'm unable to convince it to load multiple jars.
My C++ code:
MyClass::MyClass(std::string & classPath) {
classPath = "-cp "+classPath; // <-- Won't work with any path or single jar
//classPath = "-Djava.class.path="+classPath; <-- Only works with single jar
jvmOptions[0].optionString = (char *)classPath.c_str();
jvmOptions[1].optionString = "-Xms8m";
jvmOptions[2].optionString = "-Xmx24m";
jvmArgs.version = JNI_VERSION_1_6;
jvmArgs.options = jvmOptions;
jvmArgs.nOptions = 3;
jvmArgs.ignoreUnrecognized = JNI_TRUE;
int jvmInitResult = CreateJavaVM( &jvm, (void**)&environment, &jvmArgs);
if( jvmInitResult >= 0 ) {
jclass loadedClass = environment->FindClass( MyClassName.c_str() );
.....
If I pass a path via my classPath variable to a single JAR, such as "C:\path\myjar.jar", the jclass variable is located fine. However, my Java class requires additional JARs to function, so I need to pass more than one JAR to the jvmOptions. When I try to pass the second, or third JAR, in any of the following ways, the FindClass call now fails.
C:\path\myjar.jar <--------- FindClass SUCCESS; can't use due to missing jars
C:\path\myjar.jar;C:\path\secondjar.jar <-----FindClass FAIL
C:\path\myjar.jar:C:\path\secondjar.jar <-----FindClass FAIL
C:\path\* <-----FindClass FAIL
C:\path\*.jar <-----FindClass FAIL
"C:\path\myjar.jar;C:\path\secondjar.jar" <-----FindClass FAIL
"C:\path\myjar.jar:C:\path\secondjar.jar" <-----FindClass FAIL
I assume there is another option I'm not thinking of, but this is driving me nuts.

You should use -cp to set the class path. I suspect -Djava.class.path= won't do what you think it should.

The solution is to not use windows file separators when passing the argument to the program. The \ ends up getting escape sequenced with one or more \ . Changing the argument to unix style file separators correctly loads all of the jars within a directory.
eg:
MyApp "classpath"
MyApp C:\pathtojars\ <-- fails
MyApp C:/pathtojars/ <-- works
Fixed code:
MyClass::MyClass(std::string & classPath )
{
classPath = "-Djava.class.path="+classPath;
jvmOptions[0].optionString = (char *)classPath.c_str();
jvmOptions[1].optionString = "-Xms8m";
jvmOptions[2].optionString = "-Xmx24m";
jvmArgs.version = JNI_VERSION_1_6;
jvmArgs.options = jvmOptions;
jvmArgs.nOptions = 3;
jvmArgs.ignoreUnrecognized = JNI_TRUE;
int jvmInitResult = CreateJavaVM( &jvm, (void**)&environment, &jvmArgs);
if( jvmInitResult >= 0 )
{
jclass loadedClass = environment->FindClass( MyClassName.c_str() );
.....

Related

Include multiple source directories in Qt for Android

I want to include Java source code from multiple directories (which are shared between projects) in a Qt for Android project. On http://imaginativethinking.ca/what-the-heck-how-do-i-share-java-code-between-qt-android-projects/ an approach is described which copies the Java source files:
# This line makes sure my custom manifest file and project specific java code is copied to the android-build folder
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
# This is a custom variable which holds the path to my common Java code
# I use the $$system_path() qMake function to make sure that my directory separators are correct for the platform I'm compiling on as you need to use the correct separator in the Make file (i.e. \ for Windows and / for Linux)
commonAndroidFilesPath = $$system_path( $$PWD/../CommonLib/android-sources/src )
# This is a custom variable which holds the path to the src folder in the output directory. That is where they need to go for the ANT script to compile them.
androidBuildOutputDir = $$system_path( $$OUT_PWD/../android-build/src )
# Here is the magic, this is the actual copy command I want to run.
# Make has a platform agnostic copy command macro you can use which substitutes the correct copy command for the platform you are on: $(COPY_DIR)
copyCommonJavaFiles.commands = $(COPY_DIR) $${commonAndroidFilesPath} $${androidBuildOutputDir}
# I tack it on to the 'first' target which exists by default just because I know this will happen before the ANT script gets run.
first.depends = $(first) copyCommonJavaFiles
export(first.depends)
export(copyCommonJavaFiles.commands)
QMAKE_EXTRA_TARGETS += first copyCommonJavaFiles
With later Qt versions the code has to be changed to this:
commonAndroidFilesPath = $$system_path($$PWD/android/src)
androidBuildOutputDir = $$system_path($$OUT_PWD/../android-build)
createCommonJavaFilesDir.commands = $(MKDIR) $${androidBuildOutputDir}
copyCommonJavaFiles.commands = $(COPY_DIR) $${commonAndroidFilesPath} $${androidBuildOutputDir}
first.depends = $(first) createCommonJavaFilesDir copyCommonJavaFiles
export(first.depends)
export(createCommonJavaFilesDir.commands)
export(copyCommonJavaFiles.commands)
QMAKE_EXTRA_TARGETS += first createCommonJavaFilesDir copyCommonJavaFiles
Is this the standard way to go, or is there some built-in functionality for including multiple Java source directories in Qt for Android projects?
Regards,
A much cleaner solution is this one:
CONFIG += file_copies
COPIES += commonJavaFilesCopy
commonJavaFilesCopy.files = $$files($$system_path($$PWD/android/src))
commonJavaFilesCopy.path = $$OUT_PWD/android-build

Module path in C++ to Java JNI Call

When I create a Java 8 JVM in C++ I usually use something like the following code to tell JVM the class path:
JavaVMOption* options = new JavaVMOption[1]; // JVM invocation options
options[0].optionString = (char *)"-Djava.class.path=.;./lib2"; // where to find java .class
vm_args.version = JNI_VERSION_1.8; // minimum Java version
vm_args.nOptions = 1; // number of options
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
But how to tell the Java 9 JVM about the module path? There is no java.module.path system property. The best I can find is something like:
JavaVMOption* options = new JavaVMOption[2]; // JVM invocation options
options[0].optionString = (char *)"-Djdk.module.path=.;./lib2"; // where to find java .class
options[1].optionString = (char *)"-Djdk.module.main=RemkaAgentService"; // where to find java .class
vm_args.version = JNI_VERSION_9; // minimum Java version
vm_args.nOptions = 2; // number of options
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
But this code does not work, it fails when I try to create JVM. I suppose it is because it does not support the options I try.
This is a late answer but persons that make searches about this problem may find it useful. There is indeed very few documentation to address this case, which is unfortunate.
I spent some time to investigate this problem and I found a way to make it work. Here are the steps to take.
Bundle your application packages within a custom JRE by using the 'jlink' tool provided by the JDK: https://docs.oracle.com/en/java/javase/11/tools/jlink.html
Use the class loader to find your main class. To do this in JNI, you can use the "FindClass()" function : https://docs.oracle.com/en/java/javase/11/docs/specs/jni/functions.html#findclass
The first step removes the requirement to define the '--module-path' flag that is not supported by JNI. Note that to properly bundle your application, all you jars must be converted to modular jars. Third-party jars may not be true modular jars but rather automatic modules, but automatic modules are not supported by jlink (Using jlink with automatic modules). Fortunately, there is a method to perform this automatically documented here : creating module-info for automatic modules with jdeps in java 9
The second step works even if your class lies within a module. Notes that the class name parameter for a class 'p1.p2.p3.MyClass' must be 'p1/p2/p3/MyClass'. I mentioned this as I wasted some time due to this difference in the convention.
I may publish an example if I can find some time for this but it may not happens in a short time-frame.
So the right options are the following
JavaVMOption* options = new JavaVMOption[2]; // JVM invocation options
options[0].optionString = (char *)"--module-path=.;./lib2"; // where to find java .class
options[1].optionString = (char *)"--add-modules=RemkaAgentService,spring.context";
vm_args.version = JNI_VERSION_9; // minimum Java version
vm_args.nOptions = 2; // number of options
vm_args.options = options;
vm_args.ignoreUnrecognized = false; // invalid options make the JVM init fail

Java JNI GDAL native library error with ClassLoader when redeploying as web application

I'm using GDAL native library (C++ and it is installed in /usr/lib/java/gdal). I found a trick short time ago, to allow Tomcat can load the web application and this library (cannot use System.load() or System.loadLibrary() as all will return error)
Caused by: java.lang.UnsatisfiedLinkError: org.gdal.osr.osrJNI.new_SpatialReference__SWIG_1()J
Then I need to use a trick to add the library path to JVM when application starts:
final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
usrPathsField.setAccessible(true);
// get array of paths
final String[] paths = (String[]) usrPathsField.get(null);
// check if the path to add is already present
for (String path : paths) {
if (path.equals(pathToAdd)) {
return;
}
}
//add the new path
final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = pathToAdd;
usrPathsField.set(null, newPaths);
This works well when the Tomcat starts with application, however, if I redeploy the application, it will return error:
Caused by: java.lang.UnsatisfiedLinkError: Native Library /usr/lib/java/gdal/libgdaljni.so already loaded in another classloader
I could not find any solution in StackOverflow, so I ask here if anyone can give some information. I also cannot change or add library path to environment variable or Tomcat folder, all should be done in Java code only.
So to avoid to add library to Tomcat/lib folder, I copy all the GDAL native folder to a temp directory with time stamp (e.g: /tmp/gdal_native/date.time), then I use the code above normally, except when it checks for the previous path, it will override with the new one.
String tmpTargetNativeFolderPath = "/tmp/gdal_native" + "/" + current date time
int i = 0;
// check if the path to add is already present
for (String path : paths) {
String pathFolder = StringUtils.substringBeforeLast(path, "/");
if (pathFolder.equals("/tmp/gdal_native")) {
// Override the old path with the new one
paths[i] = tmpTargetNativeFolderPath;
usrPathsField.set(null, paths);
return;
}
i++;
}
Then Classloader will load the library from another folder when the web application is redeployed without the error and the usrPathsField only contains one folder path to /tmp/gdal_native/timestamp.

How to set up classpath of JavaCompiler to multiple .jar files using wildcard

I am using JavaCompiler of javax.tools to compile some java code and I am trying to use wildcard in my classpath in order to include all the .jar files but I fail.
Here is my code:
String classpath = "C:\tomcat6\webapps\myapp/WEB-INF/lib/javax.ws.rs-api-2.0-m10.jar;"
+ "C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/javax.persistence-2.1.0.jar";
Iterable<String> options = Arrays.asList("-d", classesBaseDir,
"-classpath", classpath);
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager,
diagnostics, options, null, file);
boolean result = task.call();
The code above works just fine. But when I am trying to change the classpath to
String classpath = "C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/*";
it fails with
compiler.err.doesnt.exist|package javax.ws.rs does not exist
...
symbol: class GET
location: class com.my.oasis.resources.TestClass
09/04/2014 14:27:09:030 | COMPILER_DIAGNOSTIC | compileResource() - compiler.err.cant.resolve.location|cannot find symbol
...
I have also tried the following alterations
String classpath = "\"C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/*\"";
String classpath = "'C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/*'";
but none of them worked. Any ideas?
Thanks
Note: the reason why the path includes slashes and backslashes is because the my program identifies the environment in runtime and auto completes the path.
Edit: I am using tomcat 6 and java 1.7.0_21
Wildcards: Since Java 1.6 wildcards are supported when using java/javaw/javac, more information: Windows/Solaris and Linux
example:
javac -cp "lib/*" Test.java
This uses all .jar files (not .class!) in the lib directory as classpath. This should not be confused with the *-expansion of your shell. -cp lib/* gets expanded to -cp lib/a.jar lib/b.jar which is not valid argument syntax. In order to avoid this you have to add quotation marks: -cp "lib/*"
The cause of your Problem: You are trying to call the Java compiler from source directly with its Java API. This source code does not contain the wildcard expansion.
The JDK ships with a wrapper binary (javac,javadoc,javah,javap are all the same binary) which does some things and finally calls the compiler task. This wrapper also expands the wildcards in your classpath and therefore the compiler task doesn't have to do this anymore (and it doesn't). See at Compiler Readme section "build -> Notes -> The launcher". Launcher sourcecode.
Solution:
A very poor solution would be to call javac through a Processbuilder. (This is not recommended since it is a complicated and error prone solution for a simple problem)
Expand the wildcards yourself:
example code:
String classpath = buildClassPath("lib/", "test/", "lib/*");
System.out.println(classpath);
// output: lib/;test/;lib/a.jar;lib/b.jar;
This function takes all classpath entries and builds one classpath. Classpath entries with a wildcard in it will get expanded.
/**
* This function builds a classpath from the passed Strings
*
* #param paths classpath elements
* #return returns the complete classpath with wildcards expanded
*/
private static String buildClassPath(String... paths) {
StringBuilder sb = new StringBuilder();
for (String path : paths) {
if (path.endsWith("*")) {
path = path.substring(0, path.length() - 1);
File pathFile = new File(path);
for (File file : pathFile.listFiles()) {
if (file.isFile() && file.getName().endsWith(".jar")) {
sb.append(path);
sb.append(file.getName());
sb.append(System.getProperty("path.separator"));
}
}
} else {
sb.append(path);
sb.append(System.getProperty("path.separator"));
}
}
return sb.toString();
}
Using backslashes or slashes makes no difference. But you obviously assume that the path is auto-globbed (like a normal command line would). This does not happen. So you run your compiler as you would with a command line arg of
-classpath 'C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/*'
which is not what you want. You may look into the API docs for java.io.File.listFiles(FileFilter) or even java.nio.file.Files.walkFileTree(Path, FileVisitor) to gain a better understanding.
To expand a bit on that, when your shell sees C:\tomcat6\webapps\myapp/WEB-INF/lib/* it expands it into a space-separated list of whatever is in your WEB-INF/lib directory. This is called globbing, or since it's done automatically, auto-globbing.
Now Java doesn't do that, but you can build it yourself in a few lines of code:
StringBuilder sb = new StringBuilder();
String[] filenames = new File("C:\\tomcat6\\webapps\\myapp/WEB-INF/lib/").list();
for(int i = 0; i < filenames.length; i++) {
if(i > 0) sb.append(File.pathSeparatorChar); // separate with ':' or ';' on Win
sb.append(filenames[i]); // append the filename
}
Iterable<String> options = Arrays.asList("-d", classesBaseDir, "-cp", sb.toString())
...
Since Java1.7 you can also use Files.newDirectoryStream(Path) instead of list(File). With 1.8 you could even call join instead of joining manually.

SCons not picking up all class files when creating a jar

SCons neophyte here. I am using it(version 2.0) to create a jar as follows:
compiled_classes = env.Java \
(target = compiled_classes_dir,
source = source_tld,
JAVAVERSION='1.6',
JAVACLASSPATH=['source_tld/libs/' +
file.name
for file in
Glob('source_tld/' +
'libs/*.jar')])
new_jar = env.Jar(target = jar_name,
source = compiled_classes_dir)
I am seeing an issue wherein class files belonging to classes that have inner classes(which when compiled into class files have a $ in the name) are not being handled properly i.e. they do not get included in the generated JAR. Any suggestions to address this would be greatly appreciated. TIA.
PS: This suggestion to add JAVAVERSION didn't seem to help.
Since SCons is incorrectly calculating the output classes I would suggest this workaround.
compiled_classes = env.Java \
(target = compiled_classes_dir,
source = source_tld,
JAVAVERSION='1.6',
JAVACLASSPATH=['source_tld/libs/' +
file.name
for file in
Glob('source_tld/' +
'libs/*.jar')])
#workaround to make sure classes are cleaned
env.Clean(compiled_classes, env.Dir(compiled_classes_dir))
# its important to set the JARCHDIR or the Jar command will not be run
# from the correct location if you want an executable Jar add the manifest here
new_jar = env.Jar(target = jar_name,
source = [compiled_classes_dir], JARCHDIR='$SOURCE')

Categories