LINUX.ORG.RU

Вызов native-кода из Java посредством CNI

 cni, , , ,


1

3

Всем здравствуйте.

Развлекаюсь со стюардессой.

GNU Compiler for Java предоставляет два способа вызвать native-код из Java.

  1. Во-первых, есть спецификация JNI, разработанная Sun Microsystems, и GCJ вполне следует этой спецификации.
  2. Во-вторых, есть стандарт CNI, или Compiled Native Interface, реализованный одним лишь GCJ, к которому почти нет документации, но который позволяет писать native-код сразу на C++, предоставляя более высокоуровневый API для взаимодействия между native-кодом и JVM.

Поскольку GCJ также умеет выполнять AOT-компиляцию JVM-байткода, у нас появляются следующие возможные подходы к вызову native-кода из Java:

  1. Запустить обычную JVM с помощью интерпретатора байткода gij, загрузить в неё Java-класс, в нём загрузить native-библиотеку посредством System.loadLibrary(), и вызвать native-метод. Это традиционный путь, привычный нам по Sun/Oracle JVM.
  2. То же, что и в предыдущем пункте, но предварительно выполнить AOT-компиляцию байткода JVM посредством gcj и выполнить динамическое связывание получившегося исполняемого файла с native-библиотекой, используя стандартный ключ -l. Вызов System.loadLibrary() можно оставить как есть из соображений совместимости, но, фактически, необходимость в нём отпадает, поскольку к моменту запуска JVM native-библиотека уже загружена динамическим интерпретатором (ld.so). Здесь есть варианты:
    • Загрузить библиотеку средствами JVM, через System.loadLibrary() — это прямая аналогия с вызовом dlopen().
    • Выполнить динамическое связывание с библиотекой средствами ld, который вызывается процессом gcj -l во время AOT-компиляции. Это значительно удобнее, чем loadLibrary(), поскольку gcj «понимает» многие стандартные ключи gcc, и ему можно «скормить», в частности, -rpath, что в дальнейшем избавит нас от необходимости вручную выставлять LD_LIBRARY_PATH перед запуском программы.
    • Неочевидно, но можно вообще не выполнять никакого связывания с библиотекой и просто положить её в LD_PRELOAD. Да, нормальные люди так не делают, но тоже вариант.
  3. Наконец, можно статически связать объектный файл, получившийся в результате AOT-компиляции байткода, с объектным файлом, являющимся результатом компиляции native-кода. Это полностью избавляет нас от необходимости вызывать System.loadLibrary(), поскольку уже нет никакой внешней библиотеки, a код native-метода является частью кода исполняемого файла.

Поскольку у нас есть два «протокола» вызова native-кода (JNI и CNI), в теории это удваивает количество возможных вариантов (с 3 до 6).

По результатам экспериментов, CNI прекрасно работает со статическим связыванием, но вот в случае с динамическим (и AOT-компиляцией) я получаю ошибку.

Java-класс:

public final class Main {
        static {
                System.loadLibrary("foo");
        }

        private Main() {
                assert false;
        }

        public static native void foo();

        public static void main(final String args[]) {
                foo();
        }
}

Сгенерированный header-файл в формате CNI (Main.h):

#ifndef __Main__
#define __Main__

#pragma interface

#include <java/lang/Object.h>
#include <gcj/array.h>

extern "Java"
{
    class Main;
}

class Main : public ::java::lang::Object
{

  Main();
public:
  static void foo();
  static void main(JArray< ::java::lang::String * > *);
public: // actually package-private
  static jboolean $assertionsDisabled;
public:
  static ::java::lang::Class class$;
};

#endif // __Main__

… и реализация C++-класса, который попадёт в нашу внешнюю библиотеку:

#include <iostream>

#include <gcj/cni.h>

#include "Main.h"

void Main::foo() {
        std::cout << "Hello, World!" << std::endl;
}

При связывании объектного файла со внешней библиотекой:

gcj -pie -fPIE -save-temps --main=Main -o cni-dynamic-native -L/opt/gcc64/6.5/lib64 -Wl,-rpath=/opt/gcc64/6.5/lib64 -L. -Wl,-rpath='$ORIGIN' -lstdc++ -lgcj -lfoo Main.class

… имеем:

/usr/bin/ld: Main.o: in function `void Main::main(JArray<java::lang::String*>*)':
Main.java:13: undefined reference to `hidden alias for void Main::foo()'
/usr/bin/ld: Main.o:(.data.rel+0xc0): undefined reference to `hidden alias for void Main::foo()'
collect2: error: ld returned 1 exit status

Действительно, если посмотреть на символы внутри Main.o с помощью nm, есть один неопределённый:

                 U hidden alias for void Main::foo()

Этот символ определён во внешней библиотеке (libfoo.so), с которой выполняется связывание. Но в объектном файле, из которого собирается внешняя библиотека, этот символ глобальный (T), а в самой библиотеке уже статический (t), так что показанная выше ошибка ld вполне закономерна.

0000000000001180 t hidden alias for void Main::foo()

Ровно та же самая проблема описана вот в этом багрепорте, но, увы, никакого решения там не предложено. Как уже было сказано, всё прекрасно работает при статическом связывании в режиме CNI или динамическом в режиме JNI (пример кода).

Как динамически связать внешнюю native-библиотеку с результатом AOT-компиляции байткода, используя CNI? Предназначен ли механизм CNI для динамического связывания?

★★★★★

Последнее исправление: Bass (всего исправлений: 1)
Ответ на: комментарий от ratvier

Я знаю. Жду.

В смысле AOT — мне ещё с Excelsior Jet довелось поработать.

Но у него стандартная версия для Linux умела создавать только 32-разрядные бинарники (и только для x86).

Вообще, в смысле спектра поддерживаемых программно-аппаратных платформ GCJ был уникален. Если говорить про современные аналоги, то GraalVM доступна лишь для Linux (что там с NetBSD/OpenBSD — неведомо), и только для x64 и aarch64.

Bass ★★★★★
() автор топика
Последнее исправление: Bass (всего исправлений: 3)
Ответ на: комментарий от HE_KOT

Спасибо, дружище.

Порекомендуй мне ознакомиться с другми языками программирования, а ещё с другими видами деятельности человека. Говорят, управлять самолётом тоже интересно =)

Bass ★★★★★
() автор топика
Последнее исправление: Bass (всего исправлений: 2)
Ответ на: комментарий от lovesan

Читал какую-то статью на Википедии — кажется, чтобы заснуть.

Пошёл по ссылкам, дошёл до статьи о CNI, стало интересно, решил разобраться.

Никакого «боевого» кода, разумеется, я писать не намерен.

Bass ★★★★★
() автор топика
Ответ на: комментарий от HE_KOT

Есть ещё KNI в JBlend (это японская J2ME VM которую лицензировали крупным компаниям типа Sony Ericsson и Motorola), а ещё интересная инфа что JBlend это форк IBM J9.

EXL ★★★★★
()