Search code examples
javacjarjava-native-interface

JNI: packaging code into self-contained binaries


I am currently working on a hybrid java/C project. Because I like to reduce runtime obstacles as much as possible, I tend to integrate resources (pictures) in my binary, thus eliminating errors such as "picture not found", which might occur when the user moves my binary around. I know that I can include java files into my C executable (via, e.g. C23 #embed or xxd). Normally, binaries amounting to several MBs are not a issue anymore on modern machines. I can then load the classes in my C program via (a simple foreach loop at the beginning of the program):

jclass DefineClass(JNIEnv *env, const char *name, jobject loader,
const jbyte *buf, jsize bufLen);

My question is: how can I do that for whole jar files? I would like to include, e.g. the selenium API in my binary? Is it a sufficient unzip every jar file, include it in the binary, and DefineClass them? Are there any obstacles that I might be missing?

EDIT: My entry point is on the C-Side. In fact, the whole application does not contain any Java code, it just uses some APIs from Selenium via the JNI.


Solution

  • As a completely different approach, why not abuse the ZIP file format underlying JARs? ZIP files store their metadata at the end of the file, which means that you can simply append the JAR file to your binary, run an embedded JVM with the binary file added to its own classpath. Adding the entirety of selenium is then just a matter of repackaging everything into a single uberjar.

    As a simple example, take Main.java:

    class Main {
        public static void main(String[] args) {
            System.out.println("Hello from java");
        }
    }
    

    and jni.cpp:

    #include <jni.h>
    #include <string>
    
    int main(int argc, char **argv) {
        JavaVM *jvm;       /* denotes a Java VM */
        JNIEnv *env;       /* pointer to native method interface */
        JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
        JavaVMOption* options = new JavaVMOption[1];
        std::string option = "-Djava.class.path=";
        option += argv[0];
        options[0].optionString = const_cast<char *>(option.c_str());
        vm_args.version = JNI_VERSION_1_6;
        vm_args.nOptions = 1;
        vm_args.options = options;
        vm_args.ignoreUnrecognized = false;
        /* load and initialize a Java VM, return a JNI interface
         * pointer in env */
        JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
        delete[] options;
        /* invoke the Main.test method using the JNI */
        jclass cls = env->FindClass("Main");
        jmethodID mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
        env->CallStaticVoidMethod(cls, mid);
        /* We are done. */
        jvm->DestroyJavaVM();
    }
    

    You can then do:

    $ make jni
    $ javac Main.java
    $ jar cf code.jar Main.java
    $ cat jni code.jar > jni-combined
    $ ./jni-combined
    Hello from java