Синопсис
В этом посте поговорим о вызовах функции библиотеки c++ из java класса. Это довольно интересная тема, я достаточно много времени потратил чтобы понять как осуществляется взаимодействие кода выполняющегося в виртуальной машине джава с нативным кодом.
В этом посте, тестировать взаимодействие будем на платформе Windows, а в следующем сделаем тоже самое, но только в Linux.
Похожие посты
Что пилим
Идея такая, допустим мы создаем джава приложение которое будет делать некое вычисления по каким-то формулам. Допустим по ряду причин мы не можем сами написать функции делающие вычисления по этим формулам (нам лень), но мы знаем, что кто-то когда-то, когда еще не было джавы, что-то подобное уже делал. Порывшись в яндексе мы нашли и выкачали библиотеку сишных функций, которые делают именно то, что нам нужно. И вот теперь у нас встал вопрос, как из джава приложения делать колы к функциям из сишной dll библиотеки.
Для этого существует JNI — Java Native Interface, это такой механизм джавы, который позволяет виртуальной джава машине вызывать нативный код.
Готовый проект можно взять с gitHub по ссылке: https://github.com/dev-blogs/JNIProject_windows
Архитектура проекта
На диаграмме ниже представлена схема интеракции джава приложения и сишной dll либы. Сначала запускается джава приложение com.task.JNIProject, которое стартует с вызова метода main. Далее метод main обращается к нативному методу sum. Этот нативный метод не настоящий сишный метод, через него джава машина пробрасывает вызов к функции Java_com_task_JNIProject_sum() (такое страшное сгенеренное название пока оставим без комментариев) которая находится в библиотеке JNIProject.dll, так вот, код этой функции и есть вызываемый нативный сишный код. А вот функция Java_com_task_JNIProject_sum вызывает уже функцию sum из библиотеки MathExpressions.dll:

Библиотека JNIProject.dll в этой цепочке, это посредник, предназначенный для связывания джава приложения с целевой либой, той которая предоставляет математические операции, нужные нашему джава приложению. На прямую джава приложение не может обращаться к произвольной сишной библиотеке, потому что сама по себе та библиотека не предназначена, чтобы она вызывалась из других джава приложений, из джава приложений возможны вызовы только тех сишных файлов, которые скомпилированы с использованием заголовочного файла jni.h, а так же сгенеренного джава средствами хэдера, который нужно включить в тот сишный цпп файл, к которому будет обращаться джава приложение.
so vs dll
Динаически разделяемая библиотека (Dynamic shared library) представляет из себя отдельную независимую либу (ибо она сама представляет из себя зависимость 🙂 ), в которую вынесена некая общая, для некоторых приложений, функциональность. Это делается для того, чтобы облегчить приложение (в весе). Например в разделяемой либе можно держать функции ввода-вывода, математические функции, в общем любые функции, которые могут пригодиться в каком-нибудь множестве приложений. В таком случае все эти часто пригождающиеся функции нет смысла хранить вместе с самими прилжожениями. Если, например, какая-нибудь либа из часто требуемых функций размером, скажем, в 1024 килобайт, будет поставляться с каждым приложением, и таких приложений будет, например, 1024 штук, то только для библиотек потребуется на диске 1024×1024=1Гб места, вместо того, чтобы все 1024 приложений юзали одну либу размером в метр. Недостаток здесь в том, что приложение не сможет запуститься в энвайренменте не поддерживающей все необходимые ему либы или поддерживающей, но не тех версий.
Структура проекта
Ниже представлены две структуры проекта, до компиляции и после, точнее те каталоги, которые добавятся после компиляции. К тем каталогам, которые должны быть видимы для джава машины во время компиляции, другими словами те которые должны быть добавлены в класспас, я промаркировал classpath (compile), а те которые должны быть видимы джава машиной во время выполнения, промаркированы classpath (runtime):
JNIProject <--classpath (compile) ├─src │ ├─java │ │ └─com │ │ └─task │ │ └─JNIProject.java │ └──c │ ├─com_task_JNIProject.h (generated) │ ├─JNIProject.cpp │ ├─MathExpressions.h │ └─mathExpressions.cpp └──build.bat
После того как мы запустим скрипт build.bat, в корне проекта появится еще два каталога build_java и build_c:
─build-java <--classpath (runtime) └─com └─task └─JNIProject ─build-c <--java.library.path (runtime) ├─com_task_JNIProject.h ├─JNIProject.dll ├─MathExpressions.h └─MathExpressions.dll
Запускаться будет приложение com.task.JNIProject которое находится в каталоге build_java (туда же установлен класспас. Напоминаю, все каталоги, которые расположены ниже класспаса по дереву иерархии, относятся к имени класса) которое будет обращаться к сишной библиотеке JNIProject.dll из каталога build_c (путь к этому каталогу указан в параметре утилиты java java.library.path), которая, в свою очередь, будет обращаться к библиотеке MathExpressions.dll. А теперь добавим все необходимые классы.
Java код
HelloJNI.java
package com.task; public class JNIProject { static { System.loadLibrary("JNIProject"); // Load native library at runtime // hello.dll (Windows) or libhello.so (Unixes) } private native int initialize(); private native int sum(int a, int b); private native int difference(int a, int b); private native int multiply(int a, int b); private native int divide(int a, int b); public static void main(String [] args) { int a = 3; int b = 2; JNIProject jniProject = new JNIProject(); jniProject.initialize(); System.out.println(a + " + " + b + " = " + jniProject.sum(a, b)); System.out.println(a + " - " + b + " = " + jniProject.difference(a, b)); System.out.println(a + "*" + b + " = " + jniProject.multiply(a, b)); System.out.println(a + "/" + b + " = " + jniProject.divide(a, b)); } }
C++ код
Сгенеренный файл
com_task_JNIProject.h (относится к JNIProject.dll)
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_task_JNIProject */ #ifndef _Included_com_task_JNIProject #define _Included_com_task_JNIProject #ifdef __cplusplus extern "C" { #endif /* * Class: com_task_JNIProject * Method: initialize * Signature: ()I */ JNIEXPORT jint JNICALL Java_com_task_JNIProject_initialize (JNIEnv *, jobject); /* * Class: com_task_JNIProject * Method: sum * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_task_JNIProject_sum (JNIEnv *, jobject, jint, jint); /* * Class: com_task_JNIProject * Method: difference * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_task_JNIProject_difference (JNIEnv *, jobject, jint, jint); /* * Class: com_task_JNIProject * Method: multiply * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_task_JNIProject_multiply (JNIEnv *, jobject, jint, jint); /* * Class: com_task_JNIProject * Method: divide * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_task_JNIProject_divide (JNIEnv *, jobject, jint, jint); #ifdef __cplusplus } #endif #endif
JNIProject.cpp (относится к JNIProject.dll)
#include <windows.h> #include "com_task_JNIProject.h" HINSTANCE h; int (* psum) (int, int); int (* pdifference) (int, int); int (* pmultiply) (int, int); int (* pdivide) (int, int); JNIEXPORT jint JNICALL Java_com_task_JNIProject_initialize(JNIEnv *env, jobject thisObj) { h = LoadLibrary("C:\\practice\\JNI\\JNIProject\\build_c\\MathExpressions.dll"); if (!h) { printf("%d Error - can't find library MathExpressions.dll\n", h); return 1; } psum=(int (*)(int, int)) GetProcAddress(h, "sum"); pdifference=(int (*)(int, int)) GetProcAddress(h, "difference"); pmultiply=(int (*)(int, int)) GetProcAddress(h, "multiply"); pdivide=(int (*)(int, int)) GetProcAddress(h, "divide"); if (!psum) { printf("Error psum\n"); return 2; } if (!pdifference) { printf("Error pdifference\n"); return 2; } if (!pmultiply) { printf("Error pmultiply\n"); return 2; } if (!pdivide) { printf("Error pdivide\n"); return 2; } } JNIEXPORT jint JNICALL Java_com_task_JNIProject_sum(JNIEnv *env, jobject thisObj, jint a, jint b) { return psum(a, b); } JNIEXPORT jint JNICALL Java_com_task_JNIProject_difference(JNIEnv *env, jobject thisObj, jint a, jint b) { return pdifference(a, b); } JNIEXPORT jint JNICALL Java_com_task_JNIProject_multiply(JNIEnv *env, jobject thisObj, jint a, jint b) { return pmultiply(a, b); } JNIEXPORT jint JNICALL Java_com_task_JNIProject_divide(JNIEnv *env, jobject thisObj, jint a, jint b) { return pdivide(a, b); }
MathExpressions.h (относится к MathExpressions.dll)
extern "C" { int sum(int a, int b); int difference(int a, int b); int multiply(int a, int b); int divide(int a, int b); }
mathExpressions.cpp (относится к MathExpressions.dll)
#include "MathExpressions.h" int sum(int a, int b) { return a + b; } int difference(int a, int b) { return a - b; } int multiply(int a, int b) { return a*b; } int divide(int a, int b) { return a/b; }
Сборка проекта
build.bat
set BUILD_JAVA=build_java set BUILD_C=build_c set SRC_JAVA=src\java set SRC_C=src\c set JAVA_NAME=JNIProject set HEADER_NAME=%JAVA_NAME% set DLL_NAME=JNIProject set HEADERS_FROM_JAVA_DIR=C:\bin\Java\32\jdk1.7.0_71\include :: clear %BUILD_JAVA% directory rd /s /q %BUILD_JAVA% if exist %BUILD_JAVA% rd /s /q %BUILD_JAVA% :: make %BUILD_JAVA% and %GENERATED_DIR% directories mkdir %BUILD_JAVA% :: clear %BUILD_C% directory rd /s /q %BUILD_C% if exist %BUILD_C% rd /s /q %BUILD_C% ::make %BUILD_C% and %GENERATED_DIR% directories mkdir %BUILD_C% :: compilation and generate header javac -d %BUILD_JAVA% %SRC_JAVA%\com\task\%JAVA_NAME%.java :: generate header javah -d %SRC_C% -cp %BUILD_JAVA% com.task.%JAVA_NAME% :: create MathExpressions.dll g++ -shared -o %BUILD_C%\MathExpressions.dll %SRC_C%\mathExpressions.cpp :: create JNIProject.dll lib which java application works with g++ -Wl,--add-stdcall-alias -I%HEADERS_FROM_JAVA_DIR% -I%HEADERS_FROM_JAVA_DIR%\win32 -shared -o %BUILD_C%\%DLL_NAME%.dll %SRC_C%\%DLL_NAME%.cpp :: copy headers to %BUILD_C% directory copy %SRC_C%\MathExpressions.h %BUILD_C% copy %SRC_C%\com_task_JNIProject.h %BUILD_C% :: run java program java -Djava.library.path=%BUILD_C% -cp %BUILD_JAVA% com.task.%JAVA_NAME%
Ссылки
Работа с DLL в C++ (Microsoft Visual C++)