Bass
Bass

Reputation: 5358

Calling native code from an AOT-compiled Java application using CNI

GNU Compiler for Java provides two methods to invoke native code from a Java application.

  1. First, there's a JNI specification authored by Sun Microsystems, and GCJ adheres to this specification.
  2. Second, there's CNI, the Compiled Native Interface implemented by GCJ alone, which is barely documented but allows native methods to be implemented in C++, providing a higher-level API to interact between native code and JVM.

Now, since GCJ also allows ahead-of-time compilation, this gives us the following opportunities to call native code from Java code:

  1. Run the regular JVM with gij, load your main-class which will load the native library using System.loadLibrary(), and invoke the native method. This is the conventional way we are used to when dealing with Sun/Oracle JVMs.
  2. Same as above, but AOT-compile your bytecode into native code using gcj, and dynamically link the resulting executable against the external native library using the -l switch. The call to System.loadLibrary() may be preserved for compatibility, but is actually unnecessary, since the library will already be loaded by the dynamic interpreter long before the JVM is created. There're actually three sub-options:
    • Have the library loaded by the JVM, via System.loadLibrary(), quite similarly to dlopen().
    • Have the library being linked against by ld, via gcj -l. This is more convenient than the loadLibrary() approach, since you can pass the -rpath switch to the linker and avoid the need to manually set LD_LIBRARY_PATH before running the executable.
    • None of the above, but have the library preloaded via LD_PRELOAD.
  3. Statically link the AOT-compiled Java object with the object produced from the native code portion. This completely saves us from the need to call System.loadLibrary() (since there's no library), making the code of of the native method immediately available in the resulting executable.

Since we have two protocols to access native code (JNI and CNI), this doubles the amount of options available (from 3 to 6).

While the approach of CNI with static linking works like a charm, I want to also explore how dynamic linking works together with CNI and AOT compilation, and this fails at link time.

Consider I have a simple main class:

public final class Main {
        static {
                System.loadLibrary("foo");
        }

        private Main() {
                assert false;
        }

        public static native void foo();

        public static void main(final String args[]) {
                foo();
        }
}

The Main.h CNI header produced from it looks like this:

#ifndef __Main__
#define __Main__

#pragma interface

#include <java/lang/Object.h>
#include <gcj/array.h>

extern "Java"
{
    class Main;
}

class Main : public ::java::lang::Object
{

  Main();
public:
  static void foo();
  static void main(JArray< ::java::lang::String * > *);
public: // actually package-private
  static jboolean $assertionsDisabled;
public:
  static ::java::lang::Class class$;
};

#endif // __Main__

... and here's the pretty standard default implementation of the shared library:

#include <iostream>

#include <gcj/cni.h>

#include "Main.h"

void Main::foo() {
        std::cout << "Hello, World!" << std::endl;
}

When linking the object produced from a Java main class with an external library:

gcj -pie -fPIE -save-temps --main=Main -o cni-dynamic-native -L/opt/gcc64/6.5/lib64 -Wl,-rpath=/opt/gcc64/6.5/lib64 -L. -Wl,-rpath='$ORIGIN' -lstdc++ -lgcj -lfoo Main.class

I get the following error:

/usr/bin/ld: Main.o: in function `void Main::main(JArray<java::lang::String*>*)':
Main.java:13: undefined reference to `hidden alias for void Main::foo()'
/usr/bin/ld: Main.o:(.data.rel+0xc0): undefined reference to `hidden alias for void Main::foo()'
collect2: error: ld returned 1 exit status

Indeed, if I examine the Main.o object with nm, there's the undefined symbol:

                 U hidden alias for void Main::foo()

The symbol is present in the shared library (libfoo.so) I'm linking against. It's global (T) in the object file but static (t) in the shared library.

0000000000001180 t hidden alias for void Main::foo()

The same problem is described in this bug report, but, unfortunately, there's no solution. As I said, everything works perfectly when linking two objects statically using CNI or dynamically using JNI (sample code).

So, how do I load a native library into an AOT-compiled executable using the CNI interface? Was CNI ever intended for dynamic linking?

Upvotes: 3

Views: 268

Answers (0)

Related Questions