Мы можем написать всю программу целиком на Java, учитывая большие возможности языка, большое количество досьупных фреймворков и библиотек на этом языке. Но бывают ситуации, когда может потребоваться одновременно вместе с кодом на Java использовать также код на другом языке, в частности, на C/C++. Например, если приложению требуется доступ к системным функциям или устройствам, недоступным через платформу Java, либо если уже имеется значительный объем протестированного и отлаженного кода на другом языке, который надо интегрировать в программу на языке Java. Поэтому рассмотрим как мы можем взаимодействовать в программе на Java с нативным кодом на C/C++
Начиная уже с версии Java 1.1 платформа Java имеет специальный API для взаимодействия с нативным кодом C, который называется Java Native Interface (JNI). Начиная с версии Java 20 был добавлен новый, более простой и удобный API - Foreign Functions and Memory API (FFM API). Тем не менее JNI по прежнему может использоваться, особенно на старых версиях платформы. Поэтому вначале вкратце рассмотрим, базовые моменты JNI
Общий процесс связывания нативного метода с Java-программой с помощью JNI выглядит следующим образом:
Объявляем нативный метод-обертку для функции Си в Java-классе.
С помощью команды javac -h генерируем заголовочный файл с объявлением метода на языке C.
Определяем код для нативного метода на языке C.
Компилируем код на Си в разделяемую библиотеку.
Загружаем эту библиотеку в программе на Java
Рассмотрим по шагово весь процесс.
При использовании JNI для функции языка C/C++, которую мы хотим использовать, необходимо объявить прокси-метод с помощью ключевого слова native. Это ключевое слово сообщает компилятору, что метод будет определен извне. Нативные методы не содержат кода Java, и за заголовком метода сразу следует точка с запятой. Поэтому объявления нативных методов выглядят аналогично объявлениям абстрактных методов.
Предположим, есть функция на языке C, которую необходимо использовать в коде на языке Java. Для наглядности возьмем стандартную библиотечную функцию printf с одним аргументом и для ее применения определим файл HelloNative.java со следующим кодом на языке Java:
class HelloNative {
public static native int printf(String str);
}
Здесь определен класс HelloNative с одним нативным методов, имя которого соответствует названию функции printf. Нативные методы могут быть как статическими, так и нестатическими. В данном случае нативный метод также объявлен как статический.
Далее нам надо определить заголовочный файл на языке С. Для этого необходимо скомпилироть данный класс с флагом -h, указав каталог, в который должны быть помещены заголовочные файлы:
javac -h . HelloNative.java
В итоге будет скомпилирован файл HelloNative.h со следующим кодом:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
#ifndef _Included_HelloNative
#define _Included_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloNative
* Method: printf
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_HelloNative_printf
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
Данный файл содержит объявление функции Java_HelloNative_printf. Причем обратите внимание, как называется функция: "Java_HelloNative_printf", а не просто "printf".
В JNI имена функций строго регламентированы: сначала а добавляется префикс "Java", затем добавляется пакет класса (если пакет вложенный, например, "com.metanit.test", то в полном названии пакета точка заменяется на прочерк - "com_metanit_test") и
в конце добавляется название класса и название функции через подчеркивание. То есть получается: Java_ + ИмяКласса_ + ИмяМетода. Значит, в Java этот метод объявлен в классе HelloNative как native int printf(String s)
В принципе мы могли бы и вручную прописать подобный файл, но всегда есть вероятность ошибиться, поэтому лучше использовать автогенерацию.
Далее определим еще один файл - HelloNative.c со следующим кодом на языке С:
#include "HelloNative.h"
#include <stdio.h>
JNIEXPORT jint JNICALL Java_HelloNative_printf (JNIEnv *env, jclass cl, jstring jstr) {
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
int numchars = printf(cstr);
(*env)->ReleaseStringUTFChars(env, jstr, cstr);
return numchars;
}
Рассмотрим подробно, что происходит в каждой строке:
#include "HelloNative.h
Прежде всего подключаем наш автосгенерированный заголовочный файл, чтобы Java смогла определить функцию при ее вызове.
#include <stdio.h>
Подключение стандартного заголовочного файла языка С, который содержит прототипы функций для работы с подсистемой ввода-вывода
JNIEXPORT jint JNICALL Java_HelloNative_printf
JNIEXPORT и JNICALL - это специальные макросы, которые гарантируют, что функция будет видна Java-машине (JVM) и будет использовать правильное соглашение о вызовах.
Между макросами указан возвращаемый тип - jint, которому в языке Java соответствует int. И после макросов идет имя функции - Java_HelloNative_printf и ее определение.
(JNIEnv *env, jclass cl, jstring jstr)
Каждая нативная функция получает параметр env, который необходим для доступа к API языка Си для нативных вызовов.
Параметр cl - это дескриптор Java-класса (для нестатических методов этому параметру передается дескриптор объекта).
Параметр jstr типа jstring - собственно то значение (строка), которое будет передаваться при вызове данного метода в коде Java.
Далее идет собственно код метода. И начинается он с получения строки (GetStringUTFChars)
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
В Java строки хранятся в формате Unicode, а функции C (как printf) работают с массивами char.
Эта строка конвертирует Java-строку (jstring) в стандартную C-строку (UTF-8).
Вывод на экран
int numchars = printf(cstr);
Здесь вызывается стандартная функция языка C printf. Она выводит строку в консоль. Переменная numchars сохраняет количество напечатанных символов.
Освобождение памяти (ReleaseStringUTFChars)
(*env)->ReleaseStringUTFChars(env, jstr, cstr);
Это критически важный шаг. Поскольку JNI выделил память под cstr, ее необходимо освободить вручную, чтобы избежать утечек памяти.
Виртуальная машина должна знать, когда мы закончили использовать строку, чтобы она могла выполнить сборку мусора. Поскольку сборщик мусора работает в отдельном потоке и может прерывать выполнение нативных методов,
то необходимо вызвать функцию ReleaseStringUTFChars.
Возврат значения
return numchars;
Количество напечатанных символов возвращается обратно в Java-код.
Итак, у нас есть заголовочный файл и файл кода C, и теперь нам надо скомпилировать весь этот нативный код на языке C в динамически загружаемую библиотеку. Конкретные подробности зависят от выбранного компилятора. В данном случае посмотрим, как это сделать, используя 2 распространенных компилятора.
Так, при использовании компилятора GCC на Linux команда компиляции будет выглядеть следующим образом:
gcc -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -shared -o libHelloNative.so HelloNative.c
Здесь стоит учитывать, что переменная окружения $JAVA_HOME должна быть определена и указывать на папку установки java. Соответственно путь
$JAVA_HOME/include будет указывать на папку с заголовочными файлами, которые необходимы для компиляции библиотеки на C, в частности, файл jni.h, который подключался выше в одном из файлов.
Тем не менее эта переменная может быть и не установлена, в этом случае (если нет желания устанавливать переменную $JAVA_HOME) можно передать конретный путь к папке, где располагаются заголовочные файлы.
Например, если java локально установлена в каталоге "/usr/java/jdk-25", то соответственно нам надо прописать
gcc -fPIC -I /usr/java/jdk-25/include -I /usr/java/jdk-25/include/linux -shared -o libHelloNative.so HelloNative.c
В любом случае после выполнения этой команды в текущей папке должен появиться файл библиотеки libHelloNative.so (название самой библиотеки - "HelloNative" - без префикса "lib" и расширения файла)
Если текущая ОС - Windows и компилятор - VisualC++ от Microsoft, то там мы могли бы выполнить следующую команду
cl -I %JAVA_HOME%\include -I %JAVA_HOME%\include\win32 -LD HelloNative.c -FeHelloNative.dll
В этом случае в текущей папке появится файл "HelloNative.dll" ((название самой библиотеки также "HelloNative").
Для загрузки библиотеки нам надо добавить в код программы вызов метода System.loadLibrary, в который передается название библиотеки. Чтобы гарантировать загрузку библиотеки виртуальной машиной до первого использования класса, можно использовать блок статической инициализации. Например, определим в текущей папке следующий файл Program.java:
class Program {
public static void main(String[] args) {
HelloNative.printf("Hello METANIT.COM\n");
}
static {
System.loadLibrary("HelloNative");
}
}
Таким образом, в папке проекта у нас будут следующие файлы:
HelloNative.java: класс с нативным методом-оберткой для функции языка Си
HelloNative.class: скомпилированный класс
HelloNative.h: автосгенерированный заголовочный файл
HelloNative.c: реализация функции из заголовочного файла
libHelloNative.so/HelloNative.dll: скомпилированная библиотека
Program.java: основной файл программы
Скомпилируем программу на Java стандартной командой
javac Program.java
Для запуска программы прежде всего нам надо передать утилите java дополнительную опцию, которая как-бы разрешает использование нативного кода в программе на Java. Если модули не используются, то программа запускается следующим образом:
java --enable-native-access=ALL-UNNAMED ...
В модульной же программе необходимо перечислить все модули, для которых разрешен нативный доступ:
java --enable-native-access=module1, module2 ...
При запуске на Linux также необходимо добавить текущий каталог в путь к библиотекам, чтобы java нашла нашу библиотеку. Для этого можно установить переменную среды LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
Здесь с помощью точки указываем текущую папку. Либо можно использовать системное свойство java.library.path:
java --enable-native-access=ALL-UNNAMED -Djava.library.path=. Program
Например:
eugene@Eugene:/workspace/java/test$ java --enable-native-access=ALL-UNNAMED -Djava.library.path=. Program Hello METANIT.COM eugene@Eugene:/workspace/java/test$