Zorkus
@Zorkus

Загрузка двух нативных библиотек через JNI под линуксом

Недавно по работе появилась редкая для меня задача, излагаю. Скажу сразу, решения в лоб пока не нашел, а workaround-ы мне известны, но для них требуется помощь от поставщика одной из сторонних библиотек, а потому задача более чем актуальна.

Знатоки JVM И юникса — VERY welcome…

Итак, задача. Есть две нативные либы, А и Б, или, в нейминга линукса, это libA.so и libB.so. А зависит от Б (должна вызывать функции из нее). А — написана мной, Б — предоставлена сторонними разработчиками в виде одного жалкого .so файла, доступа к исходникам нет.
Мне нужно загрузить эти две либы в JVM через JNI и вызвать функцию из библиотеки А, которая, в свою очередь, использует функцию из Б.

Следующий код:

static {
System.loadLibrary(B);
System.loadLibrary(A);
}

Отлично работает в Windows (для .dll файлов, соотетственно), при условии, что обе библиотеки лежат в java.library.path (я тут кстати раньше в блоге писал, как можно менять java.library.path в рантайме).
Как это работает в Windows — загружается B.dll, дальше A.dll пробует загрузиться, видит, что надо бы загрузить B.dll, от которой она зависит, видит, что B.dll в память процесса уже загружена, и не пытается найти и загрузить B самостоятельно, ища ее в PATH.

Теперь, что, по видимому, происходит в линуксе.
Дополнительный логгинг показывает, что libB.so корректно находится в java.library.path и загружается. Затем, рантайм-линкер пытается загрузить libA.so, и замечает, что ВНЕЗАПНО, libB.so в LD_LIBRARY_PATH нет! А менять LD_LIBRARY_PATH я не хочу, не то что неспортивно, но хочется локально решать проблемы. Чтобы уточнить — обе библиотеки находятся в директории, которая входит в java.library.path, но НЕ входит в LD_LIBRARY_PATH).

У кого-нибудь есть какие-нибудь идеи?
Перепробовал немало хаков уже, включая линковку libA.so с уничтожением таблицы символов, загрузку libB.so динамически из libA.so, получение указателя на нужную функцию (манглированную) и вызов ее — не работает ничего из этого так, как мне надо.

Еще раз уточню условия — я не хочу модифицировать LD_LIBRARY_PATH, я не хочу класть свои либы в «общеизвестные» места типа /usr/lib.
Вариант добыть вместо libB.so статически либу (libB.a) и влинковать ее жестко в libA.so мне вариант кажется хорошим, но исходников нет.
Сконвертировать libB.so -> libB.a? Теоритически наверное, можно.
  • Вопрос задан
  • 4051 просмотр
Пригласить эксперта
Ответы на вопрос 8
TheShade
@TheShade
Ну ведь нативному линкеру нужно знать, откуда взять либу, так? JVM тут вообще не при делах, модифицируйте LD_LIBRARY_PATH ;)
Ответ написан
Zorkus
@Zorkus Автор вопроса
В итоге я решил эту проблему следующим образом: оказалось, что обе библиотеки надо линковать с флагом -Wl,soname. Поскольку у меня не было доступа к исходником сторонней библиотеки, я уговорил ее создателя прислать мне объектные файлы, и слинковал ее сам :)

Всем спасибо за помощь! :)
Ответ написан
Zorkus
@Zorkus Автор вопроса
Это понятно, вопрос в том, почему под виндой нативный линкер не пытается ее взять вовсе (так как она загружена уже в память процесса JVM), а под юниксом пытается :)

Судя по коду в ClassLoader.java, на по крайней мере на уровне java код JDK и под виндой и под линуксом ссылки на кешированные библиотеки (private static Stack nativeLibraryContext = new Stack();) хранятся в любом случае одинаково.
Ответ написан
@YourChief
а разве для линковки двух либ в одну не достаточно ld? я сам этот инструмент плохо знаю, может кто ещё подскажет.
кроме того, почему нельзя запустить бинарик с LD_PRELOAD?
Ответ написан
Zorkus
@Zorkus Автор вопроса
Да, кстати, очень извиняюсь, но у меня там была опечатка (опечатка именно в посте, в коде все было правильно! :)

В посте вместо:

На самом деле, конечно, обратный порядок:
Вместо
static {
System.loadLibrary(A);
System.loadLibrary(B);
}

должно быть:

static {
System.loadLibrary(B);
System.loadLibrary(A);
}

И соотв, порядок загрузки в объяснения ниже тоже другой. Попровил пост.
Ответ написан
Zorkus
@Zorkus Автор вопроса
Еще небольшое пояснение. В коде все было верно и так, это в пост на хабре закралась ошибка.

libA.so — моя библиотека, libB.so — чужая библиотека, А слинкована с параметров -L /path-to-lib-B -lB, из явы сначала грузится B, потом А. И это не работает под линуксом (под Windows аналогичный трюк работает!), логгинг показывает следующее — System.loadLibrary(B); — работает и загружает libB.so, затем вызов System.loadLibrary(A) падает c сообщением что невозможно найти файла libB.so (хотя, эта библиотека уже загружена в память процесса).

Я сам был уверен, что это должно работать, потом так и удивился.

Пока моя гипотеза (надо посмотреть нативный код в JDK который собственно грузит библиотеки, как я понимаю это dlopen() под линуксом), при загрузке библиотеки libA.so dlopen не проверяет перед тем, как попытаться выполнить динамическую линковку библиотеки A «а загружена ли B в память процесса или нет», проверять это можно передавая в dlopen flag RTLD_NOLOAD. Возможно, еще, что это внутреннее поведение рантайм-линкера под юниксом, на который JVM никак повлиять не может.

Так или иначе — я тоже был уверен, что это должно работать, но почему-то не работает.
Ответ написан
Zorkus
@Zorkus Автор вопроса
Покопался немного в коде, интересуют комменты от разработчиков JVM. Надо может быть отдельный пост написать про это.

Итак, все знают что загружаются нативные библиотеки через класс java.lang.ClassLoader, и и точка перехода в нативный код — java.lang.ClassLoader.NativeLibrary.load.

Смотрим jdk/j2se/src/share/native/java/lang/ClassLoader.c, видим:

....
JNIEXPORT void JNICALL
Java_java_lang_ClassLoader_00024NativeLibrary_load
(JNIEnv *env, jobject this, jstring name)
{
const char *cname;
jint jniVersion;
jthrowable cause;
void * handle;

if (!initIDs(env))
return;

cname = JNU_GetStringPlatformChars(env, name, 0);
if (cname == 0)
return;
handle = JVM_LoadLibrary(cname);
if (handle) {
// разнообразные проверки и обрабока исключений
}
...


Смотрим где определена функция JVM_LoadLibrary, пролистав хедеры jvm.h, находим что-то похожее на то, что нам нужно в файле jdk/hotspot/src/share/vm/prims/jvm.cpp:

JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
//%note jvm_ct
JVMWrapper2("JVM_LoadLibrary (%s)", name);
char ebuf[1024];
void *load_result;
{
ThreadToNativeFromVM ttnfvm(thread);
load_result = hpi::dll_load(name, ebuf, sizeof ebuf);
}
if (load_result == NULL) {
char msg[1024];
jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
...
Handle h_exception =
Exceptions::new_exception(thread,
vmSymbols::java_lang_UnsatisfiedLinkError(),
msg, Exceptions::unsafe_to_utf8);

THROW_HANDLE_0(h_exception);
}


Т.е. функция dll_load уже похожа на ту, где должен идти натуральный вызов системных функций для загрузки (POSIX/WINAPI), иначе это уже слишком много уровней получается. Смотрим файл jdk/hotspot/src/os/os/linux/vm/os_linux.cpp: видим:

// Loads .dll/.so and
// in case of error it checks if .dll/.so was built for the
// same architecture as Hotspot is running on

void * os::dll_load(const char *filename, char *ebuf, int ebuflen)
{
void * result= ::dlopen(filename, RTLD_LAZY);
if (result != NULL) {
// Successful loading
return result;
}


Ха! Т.е. shared object-ы таки загружаются без добавочного флага RTLD_NOLOAD! Т.е. JVM не проверяет, перед загрузкой, загружена соотв. либа или еще нет?

Для сравнения посмотрим аналогичный код Windows, смотрим файл os_windows.cpp:

// Loads .dll/.so and
// in case of error it checks if .dll/.so was built for the
// same architecture as Hotspot is running on
void * os::dll_load(const char *name, char *ebuf, int ebuflen)
{
void * result = LoadLibrary(name);
if (result != NULL)
{
return result;
}


А теперь смотрим на документация в LoadLibrary здесь: msdn.microsoft.com/en-us/library/windows/desktop/ms682586.aspx

If a DLL with the same module name is already loaded in memory, the system checks only for redirection and a manifest before resolving to the loaded DLL, no matter which directory it is in. The system does not search for the DLL.

Т.е. поведение для линукса и виндовса будет различное в данном случае.

Читаем:
Ответ написан
Zorkus
@Zorkus Автор вопроса
По какой-то гнусной причине, в манах на kernel.org не написано, пытается ли dlopen() с флагом RTLD_NOLOAD найти .so файл в файловой системе в процессе проверки, загружена ли такая библиотека, или нет.

Все, что написано:

RTLD_NOLOAD (since glibc 2.2)
Don't load the library. This can be used to test if the library is
already resident (dlopen() returns NULL if it is not, or the library's
handle if it is resident). This flag can also be used to promote the
flags on a library that is already loaded. For example, a library that
was previously loaded with RTLD_LOCAL can be reopened with
RTLD_NOLOAD | RTLD_GLOBAL. This flag is not specified in POSIX.1-2001.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы