Пользователь
0,0
рейтинг
20 января 2009 в 18:29

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

JAVA*
Здравствуйте.

Как вы, наверное, уже догадались, речь пойдет о 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++ кода.

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

Дмитрий @DmitriyN
карма
117,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (53)

  • 0
    Интересно узнать про методику выноса части функционала в C++ модуль.
    Например, мне нужно выполнить ресурсоемкую операцию с изображением, и я не хочу это делать средствами Java.

    Допустим, я подаю C-методу на вход картинку в виде массива байтов, Сишный код выполняет операцию с изображением и возвращет мне обратно массив байтов, дальше, средствами Джавы, я отображаю ее пользователю.

    Возможно ли такое?
    Стоит ли шкура выделки? Могу ли я таким образом повысить производительность своего приложения?

    Спасибо!

    • 0
      Конечно возможно. Как передавать данные со структурой сложнее, чем у примитивных типов, я опишу в следующей части.

      На счет вопроса, можно ли таким образом получить ускорение. Далеко не обязательно. В java есть JIT, который неплохо делает свою работу. С другой стороны, управляя памятью вручную, мы снимаем нагрузку с GC, что может дать некоторое ускорение. Вообщем, нужно хорошо понимать, что вы делаете :)
      • 0
        Под JIT я, разумеется, имел ввиду JIT compiler
    • 0
      Мне кажется, не стоит передавать изображение в виде массива байтов. Лучше записать его во временный файл и передать С++ лишь имя файла (да и вообще в джаве весь файл целиком не стоит считывать, имхо). По скорости это не будет особо влиять на нормальных фс (типа xfs, где если у вас много оперативки — файл останется в RAM некоторое время).

      Плюсы — не забивается память (а точнее умно управляется фс), скорость максимальная.
      • 0
        Решение хорошее для небольших нагрузок, но что если надо обрабатывать видео? 30+ кадров в секунду к примеру. И так в несколько потоков. Бедная ФС не справится :)
        • 0
          По идее должна справляться, но в любом случае всегда можно жестко указать что файл будет храниться в RAM'е. Такой-себе юникс-вей (не мешают ведь возможности фс делать виртуальную фс для устройств).

          Насколько я понимаю, с потоковым видео вообще это вещь хорошая (чтоб поток шёл в файл, а программка уже этот файл обрабатывала), ведь ОС справится куда лучше с нормальным получением потока в файл при хороших нагрузках чем ваша программа с двумя задачами сразу (и получение и обработка). И если программа чуть (на секунду) зависнет, ос, скорее всего, с потоком справится нормально.

          p.s.: это так, «мысли вслух», подобным не занимался, потому интересно от «практиков» услышать как оно на самом деле надо делать)
          • 0
            Да вы что, с чем ОС справится лучше?! Лучше чего?
            Файловая система — особенно в многозадачной среде — опасный союзник! У вас нет никаких прогнозов на скорость доступа к ней, какой тут реалтайм. Сейчас ваш файл буферезирован, а через секунду кто-то запросил памяти и буфер слит. И ваш код ждет данных неивестно сколько — как совпадут звезды, фрагментированность фс, загруженность очереди доступа к ней и еще бог знает сколько факторов.
            Если файловая система живет в ядре — с большой вероятностью практически каждая операция доступа к ней — переключение в режим ядра, что весьма небыстро (вы все еще хотите реалтайма? =) ). От этого спасает опять-таки буферизация, но вы ведь память экономить хотели?

            В общем говоря, если вам нужна скорость и детерменнированность — данные нужно передавать только в памяти. Нужна экономия — ищите способ минимизировать эти данные — передавать их блоками, слайсами, фреймами, как угодно.
            • 0
              Не вижу разницы между хранением потока в оперативки и memory mapped file ни по скорости ни по сливу на жесткий диск (или я ошибаюсь?)

              А почему хочется такого (с файлом)? Очень просто: к примеру, можно задать входной поток подпрограмме как файл (типа внешний API для тестирования в том числе уже готов).

              Или все-таки эти файлы в оперативке которые куда медленнее кидания буферами между модулями?
              • 0
                Смотрите, если вам нужно передать данные, реально много данных, которые будут долго обрабатываться и вам совершенно все равно с какой производительностью — MMF отличный выбор. Более того, это похоже вообще лучший выбор при пересечении границ процессов. Внутри же одного процесса…
                Если вам важна производительность — быстрее блока памяти вы ничего не придумаете.
                Если вам важна экономия памяти — нужно перерабатывать алгоритмы на работу с данными небольшими порциями. mp3 жмет входные данные по 1152 семпла, jpeg жмет картинку блоками 8х8 пикселей.
                Если вам нужен удобный интерфейс — заведите его! Хотите стандартный — посмотрите на комовский IStream, например. Если вам нужны только потоковые функции, зачем притягивать использование ядра ОС и файловой системы?
                • 0
                  > Если вам важна производительность — быстрее блока памяти вы ничего не придумаете.

                  Более чем согласен. У меня просто в другом чуть вопрос. На сколько я понял, оно отлично преобразует типы и классы из джавовских в C++'ные. Скорее всего это делается клонированием? Или я не прав?

                  Тоесть когда вы передаёте этот самый блок памяти, C++ имеет доступ именно к нему (без излишеств?) Если да — всё ясно, вы правы насчет производительности :-)
                  • 0
                    Каюсь, я про JNI ничего не знаю. Если память действительно копируется, MMF представляется самым быстрым методом.
                    Там все в самом деле так трудно?
                    • 0
                      По поводу того, копируется или нет — it depends. Бывает и так и так.
      • 0
        Тогда зачем вообще JNI? Если работать через файл, то можно написать консольную программку обработки на сях и вызывать ее через Runtime.exec().
        JNI нужен именно там, где необходима более близкая интеграция java, например для проксирования вызовов к операционке.
        • 0
          Ну ясное дело что если можно в отдельную подпрограмму вынести — лучше так и поступить :-) Но если вам нужно по-быстрому в C++ передать объект какой сложный, или результат получить в Java-объекте — для того JNI и сделан (имхо).
      • 0
        DirectBuffer'а оказываются весьма полезными для передачи больших массивов байтов между нативным и Java кодом…
    • 0
      Возможно, при помощи библиотеки описанной мной в комментариях ниже, я как раз реализовывал приём картинок из dll в java приложение путем возврата указателя на массив байтов…
    • 0
      По моему опыту будет быстрее сделать все в Java, хотя зависит от операций, которые вы хотите проводить. На элементарных операциях Java не будет проигрывать С++ — при первом вызове функции JIT скомпилит ее в native, поэтому медленным будет только этот первый вызов.
      Нужно также помнить, что JNI сравнительно медленный.
      • 0
        Не при первом. Это заблуждение…
        Но если будет вызыватся часто закомпилит точно…
    • 0
      Такое возможно, но, скорее всего, не стоит.
  • 0
    Я это все плохо понимаю, если проясните, буду благодарен.
  • 0
    А как быть, если мне напрамер нужно в С++ модуль передать переменную не простого типа, а какой-нибудь класс? Или наоборот, у меня есть класс написанный на С++ и один из методов должен вернуть объект данного класса, как здесь быть? То есть как производить обмен между модулями используя более сложные структуры чем примитивные типы и String?
    • 0
      А так очень интересно, естественно, продолжайте. По этой тематике маловато обучающих статей.
      • 0
        Вот это мы в следующий раз и разберем.
    • +1
      JNI формально поддерживает только вызовы в стиле Си. Но так как C++ является расширением языка C, то возможно на основе паттерна проектирования создать прокси-объекты для преобразования структуры объекта Java-языка в серию нативных вызовов через C-функции к объекту, написанному на C++. Или же упаковать данные объекта в пакет данных (массив или структуру) и через JNI передать его в другую среду выполнения, сделав всего один вызов вместо нескольких.
    • 0
      Для доступа к объекту из С++ есть специальные методы методы GetMethodID, GetFieldID ну и другие. Это неплохо описано в java.sun.com/j2se/1.5.0/docs/guide/jni/spec/functions.html
  • +1
    Перенесите в блог Java, пожалуйста.
  • 0
    Лет семь назад у меня получилось подружить Java и Delphi (.dll) — использовал специальный модуль конвертации типов C<->Pascal, а заголовочные файлы, полученные javah, переводил на Delphi Pascal вручную (это не сложно).
  • 0
    Есть не плохая книга Взаимодействие разноязыковых программ в Microsoft Windows. Руководство программиста. Там это довольно подробно описано, также еть и описание того как связать C++ и Perl, да и еще много чего.
  • 0
    Продолжайте! У вас получилось рассказать о нетривиальной задаче довольно просто. Как-то читал про JNI и мало что понял, а теперь всё встало на свои места. Спасибо.
  • 0
    Мне тоже приходилось как-то соединять web-приложение с DLL написанной на Deplhi,
    причем одним из условий было то, что перекомпилить DLL для JNI я не мог.
    Успешно вышел из ситуации при помощи JNA (https://jna.dev.java.net/):
    написал синглтон класс DllWrapper в котором создавался инстанс dll объекта
    DllLibInterface dllInst = (DllLibInterface) Native.loadLibrary(«test.dll», DllLibInterface.class);
    далее у dllInst можно было вызывать его вложенные функции…
    В свою очередь что бы избежать паралельного обращения к одним и тем же функциям DLL
    из разных сессий веб приложения — для каждой dll функции в классе DllWrapper были созданы
    отдельные synchronized функции обертки…
    С синтаксисом передаваемых и возвращаемых объектов разобраться по вышеуказанному сайту
    довольно легко.
    Надеюсь мой комментарий кому-нибудь облегчит жизнь. ;-)
    • 0
      Преимущество JNA перед JNI — что на надо определенным образом компилировать DLL библиотеку.
      Это очень полезное свойство при использовании функционала который вы не можете никак изменить как например windows API функции.
      Из найденых касяков — если DLL написана в Borland C++ Builder, то она не будет «коннектится» с java
      до тех пора пока в ней не будет убрано всякое упоминание о VCL.
      • 0
        В таких случаях можно еще написать библиотеку-прослойку. У JNA есть недостаток: нативный код не может обращаться назад к JVM. Например, мы никаким образом не сможем создать прокси-классы. Но когда задача довольно проста, то штука удобная, согласен ;)
  • 0
    Спасибо за статью!
    Жду продолжения!
    Особенно интересуют особенности использование native-кода при наследовании классов и обработка исключений.
  • +2
    Интересная серия статей про JNI, особо рассматривается передача строк, констант и структур между Java code и native сode
    community.livejournal.com/levin_matveev/29429.html
    community.livejournal.com/levin_matveev/29449.html
    community.livejournal.com/levin_matveev/29750.html
    community.livejournal.com/levin_matveev/30803.html
  • +5
    При использовании C++ для написания JNI методов нужно иметь в виду, что могут возникать C++ exceptions, которые не мог быть обработаны JVM, так как используется C ABI. Поэтому нужно корректно перехватывать эти исключения и либо конвертировать их в Java исключения, либо корректно завершать работу JVM. При использовании чистого C такой проблемы нет.

    Другой важный момент — это то, что Java exception, брошенный в нативном методе, не прерывает работу нативного метода в точке выброса exception.

    Когда имеет смысл использовать JNI:

    — имеется только нативная версия библиотеки с каторой нужно работать
    — нужен доступ к ресурсам, которым JVM не предоставляет доступ или доступ реализован очень плохо

    На текущий момент JVM JIT генерирует очень хороший код и задача написать быстрый JNI код — это нетривиальная задача, прикручивание в лоб обычно никакого прироста может не дать. У меня есть живые примеры, когда к нативной библиотеке, которая в 5 раз быстрее соотвествующей чистой Java библиотеки, прикручивали JNI интерфейс, итоговая производительность была всего лишь чуть-чуть больше чистой Java. Накладные расходы по JNI вызовам очень большие, поэтому желательно уходить в натив надолго, слишком частые переключения контекстов испольнения значительно снизять производительность. Еще один факт из жизни, чем сложнее данные которые нужно передовать между Java и нативом, тем медленее все работает. Все работает достаточно быстро при использовании примитивных типов, массивов примитовного типа и String.

    Очень-очень большие минусы JNI кода:

    — JNI код — это огромная security дыра в Java security-модели, JVM не имеет никаких средств по контролю JNI кода и JNI код может делать что угодно, в том числе получать доступ к приватным данным.
    — Утечки памяти. Память которая выделяется в JNI коде, за исключение Java объектов созданных в нативе, выделяется в общем heap приложения, а не в Java-heap. В Java нет деструкторов, поэтому может возникнут ситуация, когда что-то навыделялось в нативе и уже не нужно, но оно не освобождается. Пример:

    Пусть у нас есть Java объект, состоящий из Java данных и C++ данных (создаем в нативе С++ объект и храним handle на него в Java объекте), удаление C++ объекта помещаем в метод finalize, так как вызов finalize метода не дерминирован, то у нас возникают утечки памяти. С точки зрения GC проводить сборку мусора не нужно, так как Java-heap имеет достаточно места.

    Еще одна головная боль — это отладка JNI кода, приходится использовать два дебагера: Java кода и нативного кода. Вроде для Eclipse есть какой-то дебагер который позволяет отлаживать как Java код, так и JNI код.
    • 0
      Согласен со всем. Просто нужно понимать, что делаешь и когда это нужно применять.

      Кстати, добавлю. Оверхед может появиться даже при передаче массивов примитивного типа и строк, потому что JVM в некоторых случаях может решить, что их нужно скопировать.
    • 0
      Ну самый большой проблем всеж технологии, именно в «накладные расходы по JNI вызовам очень большие», все остальное не настолько существенно.
  • 0
    Я думаю не стоит забывать что ява программы и билиотеки можно откомпилировать омпилятором gcj и работать с ними через CNI(Cygnus Native Interfase)
  • 0
    Вопрос чуток не по теме, но тем не менее спрошу. Полгода назад, нужно было организовать синхронизацию времени на клиентской машине с сервером через java приложении. Кроме как сделать через JNI другого варианта не нашлось. Возможно ли это без вызова сишных функций?
    • 0
      Проблема возникла в том, чтобы установить системное время? К сожалению, тут либо JNI, либо Runtime.exec
      • 0
        ну да, я вот JNI и использовал, просто не хотелось привязыватсья к dll.
  • 0
    Очень интересно, спасибо!

    Жду продолжения как использовать C++ классы в JAVA.
  • +1
    Полезные статьи про JNI:

    How to Handle Java Finalization's Memory-Retention Issues — java.sun.com/developer/technicalArticles/javase/finalization/
    Debugging integrated Java and C/C++ code — www-128.ibm.com/developerworks/java/library/j-jnidebug/index.html
    Handling events from native objects in Java code — www-128.ibm.com/developerworks/java/library/j-jniobs/index.html
    Signal Chaining — java.sun.com/javase/6/docs/technotes/guides/vm/signal-chaining.html
  • –3
    Чистой воды юмор — «а в чём проблема? ставишь c# и полетел!»
    • 0
      Юмор это циклиться на одной технологии.
      Статья хорошая, продолжнение необходимо.
      • –1
        Юмор заключается в том что C# выглядит как слияние слияние C++ с Java.
        Синтаксис и база от C++ Jit от Java.
  • 0
    Спасибо большое! Очень интересно прочитать продолжение.
  • 0
    Отличная статья. Жду продолжения!
  • 0
    Существует генераторы кода для подобных вещей. Для примера — SWIG (http://www.swig.org)
    SWIG is a software development tool that connects programs written in C and C++ with a variety of high-level programming languages.

    SWIG — инструмент для связывания программ, написанных на C/C++ с другими языками высокого уровня.

    P.S. SWIG используется в Google для связывания C++ и Python.
  • 0
    Спасибо. Очень интересно и познавательно.
  • 0
    Если бы описали работу со структурами более подробно была бы хорошая статья, а так, данные информация есть в книжке Шилдта, которая и дает базовые понятия от jni.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    У меня немного другая задача: есть сервис написанный на Java. Кроме всего прочего у него наружу торчит RMI с которым успешно работает приложение на C#. Нужно научиться работать из Linux/C++. Что посоветуете использовать?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.