📜 ⬆️ ⬇️

How to make friends with Java and C ++. Part one

Hello.

As you probably already guessed, it will be about JNI. For those who do not know what it is, I explain: JNI (or the java native interface) is such a thing that allows you to make calls to native code from a java machine and vice versa.

Why this may be required? There are several reasons: the need to use code that is already written for the native platform, the need to implement something that cannot be done with a single JVM (for example, working with some specific pieces of hardware), and of accelerating the execution of critical pieces of code (though This is a very controversial point.)
')

How JNI works

Suppose we have some java class from which we need to call a method written in c ++ and located in a dynamically linked library (for example, in windows it will be a dll). What should we do for this?

To begin with, we declare a method of some class as native. This will mean that the JVM, when calling this method, will transfer control to the native code.

Then, we need to load the native library. To do this, you can call System.loadLibrary(String) , which takes the name of the library as a parameter. After this call, the library will be loaded into the JVM address space.

Now, imagine that we have the following java class:
package my.mega.pack ;

public class NativeCallsClass
{
static
{
System . loadLibrary ( "megalib" ) ;
}

native public static void printOne ( ) ;
native public static void printTwo ( ) ;
}
Here we, for convenience, carried loadLibrary() in static area of ​​a class.

Suppose now that we call NativeCallsClass.printOne() . Then the JVM will search the libraries for a method with the following name: Java_my_mega_pack_NativeCallsClass_printOne(...) .

JNI function declaration in C ++

We wrote a class in java, which has methods that are marked as native. Now we need to create headers with declarations of C ++ functions that we want to call.

Of course, you can write them manually. But there is a more convenient method:

 javac -d bin / src / my / mega / pack / NativeCallsClass.java
 cd bin
 javah my.mega.pack.NativeCallsClass

We compile the class, and then use the javah utility. After that, we will have a file called my_mega_pack_NativeCallsClass.h. This is our header. It looks like this:
/ * DO NOT EDIT THIS FILE - it is machine generated * /
#include <jni.h>
/ * Header for class my_mega_pack_NativeCallsClass * /

#ifndef _Included_my_mega_pack_NativeCallsClass
#define _Included_my_mega_pack_NativeCallsClass
#ifdef __cplusplus
extern "C" {
#endif
/ *
* Class: my_mega_pack_NativeCallsClass
* Method: printOne
* Signature: () V
* /
JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_printOne
( JNIEnv * , jclass ) ;

/ *
* Class: my_mega_pack_NativeCallsClass
* Method: printTwo
* Signature: () V
* /
JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_printTwo
( JNIEnv * , jclass ) ;

#ifdef __cplusplus
}
#endif
#endif
The most important thing here is the signatures of 2 functions: Java_my_mega_pack_NativeCallsClass_printOne(JNIEnv *env, jclass myclass) and Java_my_mega_pack_NativeCallsClass_printTwo(JNIEnv *env, jclass myclass) .

We need to implement them. First, let's deal with their signatures. env is an interface to the virtual machine. All operations with JVM are performed using it. Later we will analyze this in more detail. myclass is an identifier of a java class that has a native method identified with this function, that is, in our case, this is NativeCallsClass . Note that the jclass as the second parameter is passed when the method is declared as static. If it were an ordinary method, then a jobject would be passed to us, which would identify the object whose method we called (in fact, this is an analogue of this).

We can only implement these functions:
#include <iostream>
#include "my_mega_pack_NativeCallsClass.h"


JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_printOne ( JNIEnv * env, jclass myclass )
{
std :: cout << "One" << std :: endl ;
}

JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_printTwo ( JNIEnv * env, jclass myclass )
{
std :: cout << "Two" << std :: endl ;
}

We transfer data to the native code and back

Let's now implement more complex behavior. Let us have 2 methods: inputInt and outputInt. One of them will read the number from the console, and the second will output. Our java class will look like this:
package my.mega.pack ;

public class NativeCallsClass
{
static
{
System . loadLibrary ( "megalib" ) ;
}

native public static int inputInt ( ) ;
native public static void outputInt ( int v ) ;
}
Run javah and see that the method signatures have changed a bit. Now they are:
JNICALL Java_my_mega_pack_NativeCallsClass_inputInt ( JNIEnv * , jclass ) ;
JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_outputInt ( JNIEnv * , jclass, jint ) ;
jint is typedef. In fact, it denotes some primitive type (for example, int), which corresponds to an int in java. As you can see, the task was not much more complicated than the previous one :) Our functions will look like this:
#include <iostream>
#include "my_mega_pack_NativeCallsClass.h"

JNIEXPORT jint JNICALL Java_my_mega_pack_NativeCallsClass_inputInt ( JNIEnv * env, jclass myclass )
{
int ret;

std :: cin >> ret;

return ret;
}

JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_outputInt ( JNIEnv * env, jclass myclass, jint v )
{
std :: cout << v << std :: endl ;
}

Summarize

So, in the first part, we looked at how JNI works, how to write java classes, with which you can make native calls when and how to write C ++ functions called via JNI. In the next part (or parts), we will look at interacting with JVM from C ++ code, working with classes, objects, fields and methods, creating java proxy classes that represent C ++ classes, and running JVM from C ++ code.

Naturally, the continuation will be only if it is interesting to someone :)

Source: https://habr.com/ru/post/49660/


All Articles