Советы по JNI

JNI (Java Native Interface) это интерфейс разработки платформо-зависимого кода. Он определяет способ для управляемого кода (написанного на языке программирования Java) взаимодействовать с машинным кодом (написанным на C/C++). Он независимый от поставщика, поддерживает загрузки кода из динамических библиотек, и в то же время громоздкое порой достаточно эффективно.

Если вы еще не знакомы с ним, прочитайте Спецификация Java Native Interface для получения представление о том, как JNI работает и какие функции доступны. Некоторые аспекты интерфейса не сразу видны в первом чтении, поэтому следующие несколько разделов доступно описывают некоторые из этих аспектов.

JavaVM и JNIEnv

JNI определяет две ключевые структуры данных, "JavaVM" и "JNIEnv". Обе из них являются по существу указателями на указатели таблицы функций. (В C++ версии, это классы с указателем на таблицу функций и функцей-членом класс для каждой функции JNI, которые косвенно ссылаются через таблицу). JavaVM обеспечивает функции "Интерфейса вызова", который позволяет создавать и уничтожать JavaVM. В теории вы можете иметь несколько JavaVM в процессе, но Android разрешает только один.

JNIEnv предоставляет большинство функций JNI. Ваши платформо-зависимые функции все получают JNIEnv в качестве первого аргумента.

JNIEnv используется для локальной памяти потока. По этой причине, вы не можете совместно использовать JNIEnv в нескольких потоках. Если кусок кода не имеет другого способа получить свой JNIEnv, вы должны использовать общий JavaVM, и использовать GetEnv чтобы обнаружить JNIEnv потока. (Предполагается, что он один существует; см. AttachCurrentThread ниже.)

Объявления функций C из JNIEnv и JavaVM отличаются от объявлений C++. "jni.h" включаемый файл предоставляет различные определения типов в зависимости от того, где это включено в С или С++. По этой причине, это плохая идея включить JNIEnv аргументы в файлы заголовков, включенных в обоих языках. (Другими словами: если ваш файл заголовка требует #ifdef __cplusplus, вам, возможно, придется сделать некоторую дополнительную работу, если что-нибудь в этом заголовке относится к JNIEnv.)

Потоки

Все потоки это Linux потоки, планируемые ядром. Они, как правило, запускаются из управляемого кода (с помощью Thread.start), но они также могут быть созданы в любом другом месте и затем прикреплены к JavaVM. Например, поток запущенный с помощью pthread_create может быть прикреплен с помощью JNI AttachCurrentThread или AttachCurrentThreadAsDaemon функции. Пока поток не прикреплен, он не имеет JNIEnv, и не может делать JNI вызовы.

Присоединение созданного потока приводит к созданию java.lang.Thread объекта, а добавление его к "главной" ThreadGroup, делая его видимым для отладчика. Вызов AttachCurrentThread для уже прикрепленного потока ничего не делает.

Android не приостанавливает потоки выполняющих платформо-зависимый машинный код. Если выполняется сбор мусора, или отладчик приостановил запрос, Android приостановит поток, когда он сделает JNI вызов в следующий раз.

Потоки прикрепленные через JNI должны вызвать DetachCurrentThread прежде чем они завершатся. Если кодирование этого напрямую неудобно, в Android 2.0 (Eclair) и выше можно использовать pthread_key_create для определения функции деструктора, которая будет вызвана, прежде чем поток завершится, и вызовет DetachCurrentThread оттуда. (Используйте этот ключ с pthread_setspecific для хранения JNIEnv в локальном хранилище потока; таким образом он будет передан в ваш деструктор в качестве аргумента.)

jclass, jmethodID и jfieldID

Если вы хотите получить доступ к полю объекта из машинного кода, вам необходимо сделать следующее:

  • Получить ссылку на объект класса с помощью FindClass
  • Получить идентификатор поля с помощью GetFieldID
  • Получить содержимое поля, используя соответствующий метод, например такой как GetIntField

Аналогично, для вызова метода, вы должны сначала получить ссылку на объект класса, а затем идентификатор метода. Идентификаторы это чаще всего указатели на внутренние структуры данных времени выполнения. Их поиск может потребовать несколько сравнений строк, но как только у вас они есть, реальный вызов для получения значения поля или вызов метода выполняется очень быстро.

Если производительность важна, полезно получить значения один раз и кэшировать результат в вашем коде. Т.к. есть ограничение на одну JavaVM на процесс, разумнее всего хранить эти данные в статической локальной структуре.

Ссылки на классы, идентификаторы полей, и идентификаторы методов гарантированно действительные до выгрузки класса. Классы выгружаются только если все классы, связанные с ClassLoader могут быть собраны сборщиком мусора, что бывает крайне редко, но не будет невозможно в Android. Заметим, однако, что jclass является ссылкой класса, и должна быть защищена с помощью вызова NewGlobalRef (см. следующий раздел).

Если вы хотели бы кэшировать идентификаторы при загрузке класса, и автоматически повторно кэшировать, если класс когда либо выгрузится и перезагрузится, правильный способ для инициализации идентификаторов это добавление куска кода, который выглядит следующим образом для соответствующего класса:

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

Создайте nativeClassInit метод в C/C++ коде, который выполняет поиск идентификаторов. Код выполнится один раз, когда класс инициализируется. Если класс когда либо выгрузится, а затем перезагрузится, то он будет выполнен снова.

Локальные и глобальные ссылки

Каждый аргумент, передаваемый в платформо-зависимый метод, и почти каждый объект, возвращаемый функцией JNI является "локальной ссылкой". Это означает, что она действует в течение всего срока выполнения текущего метода в текущем потоке. Даже если сам объект продолжает жить после выхода из метода, ссылка не действительна.

Это относится ко всем подклассам jobject, включая jclass, jstring, и jarray. (Среда выполнения предупредит вас о большинстве неправильно использованный ссылках, если расширенная проверка JNI включена.)

Единственный способ получить нелокальные ссылки это использовать функции NewGlobalRef и NewWeakGlobalRef.

Если вы хотите держаться ссылки более длительный период, вам необходимо использовать «глобальную» ссылку. NewGlobalRef функция принимает локальную ссылку в качестве аргумента и возвращает глобальную. Глобальный ссылка гарантированно действительна до вызова DeleteGlobalRef.

Этой шаблон обычно используется, когда кэшируется jclass возвращенный FindClass, например.:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

Все методы JNI принимают как локальные, так и глобальные ссылки в качестве аргументов. Ссылки на один и тот же объект могут иметь разные значения. Например, возвращенные значения из последовательных вызовов NewGlobalRef на для того же объекта могут быть различными. Чтобы убедиться, что две ссылки указывают на один и тот же объект, необходимо использовать IsSameObject функцию. Никогда не сравнивайте ссылки с помощью == в С/С++ коде.

Одним из следствий этого является то, что вы не должны думать, что ссылки на объекты постоянны или уникальны в платформо-зависимом коде. 32-разрядное значение, представляющее объект, может отличаться от одного вызова метода к другому, и вполне возможно, что два различных объекта могут иметь такое ​​же 32-битное значение на последовательных вызовов. Не используйте jobject значения как ключи.

Программисты не должны "чрезмерно выделять" локальные ссылки. В практическом плане это означает, что если вы создаете большое количество локальных ссылок, возможно, во время работы с массивом объектов, вы должны освободить их вручную с помощью DeleteLocalRef вместо того, чтобы JNI сделать это за вас. Реализация требуется зарезервировать слоты только для 16 локальных ссылок, так что если вам нужно больше чем это значение, вы должны либо удалить по завершении метода, или использовать EnsureLocalCapacity/PushLocalFrame чтобы зарезервировать больше.

Обратите внимание, что jfieldID и jmethodID "непрозрачные" типы, не ссылки на объекты, и не должны передаваться в NewGlobalRef. Указатели на машинные данных, возвращаемые функциями, такими как GetStringUTFChars и GetByteArrayElements также не объекты. (Они могут быть переданы между потоками, и действительны до соответствующего вызова Release.)

Один необычный случай заслуживает отдельного упоминания. Если вы прикрепили поток с помощью AttachCurrentThread, выполняемый код не будет автоматически освобождать местные ссылки, пока поток не открепится. Все локальные ссылки, которые вы создадите, должны быть удалены вручную. В общем, любой платформо-зависимый код, который создает локальные ссылки в цикле, вероятно, должен делать некоторое ручное удаление.

UTF-8 и UTF-16 строки

Язык программирования Java использует UTF-16. Для удобства JNI предоставляет методы, которые работают так же с Модифицированной UTF-8 . Модифицированная кодировка полезна для C кода, поскольку она кодирует \u0000 как 0xc0 0x80 вместо 0x00. Это хорошо тем, что вы можете рассчитывать на работу в стиле С с нулем для завершения строки, используя стандартные функции строк из libc. С другой стороной вы не можете передать произвольные UTF-8 данные в JNI и ожидать, что оно будет работает правильно.

Если это возможно, то как правило быстрее работать со строками UTF-16. Android в настоящее время не требует копирования в GetStringChars, в то время как GetStringUTFChars требуется выделение и преобразование в UTF-8. Обратите внимание, что UTF-16 строки не завершаются нулем, и \u0000 разрешен, так что вам нужно следить за длиной строки также как и за jchar указателем.

Не забудьте Release для строк, для которых вы использовали Get. Строковые функции возвращают jchar* или jbyte*, которые являются указателями в C-стиле на примитивные данные, а не локальными ссылками. Они гарантированно действует до вызова Release , и это также означает, что они не будут освобождены по завершению метода.

Данные, передаваемые в NewStringUTF должны быть в модифицированном формате UTF-8. Распространенная ошибка это чтение символьных данных из файла или сети и передача их в NewStringUTF без фильтрации. Если вы не знаете, данные это 7-битный ASCII, необходимо вырезать символы высокого ASCII или конвертировать их в надлежащий модифицированный UTF-8 формат. Если вы этого не сделаете, преобразование UTF-16 скорее всего будет не таким как вы ожидаете. Расширенные проверки JNI будут сканировать строки и предупреждать вас о неправильных данных, но они не смогут ловить все.

Массивы примитивов

JNI предоставляет функции для доступа к содержимому массива объектов. В то время как доступ к элементам массива объектов должен осуществляться к одной записи за один раз, массивы примитивов могут быть считаны и записаны непосредственно, как если бы они были объявлены в С.

Чтобы сделать интерфейс как можно более эффективным, не накладывая ограничений на реализацию VM, семейство методов Get<PrimitiveType>ArrayElements позволяет среде выполнения либо возвращать указатель на фактические элементы, или выделить некоторый объем памяти и сделать копию. В любом случае, сырой указатель возвращаемый одной из функций, гарантировано годный до соответствующего вызова Release (это означает, что если данные не были скопированы, объект массив будет удерживаться и не сможет быть перемещен в рамках сжатия кучи). Вы должны вызвать Release для каждый массива, для которого вызывался Get. Кроме того, если Get вызов не удался, вы должны убедиться, что ваш код не пытается вызвать Release с NULL указателем позже.

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

Release метод принимает mode аргумент, который может иметь одно из трех значений. Действия, выполняемые во время выполнения зависят от того, возвращался ли указатель на фактических данные или их копию:

  • 0
    • Фактические: объект массива перестает удерживаться.
    • Копия: данные копируются обратно. Буфер с копией освобождается.
  • JNI_COMMIT
    • Фактические: ничего не делает.
    • Копия: данные копируются обратно. Буфер с копией не освобождается.
  • JNI_ABORT
    • Фактические: объект массива перестает удерживаться. Ранее выполненные записи не прерванные.
    • Копия: буфер с копией освобождается; любые изменения в нем будут потеряны.

Одной из причин для проверки isCopy флага является необходимость знать нужно ли вызвать Release с JNI_COMMIT после внесения изменений в массиве - если вы чередуете внесение изменений и выполнение кода, использующего содержимое массива, вы можете пропустить ничего не делающий commit. Другая возможная причина для проверки флага это эффективная обработка JNI_ABORT. Например, вы можете получить массив, изменить его, передать часть данных в другие функции, а затем отменить изменения. Если вы знаете, что JNI делает новую копию для вас, нет никакой необходимости создавать еще одну "редактируемую" копию. Если JNI передает вам оригинал, то вам нужно создать собственную копию.

Распространенная ошибка (повторяется в примере кода) предполагать, что вы можете пропустить Release вызов, если *isCopy равна false. Это не тот случай. Если не было выделено буфера для копии, то исходная память удерживается и не может быть перемещена сборщиком мусора.

Также отметим, что JNI_COMMIT флаг не отпускает массив, и вам нужно будет вызвать Release еще раз с другим флагом в конце концов.

Вызовы для диапазона

Существует альтернатива вызовам типа Get<Type>ArrayElements и GetStringChars которые могут быть очень полезны, когда все что вы хотите сделать, это скопировать данные в или из. Рассмотрим следующий кусок кода:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

Он захватывает массив, копирует первые len байт элементов из него, а затем освобождает массив. В зависимости от реализации, Get вызов будет либо удерживать либо копировать содержимое массива. Код копирует данные (возможно даже во второй раз), а затем вызывает Release; в этом случае JNI_ABORT гарантирует, что не будет третьей копии.

Можно сделать то же самое проще:

    env->GetByteArrayRegion(array, 0, len, buffer);

Это имеет ряд преимуществ:

  • Требуется один JNI вызов, вместо 2-х, уменьшая накладные расходы.
  • Не требуется удерживание или дополнительные копии данных.
  • Снижает риск ошибки программиста — отсутствие риска забыть вызвать Release после каких либо ошибок.

Аналогично, вы можете использовать Set<Type>ArrayRegion для копирования данных в массив, и GetStringRegion или GetStringUTFRegion для копирования символов из String.

Исключения

Вы не должны вызвать большинство функций JNI после того как было брошено исключение.. Ожидается, что ваш код обратит внимание на исключения (используя возвращенное значения функции, ExceptionCheck, или ExceptionOccurred) и завершится, или очистит исключение и обработает его.

Единственные функции JNI, которые вы можете вызвать во время исключений:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Многие вызовы JNI могут вызвать исключение, но часто обеспечивают более простой способ проверки на ошибки. Например, если NewString возвращает не нулевое значение, вам не нужно проверять исключения. Однако, если вы вызываете метод (с помощью таких функций как CallObjectMethod), вы должны всегда проверять исключения, потому что возвращенное значение не будет действительным, если было сгенерировано исключение.

Обратите внимание, что исключения генерируемые интерпретируемым кодом не разворачивает стек платформо-зависимого кода, и Android еще не поддерживает исключения C++. JNI Throw и ThrowNew инструкции просто устанавливают указатель исключений в текущем потоке. По возвращении в управляемый код из машинного кода, исключение будет отмечено и обработано соответственно.

Машинный код может "поймать" созданное исключение методом ExceptionCheck или ExceptionOccurred, и очистить его с помощью ExceptionClear. Как обычно, игнорирование исключений без их обработки может привести к проблемам.

В JNI нет встроенных функций для работы с Throwable объектом, так что если вы хотите получить строку исключения вы должны будете найти Throwable класс, найти идентификатор метода для getMessage "()Ljava/lang/String;", вызвать его, и если результат не равен NULL, использовать GetStringUTFChars для получения того, что вы можете передать в printf(3) или эквивалент.

Расширенные проверки

JNI делает очень небольшую проверку на ошибки. Ошибки, как правило, приводят к аварийным ситуациям. Android также предлагает режим под названием CheckJNI, где указатели на таблицы функций JavaVM и JNIEnv переключаются на таблицы функций, которые выполняют расширенную серию проверок перед вызовом стандартной реализации.

Дополнительные проверки включают:

  • Массивы: попытка выделить массив с отрицательным размером.
  • Плохие указатели: передача плохого jarray/jclass/jobject/jstring в вызов JNI, или передача NULL указателей в вызовы JNI для аргументов, которые не могу быть null.
  • Имена классов: передача любых имен классов в стиле “java/lang/String” для вызова JNI.
  • Критические вызовы: делая JNI вызовы между "критическими" вызовами get и соответствующих release.
  • Прямые ByteBuffers: передача плохих аргументов в NewDirectByteBuffer.
  • Исключения: выполнение вызова JNI в то время как есть исключения в ожидании.
  • JNIEnv*: использование JNIEnv* из неправильного потока.
  • jfieldID: использование jfieldID со значением NULL, или с использование jfieldID для установки поля в значение неверного типа (пытаясь присвоить StringBuilder полю типа String, например), или использование jfieldID статического поля для установки поля экземпляра или наоборот, или использование jfieldID одного класса с экземплярами другого класса.
  • jmethodID: использование неправильный типа jmethodID при выполнении Call*Method JNI вызова: неправильный тип возврата, несоответствие на статический/нестатический тип метода, неправильный тип для ‘this’ (для нестатических вызовов) или неправильный класс (для статических вызовов).
  • Ссылки: использование DeleteGlobalRef/DeleteLocalRef с неправильным видом ссылки.
  • Режимы Release: передача неверного режима при вызове Release (значение отличное от 0, JNI_ABORT, или JNI_COMMIT).
  • Безопасность типов: возвращение несовместимого типа из вашего платформо-зависимого метода (возврат StringBuilder из метода, объявленного с типом возврата String, например).
  • UTF-8: передача неверной Модифицированной UTF-8 последовательности байтов в вызов JNI.

(Доступность методов и полей все ещё не проверяется: ограничения доступа не применяются для машинного кода.)

Есть несколько способов для включения CheckJNI.

Если вы используете эмулятор, CheckJNI включена по умолчанию.

Если у вас есть устройство с root-правами, вы можете использовать следующую последовательность команд для перезапуска выполнения с включеной CheckJNI:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

В любом из этих случаев, вы увидите что-то вроде этого в вашем logcat выводе при запуске среды выполнения:

D AndroidRuntime: CheckJNI is ON

Если у вас есть обычное устройство, вы можете использовать следующую команду:

adb shell setprop debug.checkjni 1

Это не повлияет на уже запущенные приложения, но любая программа запущена с этого момента будет иметь включенную CheckJNI. (Изменение свойства на любое другое значение или просто перезагрузка отключит CheckJNI снова). В этом случае, вы увидите что-то вроде этого в вашем выводе logcat в следующий раз при запуске приложения:

D Late-enabling CheckJNI

Платформо-зависимые библиотеки

Вы можете загрузить собственный код из разделяемых библиотек стандартным System.loadLibrary вызовом. Предпочтительный способ подключить свой платформо-зависимый код:

  • Вызовите System.loadLibrary из статического инициализатора класса. (См. предыдущий пример, где используется для вызова nativeClassInit.) Аргументом является имя библиотеки "без отделки", поэтому чтобы загрузить "libfubar.so" вы просто передаете в функцию "fubar".
  • Реализуйте платформо-зависимую функцию: jint JNI_OnLoad(JavaVM* vm, void* reserved)
  • В JNI_OnLoad, зарегистрируйте все ваши методы. Вы должны объявить методы как "static", чтобы имена не занимали много места в таблице символов на устройстве.

JNI_OnLoad функция должна выглядеть примерно так, если будет написана на C++:

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // Get jclass with env->FindClass.
    // Register methods with env->RegisterNatives.

    return JNI_VERSION_1_6;
}

Вы также можете вызвать System.load с указанием полного пути разделяемой библиотеки. Для приложений Android, вы можете найти это полезным для получения полного пути к приватной зоне хранения данных приложения из объекта контекста.

Это рекомендуемый, но не единственный подход. Явная регистрация не требуется, как и не обязательно реализовывать JNI_OnLoad функцию. Вместо этого можно использовать "поиск" собственных методов, которые называются определенным образом (см. спецификация JNI ), хотя это менее желательно, поскольку, если сигнатура метода неверная, то вы не будете знать об этом до фактического использования метода.

Еще одно замечание о JNI_OnLoad: любым FindClass вызовом, который вы делаете оттуда, будет будет выполняться в контексте загрузчика класса, который был использован для загрузки разделяемой библиотеки. Обычно FindClass использует загрузчик, связанный с методом в верхней части интерпретируемого стека, или если нет ни одного (так как поток был просто прикреплен), то он использует "системный" загрузчик классов. Это делает JNI_OnLoad удобным местом для поиска и кэширования ссылок на классы объектов.

Рассмотрение 64-разрядности

Android в настоящее время работает на 32-битных платформах. В теории он может быть построен для 64-битной системы, но это не является целью в это время. По большей части это не то о чем вам нужно будет беспокоиться при взаимодействии с машинным кодом, но это может стать существенным, если вы планируете хранить указатели на платформо-зависимые структуры в целочисленных полях объекта. Для поддержки архитектуры, которая используют 64-битные указатели, вам нужно хранить свои родные указатели в long полях, а не в int.

Неподдерживаемые функции/Обратная совместимость

Вся функциональность JNI 1.6 поддерживаются, за следующим исключением:

  • DefineClass не реализован. Android не использует Java байт-код или файлы классов, поэтому передача данных двоичного класса не работает.

Для обратной совместимости со старыми версиями Android, возможно, потребуется быть в курсе слудующего:

  • Динамический поиск платформо-зависимых функций

    До Android 2.0 (Eclair), символ '$' не был должным образом преобразован в "_00024" во время поиска имен методов. Работа вокруг этого требует использование явной регистрации или перемещения платформо-зависимых методов за пределы внутренних классов.

  • Открепление потоков

    До Android 2.0 (Eclair), было не можно использовать pthread_key_create деструктор для избежания проверки того, что "поток должен быть откреплен перед выходом из потока". (Среда выполнения также использует ключевую функцию деструктора pthread, поэтому будет "гонка" при определении того, что вызывается первым.)

  • Слабые глобальные ссылки

    До Android 2.2 (Froyo), слабые глобальные ссылки не были реализованы. Старые версии будут активно отвергать попытки их использования. Вы можете использовать константы версии платформы Android, чтобы проверить наличие поддержки.

    До Android 4.0 (Ice Cream Sandwich), слабые глобальные ссылки можно было передать только в NewLocalRef, NewGlobalRef, и DeleteWeakGlobalRef. (Спецификация настоятельно рекомендует программистам создавать жесткие ссылки на слабых глобальные ссылки прежде чем делать что-либо с ними, так что это вообще не должно быть ограничением.)

    Начиная с Android 4.0 (Ice Cream Sandwich), слабые глобальные ссылки можно использовать как и любые другие ссылки JNI.

  • Локальные ссылки

    До Android 4.0 (Ice Cream Sandwich), локальные ссылки были на самом деле прямыми указателями. Ice Cream Sandwich сделал их косвенными для поддержки лучшей сборки мусора, а также это означает, что много JNI ошибок не обнаруживаются на более старых версиях. Читайте Изменения в локальных ссылках в ICS для более подробной информации.

  • Определение ссылочного типа с помощью GetObjectRefType

    До Android 4.0 (Ice Cream Sandwich), как следствие использования прямых указателей (см. выше), было невозможно реализовать GetObjectRefType правильно. Вместо этого мы использовали эвристику, которая искала по таблице слабых глобальных ссылок, аргументам, таблице локальных ссылок, и таблице глобальных переменных в таком порядке. Как только в первый раз он нашел прямой указатель, он сообщает, что ваша ссылка была этого типа. Это означало, например, что если вы назвали GetObjectRefType для глобального jclass, и произошло так, что он был такими же как jclass, передаваемый в качестве неявного аргумента вашему статическому платформо-зависимому методу, вы получите JNILocalRefType а не JNIGlobalRefType.

Вопросы и ответы: Почему я получаю UnsatisfiedLinkError?

При работе с платформо-зависимым кодом это не редкость увидеть ошибку, как эта:

java.lang.UnsatisfiedLinkError: Library foo not found

В некоторых случаях это означает, что библиотека не найдена. В других случаях, что библиотека существует, но не может быть открыта с помощью dlopen(3), и детали об ошибке можно найти в подробном сообщении исключения.

Наиболее распространенные причины, почему вы можете столкнуться с исключением "библиотека не найдена":

  • Библиотека не существует или не доступна для приложения. Используйте adb shell ls -l <path> для проверки существования и разрешений.
  • Библиотека не была построена с помощью NDK. Это может привести к зависимостям от функций или библиотек, которые не существуют на устройстве.

Другой класс UnsatisfiedLinkError ошибок выглядит следующим образом:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

В logcat, вы увидите:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

Это означает, что среда выполнения пыталась найти соответствующий метод, но безуспешно. Некоторые общие причины этого:

  • Библиотека не была загружена. Проверьте вывод в logcat с сообщениями о загрузке библиотек.
  • Этот метод не был найден из-за имени или несоответствия сигнатуры. Это обычно вызывается:
    • Для отложенного метода поиска, из-за отсутствия объявления функции C++ с помощью extern "C" и соответствующей видимости(JNIEXPORT). Обратите внимание, что до Ice Cream Sandwich, JNIEXPORT макрос был неправильным, так что использование нового GCC со старым jni.h работать не будет. Вы можете использовать arm-eabi-nm чтобы увидеть символы, как они появляются в библиотеке; если они выглядят как-то неправильно (что-то вроде _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass а не Java_Foo_myfunc), или если тип символ является строчной 't', а не прописной 'T', то вам нужно настроить объявление функций.
    • Для явной регистрации, незначительные ошибки при вводе сигнатуры метода. Убедитесь в том, что то, что вы передаете в регистрирующий вызова соответствует сигнатуре в файле журнала. Помните, что "B" это byte и 'Z' это boolean. Компонента имени класса в сигнатуре начинается с 'L', заканчиваться ';', используйте '/' для разделения имен пакета/класса, а также используйте '$', чтобы отделить имена внутренних классов(Ljava/util/Map$Entry;, например).

Используя javah для автоматического генерирования заголовков JNI вы сможете избежать некоторых проблем.

Вопросы и ответы: Почему FindClass не находит мой класс?

Убедитесь в том, что строка с именем класса имеет правильный формат. Имена классов в JNI начинаются с имени пакета и разделяются косой чертой, например java/lang/String. Если вы ищете класс массива, вам нужно начать с соответствующего количества квадратных скобок, а также должны обернуть класс с помощью 'L' и ';', таким образом одномерный массив состоящий из String будет [Ljava/lang/String;.

Если имя класса выглядит правильно, возможно вы столкнулись с проблемой загрузчика. FindClass начинает поиск класса в загрузчике классов, связанного с вашим кодом. Он рассматривает стек вызовов, который выглядит примерно так:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)
    dalvik.system.NativeStart.main(Native Method)

Самый верхний метод это Foo.myfunc. FindClass находит ClassLoader объект, связанный с Foo классом и использует его.

Как правило это работает так, как вы хотите. У вас могут возникнуть проблемы, если вы создаете поток сами (возможно, с помощью вызова pthread_create и затем прикрепления его с помощью AttachCurrentThread). Теперь трассировка стека выглядит следующим образом:

    dalvik.system.NativeStart.run(Native Method)

Самый верхний метод это NativeStart.run, который не является частью приложения. Если вы вызовете FindClass из этого потока, JavaVM начнет в "системном" загрузчике классов вместо связанного с приложением, поэтому попытки найти конкретные классы приложения не удастся.

Есть несколько способов, чтобы обойти это:

  • Выполняйте ваши FindClass поиски сразу, в JNI_OnLoad, и кэшируйте ссылки на класс для последующего использования. Любой FindClass вызов, сделанный в рамках выполнения JNI_OnLoad будет использовать загрузчик классов, связанный с функцией которая вызвала System.loadLibrary (это специальное правило, позволяет выполнять инициализации библиотек удобнее). Если ваш код приложения загружает библиотеку, FindClass будет использовать правильный загрузчик классов.
  • Передайте экземпляр класса в функции, которые в нем нуждаются, объявив свой собственный метод принимающий Class в качестве аргумента, а затем передайте в него Foo.class.
  • Закэшируйте ссылку на ClassLoader объект где-то под рукой, и делайте loadClass вызовы непосредственно. Это потребует некоторых усилий.

Вопросы и ответы: Как я могу совместно использовать машинные данные с машинным кодом?

Вы можете оказаться в ситуации, когда вам нужно получить доступ к большому буферу необработанных данных из управляемого и машинного кода. Общие примеры включают манипуляции с растровыми изображениями или звуковыми образцами. Есть два основных подхода.

Вы можете хранить данные в byte[]. Это дает очень быстрый доступ из управляемого кода. Однако, на платформо-зависимой стороне нет гарантии, что вы будете иметь возможность получить доступ к данным без необходимости их копирования. В некоторых реализациях, GetByteArrayElements и GetPrimitiveArrayCritical возвращают фактические указатели на исходные данные в управляемой куче, но в других они будут выделять буфер на куче машинного кода и копировать данные.

В качестве альтернативы можно хранить данные в прямом буфере байтов. Его можно создать с помощью java.nio.ByteBuffer.allocateDirect, или JNI NewDirectByteBuffer функции. В отличие от обычных байт буферов, область хранения не выделяется в управляемой куче, и может всегда быть доступна непосредственно из машинного кода (получив адрес с помощью GetDirectBufferAddress). В зависимости от того, как реализован доступ к прямому буферу байт, доступ к данным из управляемого кода может быть очень медленным.

Выбор того, какой подход использовать, зависит от двух факторов:

  1. Будет ли большая часть доступа к данным осуществляться кодом, написанным на Java или на C/C++?
  2. Когда данные в конечном счете будут переданы системному API, в каком формате они должны быть? (Например, если данные в конечном итоге передаются функции, которая принимает byte[], то делать обработку в прямом ByteBuffer может быть неразумно.)

Если нет явного победителя, используйте прямой буфер байтов. Поддержка для них встроена непосредственно в JNI и производительность должна улучшиться в будущих выпусках.