This is what I think of JNI in Java.
A managed runtime which implements a virtual machine, has to abstract two types of programming needs, in one of many such abstractions - programming needs in the user space, and those in the kernel space. While the programming needs in the user space such as assignments, arithmetic operations, method invocations etc. are abstracted in well defined bytecodes which theoretically runs on a stack based virtual machine, abstracting the programming needs in the kernel space is not very easy and straightforward, and require special treatment.
Kernel services are exposed to the user world through APIs called system calls, and pluralities of platforms expose multitudes of methods which differ in their nomenclature, input, service they provide, and the side effect they cause in the execution environment.
There are three approaches to abstracting the programming needs in the kernel space:
1. Define one byte code for each system call in each platform. This approach has many drawbacks: i) The number of byte codes will bloat up beyond maintainable limit, ii) will cause the size of the bytecode to cross one byte, ii) The programmer should have the precise knowledge about the underlying system, iv) the program will no longer be platform independent, as it will contain platform specific conditionals and considerations.
2. Define one byte code (such as invokenative) for all the system calls in all the platforms. This has the last two disadvantages mentioned above - the platform independence is lost, the programmer should keep track of the platform differences and code accordingly. Also there are other logistical issues such as arrival of new bytecode combinations (such as invokenativevirtual, invokenativestatic etc. based on the access type of the native method).
3. Abstract all the kernels and define generic APIs which meet the system needs of the program (the existing approach) and manage the platform dependent details from within. This includes mapping each high level APIs to one or more system calls, preparing their input, and manage the call dispatch. It is not possible to do these procedures in an interpreter, as for different methods these activities will differ. And moreover, it is tiresome to identify custom invocation sequences for each native calls based on the name and signature of the generic API under execution. At this point it makes sense to have custom native (programs which compile into native code and run in an un-managed runtime such as the machine itself) wrappers around the system (or library) calls, and define a protocol for invoking these wrappers from the Java APIs. All of the (operating) system abstractions in the JRE (file system, console, networking, graphics, process management, threading, synchronization, etc.) have native interfaces wrapping around their system counter parts, and manage the service invocation between the system layer and the java layer.
This protocol, which is used to communicate between java APIs and their native back-ends which interface with the underlying system, is called JNI protocol. JNI is a necessity for the Java language and the virtual machine to achieve kernel abstraction and thereby achieve platform neutrality. As a matter of fact, not all the system calls are abstracted in this way. Also there could be scenarios where programmer would need to perform tasks natively, or avail computation service from pre-existing native libraries. So it makes perfect sense to open up JNI as a language feature for hybrid programming.
JNI is not necessarily a Java feature, rather an indispensable part of the design of the language runtime. A crucial internal capability which implements a subroutine linkage channel between the abstract and the real machine. The bedrock infrastructure which underpins the platform independent programming model, the by-product of which was exposed outside under the pretext of a language feature.