Michael Anderson
Michael Anderson

Reputation: 73490

Determine JNI/JVM version before launching JVM

I have a C application that uses JNI to embed a JVM and supports various Java versions. The app runs on linux, OSX and windows.

At the moment it allows providing JVM args via environment variables, but I'd like to make it smarter.

Unfortunately, I need to set some of the VM arguments differently depending on the Java version.

I could get the version if I had a JNIEnv* using GetVersion - but by the time I've got a JNIEnv*, I've already spun up a JVM and its too late. (And you can't shut down and restart a new JVM.)

It looks like JNI_GetDefaultJavaVMInitArgs would do the trick - but while it compiles OK I can't get a sensible answer from it. For example, using Java 9:

    JavaVMInitArgs vmArgs;
    vmArgs.version = JNI_VERSION_1_2;
    if (JNI_GetDefaultJavaVMInitArgs(&vmArgs)!=JNI_OK) {
        fprintf(stderr, "JNI_GetDefaultJavaVMInitArgs failed!\n");
    };
    fprintf(stderr, "Query with version = %x\n", JNI_VERSION_1_2);
    fprintf(stderr, "JAVA version = %x\n", vmArgs.version);

prints

Query with version = 10002
JAVA version = 10002

i.e. it hasn't updated the version.

Is there a way to get this information (in a cross-platform way)? Am I doing something wrong in my use of JNI_GetDefaultJavaVMInitArgs?

Upvotes: 0

Views: 1269

Answers (2)

domusmagister
domusmagister

Reputation: 167

You have to distinguish between JNI and JVM. Each JVM version supports a set of JNI API depending on its version (see https://docs.oracle.com/en/java/javase/18/docs/specs/jni/functions.html#version-information for the latest list). When you request the activation of the JVM you have to set the minimum version of JNI your application is written for; the C linkage is incremental, e.g. the GetModule is available from version 9. If you call CreateJavaVM with an highest version of JNI than the one the JVM supports, the API returns JNI_EVERSION.

If the arguments of your application depends on the JVM version the JNI version is unuseful: the latest JNI version is 10 and it is the same from JRE 10 to JRE 18.

To be able to invoke JNI API you have to load the JVM main library. When you load the JVM library you decide which version of JVM you will use and consequently the JNI version. A system can have multiple installed JVMs, one is the default while other can be accessed using the path to jvm.dll, libjvm.so, etc. Generally, the version of the JVM is written in the path and it depends on many things like the specification, the build, the vendor, the platform. This is an output of apt:

sudo apt install openjdk-11-jre-headless  # version 11.0.10+9-0ubuntu1~20.04, or
sudo apt install default-jre              # version 2:1.11-72
sudo apt install openjdk-8-jre-headless   # version 8u282-b08-0ubuntu1~20.04
sudo apt install openjdk-13-jre-headless  # version 13.0.4+8-1~20.04
sudo apt install openjdk-14-jre-headless  # version 14.0.2+12-1~20.04

You can parse the path of the JVM library trying to identify the JRE specification (8, 11, 13, ...) then you can call CreateJavaVM with the arguments your application needs for that version.

Upvotes: 0

Botje
Botje

Reputation: 30840

Why not do the simplest thing first and just start a JVM in a separate process to figure out the JVM version:

int fds[2];
pipe(fds);
if (fork() == 0) { // child
  close(fds[0]);
  JavaVMInitArgs vm_args;
  vm_args.version = JNI_VERSION_1_2;
  vm_args.options = nullptr;
  vm_args.nOptions = 0;
  vm_args.ignoreUnrecognized = true;

  JNIEnv *env = nullptr;
  jint res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);

  jclass cls_Runtime = env->FindClass("java/lang/Runtime");
  jmethodID mid_version = env->GetStaticMethodID("version", "()Ljava/lang/Runtime$Version;");
  jobject obj_Version = env->CallStaticObjectMethod(cls_Runtime, mid_getVersion);

  jclass cls_Version = env->GetObjectClass(obj_Version);
  jmethodID mid_toString = env->getMethodID(cls_Version, "toString", "()Ljava/lang/String;");
  jstring str_Version = (jstring) env->CallObjectMethod(obj_version, mid_toString);

  const char *version = env->GetStringUTFChars(str_Version, null);

  write(fds[1], version, env->GetStringUTFLength(str_Version));
  exit(0);
} else if (fork() > 0) { // parent
  close(fds[1]);
  char version[100];
  int size = read(fds[0], version, 100);
  close(fds[0]);
  version[size] = '\0'; 
  // continue initialization
} else { // error }

Instead of calling toString you could of course just grab feature() instead.

Upvotes: 3

Related Questions