Всем здравствуйте.
Развлекаюсь со стюардессой.
GNU Compiler for Java предоставляет два способа вызвать native-код из Java.
- Во-первых, есть спецификация JNI, разработанная Sun Microsystems, и GCJ вполне следует этой спецификации.
- Во-вторых, есть стандарт CNI, или Compiled Native Interface, реализованный одним лишь GCJ, к которому почти нет документации, но который позволяет писать native-код сразу на C++, предоставляя более высокоуровневый API для взаимодействия между native-кодом и JVM.
Поскольку GCJ также умеет выполнять AOT-компиляцию JVM-байткода, у нас появляются следующие возможные подходы к вызову native-кода из Java:
- Запустить обычную JVM с помощью интерпретатора байткода
gij
, загрузить в неё Java-класс, в нём загрузить native-библиотеку посредствомSystem.loadLibrary()
, и вызвать native-метод. Это традиционный путь, привычный нам по Sun/Oracle JVM. - То же, что и в предыдущем пункте, но предварительно выполнить 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
. Да, нормальные люди так не делают, но тоже вариант.
- Загрузить библиотеку средствами JVM, через
- Наконец, можно статически связать объектный файл, получившийся в результате 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 для динамического связывания?