Почему JNI C API использует указатель на указатель вместо прямых указателей для JNIEnv? - PullRequest
1 голос
/ 17 марта 2020

Отказ от ответственности: Я реализовал свои собственные объектно-ориентированные языки программирования, поэтому знаком с (и научил) указателями и C. Управление памятью 101 не является тем, о чем этот вопрос. Это также не относится к API JNI C ++, который выглядит похожим. Спасибо.

Когда вы смотрите на JNI C API для Java, в jni.h, JNIEnv - это typedef для указателя на struct JNINativeInterface. Учитывая, что весь JNI C API принимает JNIEnv*, это означает, что это действительно JNINativeInterface **, что означает указатель на указатель на struct.

Почему JNI использует этот дополнительный уровень косвенности ?

Имитация объектоподобной конструкции в C будет прекрасно работать с JNINativeInterface*. Вы можете просто позвонить

env->NewGlobalRef(env, my_object);

, так зачем нас заставлять делать

(*env)->NewGlobalRef(env, my_object);

? Дополнительный уровень косвенности передается в функции в качестве первого аргумента, поэтому я думаю, они могли бы обновить этот указатель. Это так?

Исправление: Первоначально я запомнил неправильно и передал (*env) вместо env в качестве первого аргумента и поэтому исключил возможность вызываемого редактора редактировать сам указатель , Я исправил пост. Спасибо Джону Боллинджеру за указание на это.

Ответы [ 3 ]

1 голос
/ 17 марта 2020

Из jni.h есть комментарий:

Мы используем встроенные функции для C ++, чтобы программисты могли писать: env->FindClass("java/lang/String") на C ++, а не: (*env)->FindClass(env, "java/lang/String") в C.

Например, в struct JNIEnv_, если c ++, есть метод:

jclass FindClass(const char *name) {
    return functions->FindClass(this, name);
}

У класса есть указатель:

const struct JNINativeInterface_ *functions;

[который является единственным видимым для C]. Это указатель на таблицу виртуальных функций.

Итак, AFAICT, правильное значение deref [для C]:

env->functions->FindClass(env,name)

Это то, как функция-член выполняет вызов, а также соответствует приведенному выше цитируемому комментарию.

Итак, вы уверены , что:

env->functions->FindClass(*env,name)

работает?

Просто так получается, что (*env)->FindClass(env,name) работает потому что functions является первым элементом [и для C единственным элементом].

Итак, для меня я бы создал макрос, который выполняет разыскивание:

#define DEREF(_env) ((_env)->functions)
DEREF(env)->FindClass(env,name)
1 голос
/ 18 марта 2020

TL; DR : множество факторов, среди которых двоичная совместимость, поддержка многопоточности и требуемые характеристики интерфейса, привели к разработке JNI и парадигмы вызова C, которая следует из него.

C JNI вызывает идиому

, так зачем нас заставлять

(*env)->NewGlobalRef((*env), my_object);

? Дополнительный уровень косвенности не передается в функции в качестве первого аргумента, поэтому они не могут обновить этот указатель.

Это неправильная форма для вызова JNI, Как свидетельствует спецификация , и это действительно ясно (i sh) из распределенных заголовочных файлов JNI, правильная форма будет

JNIEnv *env = /* ... */;

(*env)->NewGlobalRef(env, my_object);

Обратите внимание, что

  1. В API C JNIEnv сам по себе является типом указателя (как необходимо для применения оператора -> к *env), и особенно
  2. Это само по себе env , а не *env, который передается в функцию JNI. Таким образом, дополнительная косвенность равна , передаваемой функциям JNI , вопреки утверждению вопроса.

Соображения JNI

Имея Потребовалось некоторое время, чтобы подумать об этом и прочитать некоторые информативные разделы спецификации JNI , однако я передумал о том, что дизайн специально предназначен для создания чистого интерфейса C ++ за счет чуть менее чистого C интерфейса. Единственная техническая причина, по которой интерфейс JNI C должен быть спроектирован для использования вызывающей идиомы, это то, что функции JNI могут заменять одну среду на другую, но нет никаких причин думать, что любая функция JNI делает это или что предполагалось, что любая когда-либо будет. (Больше комментариев об этом выборе дизайна позже.)

Глава 1 спецификации содержит столько официальной истории, сколько доступно. В нем обсуждаются историческая справка и цели проектирования для текущей (второй) основной итерации JNI. В частности, позиция Sun заключалась в том, что хорошо разработанный интерфейс будет иметь следующие преимущества:

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

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

  • Двоичная совместимость - основная цель - двоичная совместимость нативных библиотек методов для всех Java Реализация ВМ на данной платформе. Программисты должны поддерживать только одну версию своих собственных библиотек методов для данной платформы.
  • Эффективность - Для поддержки критичного ко времени кода интерфейс собственных методов должен накладывать небольшие накладные расходы. Все известные методы, обеспечивающие независимость от ВМ (и, следовательно, двоичную совместимость), несут определенную нагрузку. Мы должны каким-то образом найти компромисс между эффективностью и независимостью от виртуальной машины.
  • Функциональность - интерфейс должен предоставлять достаточно Java внутренних компонентов виртуальной машины, чтобы нативные методы могли выполнять sh полезных задач.

Документы express высоко ценят COM как интерфейсную технологию, которая решает эти задачи, и, действительно, Microsoft создала интерфейс COM для своей Java 1 VM. Но, конечно, у COM также есть некоторые проблемы, не только в отношении технических деталей по сравнению с Java, но также и с незначительным (не) наличием на интересующих платформах, включая собственную Sun Solaris. Следовательно, и я думаю, что это может быть верным ответом на поставленный вопрос:

Хотя объекты Java не представлены в собственном коде как объекты COM, сам интерфейс JNI является двоично-совместимым с COM. JNI использует ту же структуру таблицы переходов и соглашение о вызовах, что и COM. Это означает, что, как только станет доступна кроссплатформенная поддержка COM, JNI может стать интерфейсом COM для Java VM.

(Акцент в оригинале .)

Окончательный проект JNI

В спецификации дается высокоуровневое описание того, что означает наличие COM-конгруэнтной формы , причем ключевая часть:

Собственный код обращается к Java функциям виртуальной машины путем вызова функций JNI. Функции JNI доступны через указатель интерфейса. Указатель интерфейса - это указатель на указатель. Этот указатель указывает на массив указателей, каждый из которых указывает на интерфейсную функцию. Каждая интерфейсная функция имеет предопределенное смещение внутри массива.

Это именно то, что мы на самом деле видим, и spe c переходит к express, как это похоже на C ++ таблица виртуальных функций и интерфейс COM. Также поясняется, что использование таблицы функций имеет следующие преимущества:

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

Кроме того, он объясняет, что предоставление двойного указателя на таблицу функций облегчает представление разных таблиц в разных потоках:

Указатель интерфейса JNI действителен только в текущем потоке. Поэтому нативный метод не должен передавать указатель интерфейса из одного потока в другой. ВМ, реализующая JNI, может выделять и хранить локальные данные потока в области, указанной указателем интерфейса JNI.

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

(«Указатель интерфейса JNI» - это вышеупомянутый двойной указатель, тип которого выражен в C JNI как JNIEnv *.)

Заключение

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

То же самое происходит и в C ++ API, но его маскируют, заключая указатель таблицы функций в класс и маскируя указатель интерфейса JNI как указатель на экземпляр этого класса. Это также дает возможность предоставлять функции-оболочки, которые маскируют тот факт, что указатель интерфейса JNI передается функциям JNI. Я воспринимаю это как хорошее использование возможностей C ++ для обеспечения простого и естественного интерфейса на этом языке, а не как свидетельство C ++ - первого подхода к проектированию JNI.

1 голос
/ 17 марта 2020

JNIEnv на самом деле не указатель на указатель, а на структуру данных, содержащую другую (приватную) информацию, специфичную для потока c. JNINativeInterface* - это только первое поле в структуре, а остальные не публикуются c. Это обеспечивает большую гибкость в реализации виртуальных таблиц функций JNI в VM.

Некоторые ссылки приведены здесь для удобства тех, кто может столкнуться с этим:

  1. Потоки и JNI - здесь объясняется:

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

  2. JNI spe c

...