Всем привет! Меня зовут Роман Аймалетдинов, я разрабатываю клиентское приложение Ситимобил. Продолжаю свою серию статей по JNI, так как технология используется редко, но иногда она бывает очень полезной (или просто интересной). В этот раз я покажу примеры решений на JNI, которые совсем немного сложнее, чем hello world. И если вы не знакомы с JNI, то советую начать с первой части.

Содержание

  • JNI-типы.

  • Return в нативном методе.

  • Как передать List<List>.

  • Как пройтись по циклу в нативе.

  • Вызов Java-метода из С++.

JNI-типы

В JNI мы вынуждены использовать специальные типы для native-окружения. Мы не можем просто так передать int и работать с ним в С++, хотя это и кажется логичным. Таким образом, типы существующие в Java для JNI, дублируются с префиксом j. Например:

  • boolean → jboolean

  • byte → jbyte

  • char → jchar

И дело не ограничивается одними лишь примитивными типами, как описано в документации Oracle. Вам также придётся иметь дело с некоторыми неудобствами, то есть трансформациями.

Как с этим работать — рассмотрим в следующей главе.

Return в нативном методе

  1. В файле AwesomeLib, который содержит наши нативные методы, создадим метод getRandom, который возвращает int.

public class AwesomeLib {
  
    static {
        System.loadLibrary("nativeLib");
    }

    public native void helloHabr();

    public native int getRandom(); // <- new method
}
  1. Затем генерируем заголовок командой javac -h . AwesomeLib.java, наш файл .h обновится и появится новый метод:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class nativelib_AwesomeLib */
#ifndef _Included_nativelib_AwesomeLib
#define _Included_nativelib_AwesomeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
    Class:     nativelib_AwesomeLib
    Method:    helloHabr
    Signature: ()V
*/
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
  JNIEnv *,
  jobject
);

/*
    (Наш новый метод!)
    Class:     nativelib_AwesomeLib
    Method:    getRandom
    Signature: ()I
*/
JNIEXPORT jint JNICALL Java_nativelib_AwesomeLib_getRandom(
  JNIEnv *,
  jobject
);

#ifdef __cplusplus
}
#endif
#endif
  1. Теперь напишем код на С++, который будет возвращать случайное число. Подключим необходимую для этого библиотеку #include <ctime>, затем реализуем простейшее решение получения числа в С++:

#include "nativelib_AwesomeLib.h"
#include <iostream>
#include <ctime> // new lib

JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
  JNIEnv* env,
  jobject thisObject
  ) {
      std::cout << "Hello Habr! This is C++ code!!" << std::endl;
  }

// Новый метод
JNIEXPORT jint JNICALL Java_nativelib_AwesomeLib_getRandom(
  JNIEnv* env,
  jobject obj
  ) {
      std::srand(std::time(nullptr));
      int randomValue = std::rand();
      return randomValue;
  }

Как видно из примера (JNIEXPORT jint), возвращаем не int, а jint, хотя и в этом примере всё будет работать отлично, но в некоторых случаях придется делать каст к JNI-формату. Например, так: return (jint) variable;.

Как передать List

Как передать в конструктор простой тип? Проблем нет. А что делать, если нужно передать что-то посложнее? Например, List<List<Float>>?

  1. Создаем ещё один метод в нашей крутой библиотеке:

public class AwesomeLib {
    // code 

    public native void printMatrix(float[][] matrix);
}
  1. Но в заголовке (nativelib_AwesomeLib.h) будет jobjectArray — совсем не то, что мы хотели бы увидеть. Но придётся с этим жить.

/*
 * Class:     nativelib_AwesomeLib
 * Method:    printMatrix
 * Signature: ([[F)V
 */
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
    JNIEnv *,
    jobject,
    jobjectArray
);
  1. Пишем реализацию на С++:


JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
    JNIEnv * env,
    jobject obj,
    jobjectArray matrix
) {
    std::cout << "C++ code: print jobjectArray: " << matrix << std::endl;
}
  1. Передали, попробуем запустить программу (не забывайте, что после каждого изменения .cpp необходимо запустить в консоли команды для обновления .dll, это описано в первой статье):

Видим только адрес, а если будет matrix[0] или matrix[0][0] ? На самом деле, ни то, ни другое просто не скомпилируется, хотя будет нормально работать для jintArray. Всё дело в jobjectArray: сначала нужно получить из массива необходимые типы, скастить и только потом печатать. Так мы подошли к следующей теме.

Пройтись циклом по массиву в native

Чтобы пройтись по циклу, нужно:

  1. Узнать длину массива GetArrayLength.

  2. Получить элемент массива объектов GetObjectArrayElement.

  3. Скастить полученный jobjectArray в необходимый нам jfloatArray.

  4. Написать типичный код на С++.

JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
    JNIEnv * env,
    jobject obj,
    jobjectArray matrix
) {
    std::cout << "C++ code: print jobjectArray:" << std::endl;
    int sizeFirstArr = env->GetArrayLength(matrix);
    
    for (int i = 0; i < sizeFirstArr; i++) {
        jfloatArray secondArr = (jfloatArray) env->GetObjectArrayElement(matrix, i);
        jfloat *elements = env->GetFloatArrayElements(secondArr, 0);
        int sizeSecondArr = env->GetArrayLength(secondArr);
        
        for (int k = 0; k < sizeSecondArr; k++) {
            float value = elements[k];
            std::cout << value << ", ";
        }
        
        std::cout << std::endl;
    }
}

Запустим наш код и посмотрим, что получилось:

Классно, напечаталось. Но знаете, что ещё? Это же C++, никаких тебе тут gc и магии, будь добр, подчищай за собой.

env->ReleaseFloatArrayElements(secondArr, elements, 0);
env->DeleteLocalRef(secondArr);

Необходимо высвобождать память самостоятельно. Пусть этот код вызывается и из Java, но тут нет сборщика мусора, и утечки — ваша головная боль. Справедливости ради, считаю, что сложным кодом на C++ должна заниматься другая команда, которая его хорошо знает, но если это ваш pet-project и вы один, то не забывайте и об особенностях.

Как вызвать Java-метод из C++?

Мы научились простым, но основным операциям. Как вызвать метод, как вернуть значение и передать в конструктор. Но иногда хочется из С++ дёрнуть нечто полезное из Java. Как провернуть такой трюк?

  1. В нашем классе AwesomeLib.java создадим обычный метод, который вызовем из C++. Чтобы было интереснее, передадим в него float и int. Вызовем из Main наш метод из первой статьи — helloHabr. Немного его модифицируем и вызовем из него Java-метод. Таким образом у нас получится последовательность вызовов: Java → C++ → Java.

public class AwesomeLib {
    // from article: JNI Part 1
    public native void helloHabr();

    // new code
    public void printNativeResult(float value1, int value2) {
        System.out.println(
          "Java code: value1: " + value1 + " value2: " + value2
        );
    }
}
  1. Идем в AwesomeLib.cpp и изменяем метод из первой статьи:

JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
    JNIEnv* env,
    jobject thisObject
) {
    std::cout << "Hello Habr! This is C++ code!!" << std::endl;

    jclass cls_awesome_lib = env -> GetObjectClass(thisObject);
    jmethodID mid_compare = env->GetMethodID(
        cls_awesome_lib,
        "printNativeResult",
        "(FI)V"
    );
    
    // call method
    env->CallVoidMethod(
        thisObject,
        mid_compare,
        2.0,
        3
    );
}

Что тут происходит?

  • Сначала находим и получаем Java-объект, чтобы потом вызывать его методы.

  • Получаем id метода, чтобы обратиться к нему. Мы указываем название метода и то, что в нашем случае он содержит в конструкторе “(FI)”, поскольку у нас в нём float и int. V говорит о том, что это void-метод, а не возвращаемый.

  • Вызываем метод.

  1. Обновляем .dll и запускаем наш код:

Ура, мы умеем запускать код Java из native-кода!

Чуть подробнее про GetMethodID:

  • “**(FI)V**” → void someFunc(float, int)

  • “**(FF)V**” → void someFunc(float, float)

  • “**(FI)Z**” → boolean someFunc(float, int)

  • “**()Z**” → boolean someFunc()

Обращаемся к информации с сайта Oracle:

Заключение

Напоследок скажу, что JNI — это не совсем обычный C/C++. Но не стоит бояться, нужно исследовать! В моей небольшой серии про JNI осталась последняя статья, в ней я расскажу про самое важное и интересное: производительность! Напишу простые синтетические тесты и покажу, когда в JNI есть смысл, а когда нет.

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


  1. kamasit
    11.02.2022 14:30

    спасибо за статью. а схему String -> wstring -> String можно реализовать также как в первой части статьи?


    1. Evleaps Автор
      11.02.2022 14:34
      +1

      Здравствуйте, простите меня за мой непрофессионализм в c/c++, но вопрос я не понял.
      Насколько я знаю, wstring в плюсах используется для unicode строк, а string для ASCII строк. Но какое это отношение имеет к моей серии статей? :)
      Попробуйте переформулировать вопрос, больше деталей. Возможно я, или кто-то из сообщества даст ответ.


      1. equeim
        12.02.2022 13:53
        +1

        Насколько я знаю, wstring в плюсах используется для unicode строк, а string для ASCII строк

        Не совсем верно (а точнее, совсем не верно). string и wstring никакого отношение к кодировкам и символам не имеют. Это просто массив байтов (char) либо значений wchar_t. То, в какой кодировке закодированы (простите) эти байты (или значения wchar_t если кодировка это позволяет) зависит только от того как вы их используете (их содержимое также может в принципе не быть текстом).

        Например, если вы получаете пользовательский ввод от системы в виде байтов и сохраняете его в string, то ее кодировка будет зависит от системы и ее конфигурации. На юниксах обычно будет UTF-8, а на windows локальная 8-битная кодировка (например CP-1251). Но в обоих случаях возможны любые варианты.

        Более того wchar_t на разных платформах имеет разный размер так что wstring в принципе имеет смысл использовать только коде завязанном на windows.

        Также по этой причине wstring нельзя использовать для java String в кросс-платформенном JNI коде потому что размер jchar фиксирован (16 бит) а wchar_t - нет.


  1. RyAtex
    11.02.2022 18:18

    Спасибо, очень интересно. А вопрос, в Qt (С++) есть сигналы и слоты. Их можно как-то связять с JNI?


  1. igormich88
    11.02.2022 18:22
    +1

    Возник такой вопрос можно ли передать лямбду параметром native метода и есть ли в этом смысл (в первую очередь в плане производительности)?