Pull to refresh

Как подружить Java и C++. Часть первая

Reading time5 min
Views101K
Здравствуйте.

Как вы, наверное, уже догадались, речь пойдет о JNI. Для тех, кто не знает что это, объясняю: JNI (или java native interface) — это такая штука, которая позволяет делать вызовы нативного кода из java машины и наоборот.

Зачем это может потребоваться? Есть несколько причин: необходимость использовать код, который уже написан для нативной платформы, необходимость реализовать что-то такое, что невозможно сделать с помощью одной JVM (например, работа с какими-нибудь специфическими железками), ну и ускорение выполнения критических кусков кода (правда, это весьма спорный момент).

Как работает JNI

Допустим, у нас есть какой-то java класс из которого надо вызвать метод, написанный на c++ и находящийся в динамически связываемой библиотеке (например, в windows это будет dll). Что мы должны для этого сделать?

Для начала мы объявляем метод какого-нибудь класса как native. Это будет означать, что JVM при вызове этого метода будет передавать управление нативному коду.

Затем, нам надо загрузить нативную библиотеку. Для этого можно вызвать System.loadLibrary(String), которая принимает в качестве параметра имя библиотеки. После этого вызова библиотека будет загружена в адресное пространство JVM.

Теперь, представим, что у нас есть следующий java класс:
package my.mega.pack;
 
public class NativeCallsClass
{
    static
    {
       System.loadLibrary("megalib");
    }
 
    native public static void printOne();
    native public static void printTwo();
}
Здесь мы, для удобства, вынесли loadLibrary() в static область класса.

Допустим, теперь, что мы вызываем NativeCallsClass.printOne(). Тогда JVM будет искать в библиотеках метод со следующим именем: Java_my_mega_pack_NativeCallsClass_printOne(...).

Объявление JNI функций в C++

Мы написали класс на java, у которого есть методы, помеченные как native. Теперь нам надо создать хедеры с объявлениями функций C++, которые мы хотим вызывать.

Конечно, можно написать их вручную. Но есть более удобный метод:

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

Мы компилируем класс, а потом используем утилиту javah. После этого у нас появится файл, который называется my_mega_pack_NativeCallsClass.h. Это и есть наш хедер. Выглядит он примерно так:
/* 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
Самое главное здесь — это сигнатуры 2х функций: Java_my_mega_pack_NativeCallsClass_printOne(JNIEnv *env, jclass myclass) и Java_my_mega_pack_NativeCallsClass_printTwo(JNIEnv *env, jclass myclass).

Их-то нам и надо реализовать. Для начала разберемся с их сигнатурами. env — это интерфейс к виртуальной машине. Все операции с JVM выполняются с помощью него. Позже мы разберем это подробнее. myclass — это идентификатор java класса, у которого есть метод native, отождествленный с этой функцией, то есть в нашем случае это NativeCallsClass. Обратите внимание, что jclass в качестве второго параметра передается тогда, когда метод объявлен как static. Если бы он был обычным методом, то нам бы передавался jobject, который бы идентифицировал объект, метод которого мы вызвали (фактически это аналог this).

Нам остается только реализовать эти функции:
    #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;
    }

Передаем данные в нативный код и обратно

Давайте теперь реализуем более сложное поведение. Пусть у нас будет 2 метода: inputInt и outputInt. Один из них будет считывать число с консоли, а второй — выводить. Наш java класс будет выглядеть так:
package my.mega.pack;
 
public class NativeCallsClass
{
    static
    {
        System.loadLibrary("megalib");
    }
 
    native public static int inputInt();
    native public static void outputInt(int v);
}
Запускаем javah и видим, что сигнатуры методов несколько изменились. Теперь они такие:
JNICALL Java_my_mega_pack_NativeCallsClass_inputInt(JNIEnv *, jclass);
JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_outputInt(JNIEnv *, jclass, jint);
jint — это typedef. Фактически он обозначает некоторый примитивный тип (например, int), который соответствует int в java. Как видим, задача оказалась не на много сложнее предыдущей :) Наши функции будут выглядеть так:
#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;
}

Подводим итог

Итак, в первой части мы рассмотрели, как работает JNI, как писать java классы, с помощью которых можно осуществлять вызовы нативного когда и как писать C++ функции, вызываемые через JNI. В следующей части (или частях) мы рассмотрим взаимодействие с JVM из C++ кода, работу с классами, объектами полями и методами, создание proxy классов java, которые бы представляли C++ классы и запуск JVM из C++ кода.

Естественно, продолжение будет только в том случае, если это кому-то интересно :)

Tags:
Hubs:
Total votes 41: ↑39 and ↓2+37
Comments53

Articles