Making native calls from the JVM

John Rose, 12/2014 (version 0.11)

Introduction

The JNR runtime is able to form calls to arbitrary C functions without online code generation. It manages this within the bounds of a fixed assembly-coded library of routines called libffi, which provides a small set of data-driven subroutines which can quickly make C function calls of all shapes. They are, of course, coded in assembly.

All JVMs have an internal means for making a wide range of calls to C functions, as part of the standard native function linkage mechanism used by JNI. Mixed-mode JVMs generally have at two ways to make these calls, one for compiled code and one for the interpreter. These calls are somewhat less general than those formed by libffi, since they only apply to JNI functions, and JNI functions only process a limited set of argument and return types. For example, libffi can manage struct and vector arguments and return values, which is something JNI has no need for.

Why doesn’t the JVM use something like libffi to form all of its native calls? A minimal JVM implementation could do so, but since most JVMs can generate code on-line, they can create C call sites as needed. The HotSpot JVM uses assembly-level code generation to create an efficient C call site for each distinct JNI signature.

More crucially, any call sites created by the JVM can be optimized by the JVM, since the JVM “knows” all invariants that must be maintained near out-calls to native C code. For example, back-to-back native calls can sometimes be performed with less book-keeping than isolated native calls, since invariants peculiar to Java execution need not be fully re-asserted between the first and second native call. Also, since the JVM can generate code on-line, it is possible for the JVM to produce more efficient calling sequences than any DLL which is limited to a fixed number of entry points.

This means that, even though libffi is more general than the JVM’s JNI invoker mechanisms, it is worth considering retargeting the JVM’s invoker mechanisms to the wider range of C function call signatures. This will let us adjust JNR (or an equivalent native interconnect) to be more tightly integrated with the JVM.

This note explores some options for doing so.

Method handles

Since Java SE 7, free-floating, weakly-typed behaviors can be represented at the JVM level as method handles. Any Java method reference can be converted to a method handle which provides equivalent access to the method, but without any commitment on the caller to naming the method or its containing class.

For example, a static method C::m can be converted to a method handle using the factory method MethodHandles.Lookup.findStatic. The factory method verifies the caller’s permission to access C and m and issues a method handle which refers to the entry point of m.

Assuming the method takes an int and returns a float, the call would look like this:

MethodHandle mh = lookup.findStatic(C.class, "m",
  methodType(float.class, int.class));

Later on, when the method handle is invoked, the only check which is performed is that the caller and callee agree on the argument and return types for the method. The caller does know or care about the names of C or m. The caller does not even know how the method is called; the method handle simply gives a capability for performing an invocation. Here is a sample invocation:

float f = (float) mh.invoke(42);

This pattern could be directly extended to C functions, simply by adding an appropriate factory method findNative which is able to find a named entry point within a given DLL (or DLL search path).

MethodHandle mh = lookup.findNative(dll, "m",
  methodType(float.class, int.class));

Note that the permission checks for m and the containing DLL (if any) are completely different from those performed for access to a Java method m. In fact, even if the JVM could enforce some appropriate access check for findNative, additional checks are necessary for most C functions, since C calling sequences are less safe than Java calling sequences.

The factory method pattern could be modified to obtain a function pointer invocation primitive:

MethodHandle mh = MethodHandles.nativeInvoker(
  methodType(float.class, int.class));
long mAddr = lookup.findNativeAddress(dll, "m");
float f = (float) mh.invoke(mAddr, 42);

This modified pattern relies on the fact that any C function exported by a DLL can be invoked namelessly via a function pointer whose value is the virtual address of the function’s entry point. The advantage of this pattern is that it separates the issues of invocation from lookup.

Unsafe at any speed

We touched on the fact that C function calls are significantly less safe than Java method calls. Generally speaking, a C function must be carefully wrapped with suitable adapter logic in order to make it as safe to use as other Java methods. This wrapper in general requires some human intervention. In current Java systems, the wrapping is performed in C code by the JNI function entry point called by the JVM.

If we are to open up the JVM to calling any C functions, we need to make a clear distinction between “raw” and “wrapped” calling sequences. The former could cause the JVM to malfunction if used wrong, while the latter are safe for general use. (Perhaps there are conventional Java-style access checks to restrict how general that general use really is.) It makes sense to represent the raw and wrapped calling capabilities using completely separate method handles (or other objects, such as lambdas).

This raises two questions: First, how is the initial raw calling capability created? Second, how is it made safe for JVM usage?

The second quesiton is outside of the scope of this note, but the first is easily answered in terms of the patterns above. Simply move the methods findNative, nativeInvoker, and findNativeAddress into a restricted system-level class, such as sun.misc.Unsafe.

Down the rabbit hole

As it happens, the implementation of method handles (in HotSpot at least) is also factored into safe and unsafe layers. The method handle API is completely type-safe and access checked, but it is implemented on top of a loosely typed framework of low-level behaviors and raw method pointers. (The behaviors are “lambda forms” and the raw pointers are “member names”.) A seemingly atomic method handle for a method like C::m is really composed of a behavior that performs a type signature check, shuffles arguments, and makes an untyped call to the entry point of m.

In the above example call to m via mh, the last thing that happens before JVM-managed assembly code takes over is a peculiar private subprimitive called linkToStatic, as follows:

MemberName mn = ...some constant that refers directly to C::m...;
float res = (float) MethodHandle.linkToStatic(arg1, mn);  //arg1==42
return res;

The non-public method linkToStatic pops a trailing argument, digs a raw method entry point out of it, and jumps into (not calls) the indicated entry point. When the method returns, it returns directly to the above behavior code.

(For technical reasons, it is best to put the magic argument at the end of the argument list, rather than the beginning. The interpreter can easily pop a trailing argument, and compiled calling sequences can generally afford to ignore trailing arguments. This is not the case with leading arguments. If the linkToStatic primitive took its target method in a leading argument, the assembly code would have to move all arguments into position. This turns out to make the primitive much more complex. The HotSpot implementation gets argument motion “for free” by forcing the enclosing behavior to manage that as well as other book-keeping.)

In any case, the JVM code for linkToStatic (and linkToVirtual and a few others) is subtle but compact and relatively simple to manage.

This trick extends readily to native C functions. A new method linkToNative would take one or more trailing arguments that would directly express the intended native call. It would pop those trailing arguments, and jump to the required native entry point. (This account would apply with modification to either the named or function-pointer-based calling sequences sketched above.) This behavior of linkToNative would be hard-coded in JVM assembly code. It would also be optimizable if encountered by the JIT.

Note that Java calling sequences are completely independent from native calling sequences. The JVM defines its own private “ABI” (application binary interface) for Java-to-Java calls. This means that Java-to-C calls in general require some argument shuffling, register saving, and other book-keeping.

Based on the contents of the trailing argument(s), the JVM would perform any necessary argument list shuffling going into and out of the native C function. The precise details of this are complex, and parallel the complexities of the libffi and libjffi libraries on which JNR is built. They can be pushed down into the interpreter and JIT.

At present, we are proposing a single primitive method linkToNative for managing all weakly-typed native calls. The engineering details may call for several such primitive methods, just as there is an existing distinction between linkToStatic and linkToVirtual. Those two methods could be been handled by a single linkToMethod, but it was convenient to split them, since their behaviors were different enough.

A trick tail

So what is the natural type of the trailing argument to linkToNative? Let’s call it a NativeEntryPoint.

A NativeEntryPoint should be something like the JVM’s MemberName, a low-level, untyped reference to two things: a C entry point, and some argument shuffling code.

(The C entry point could be replaced by a function pointer trampoline as noted above. Most of the details are the same either way; the function pointer would be popped from the stack as well as the NativeEntryPoint.)

The argument shuffling code can be complex, and will in general have to be assembled by the JVM (as is the case today with JNI methods). A factory method for NativeEntryPoints would look like this:

NativeEntryPoint nep = makeNativeEntryPoint(
  mAddr, methodType(float.class, int.class), ...options...);

Inside a wrapping method handle, the primitive call would look like this:

float res = (float) MethodHandle.linkToNative(arg1, nep);

The JVM would take full control over the contents of the entry point descriptor, and would not need to share access to them with any other code.

More strange arguments

Some Java arguments would never be passed directly to linkToNative or its enclosing raw native method handle. For example, the Java type java.lang.String is meaningless to C code. In general, Java references cannot be safely passed to C code, since C code does not know how to make the necessary handshakes with the JVM’s garbage collection framework. So it is unlikely that makeNativeEntryPoint would be handed method type signatures that include arbitrary Java types.

For the sake of JNI, there could be a convention that occurrences of the type Object would be transformed into jobject handles. These values are useless to most C code, but would be significant to old-style JNI code.

Does to make sense to translate between Java Strings and the C type char*? Probably not, since there is not a clear 1-1 mapping between the two types. This is the sort of mismatch that requires a certain amount of human intervention, and so it belongs in the wrapped version of a C function, not its raw version.

The translation of Java primitive types is straightforward, with the proviso that the corresponding C types might not have the same names.

C pointer types have no direct Java analog. They can be represented in a raw interface using long values, at least until Java is ported to 128-bit ISAs. This leads to the first major hitch: If Java passes a long, how does the system know whether to keep all 64 bits, or to pass the suitable number of address bits (perhaps 32)? Getting this right requires giving some low-level guidance to the assembly code for linkToNative. The “options” argument to makeNativeEntryPoint has to carry this information. For simplicity, the options can be a simple coded string, one character per argument that requires special handling.

Other types might require special handling also. In general, any C type that does not correspond directly to a Java type requires two things, first representation as a low-level Java type (probably a primitive), and second some sort of advertisement in the “options” string to distinguish it from its carrier Java type.

(We could also use special Java classes to represent C types, but this should be done sparingly if at all, to avoid the overhead of boxing and unboxing when Java calls C.)

C can express arguments and return values which are larger than 64 bits. Most system-level ABIs for C transfer such values on the stack “under the hood”, but sometimes they are passed in vector registers. As with libffi, these details are best pushed down into assembly code. But the presence of such values must be advertised, again, in the “options” string (or equivalent means). A Java long might be passed which is the address of a temporary buffer containing the oversized argument or return value. Even better, the Java long should be accompanied by an Object reference, with the usual semantics of an unsafe addressing mode; see Unsafe.getInt for details.

A final example of a strange option is peculiar to the interplay of Java with C. Sometimes JNI functions need to process slices of Java arrays, either for input or output. There are “critical section” primitives in JNI which allow the JVM an option to work with internal pointers into arrays. This is very similar to C conventions for internal pointers, and it is worth trying to connect these conventions in the new calling sequences. The “options” string would advertise an internal pointer, and the corresponding Java arguments would contain a pair of values, an Object and a long which would together denote an address either inside an object or at an absolute unmanaged memory address.

Prototype

There is not yet a prototype of this functionality, although something similar was built in HotSpot around 2005 by John Rose and Ken Russell. It seems likely that we will be experimenting with this soon in Project Panama. Watch this space.