Здравствуйте.
Как вы, наверное, уже догадались, речь пойдет о JNI. Для тех, кто не знает что это, объясняю: JNI (или java native interface) — это такая штука, которая позволяет делать вызовы нативного кода из java машины и наоборот.
Зачем это может потребоваться? Есть несколько причин: необходимость использовать код, который уже написан для нативной платформы, необходимость реализовать что-то такое, что невозможно сделать с помощью одной JVM (например, работа с какими-нибудь специфическими железками), ну и ускорение выполнения критических кусков кода (правда, это весьма спорный момент).
Для начала мы объявляем метод какого-нибудь класса как native. Это будет означать, что JVM при вызове этого метода будет передавать управление нативному коду.
Затем, нам надо загрузить нативную библиотеку. Для этого можно вызвать
Теперь, представим, что у нас есть следующий java класс:
Допустим, теперь, что мы вызываем
Конечно, можно написать их вручную. Но есть более удобный метод:
Мы компилируем класс, а потом используем утилиту javah. После этого у нас появится файл, который называется my_mega_pack_NativeCallsClass.h. Это и есть наш хедер. Выглядит он примерно так:
Их-то нам и надо реализовать. Для начала разберемся с их сигнатурами. env — это интерфейс к виртуальной машине. Все операции с JVM выполняются с помощью него. Позже мы разберем это подробнее. myclass — это идентификатор java класса, у которого есть метод native, отождествленный с этой функцией, то есть в нашем случае это
Нам остается только реализовать эти функции:
Естественно, продолжение будет только в том случае, если это кому-то интересно :)
Как вы, наверное, уже догадались, речь пойдет о 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 */Самое главное здесь — это сигнатуры 2х функций:
#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
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;Запускаем javah и видим, что сигнатуры методов несколько изменились. Теперь они такие:
public class NativeCallsClass
{
static
{
System.loadLibrary("megalib");
}
native public static int inputInt();
native public static void outputInt(int v);
}
JNICALL Java_my_mega_pack_NativeCallsClass_inputInt(JNIEnv *, jclass);jint — это typedef. Фактически он обозначает некоторый примитивный тип (например, int), который соответствует int в java. Как видим, задача оказалась не на много сложнее предыдущей :) Наши функции будут выглядеть так:
JNIEXPORT void JNICALL Java_my_mega_pack_NativeCallsClass_outputInt(JNIEnv *, jclass, jint);
#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++ кода.Естественно, продолжение будет только в том случае, если это кому-то интересно :)