Совместная отладка Java- и C/C++-кода. Два подхода к использованию JNI

Совместная отладка Java- и C/C++-кода. Два подхода к использованию JNI

Каким образом вы собираетесь проводить эффективную отладку вашего приложения, если в его реализации нет возможности обойтись чистым Java-кодом, и приходится использовать другие языки (например, C/C++), тем более, что пока не существует хороших отладчиков, способных производить проверку и отладку подобных приложений-гибридов. В этой статье мы, используя лишь инструменты командной строки (command-line), рассмотрим основные приемы работы и решения проблем, с которыми многие сталкиваются при отладке мультиязыковых приложений. По прочтении этой статьи вы узнаете, как запускать приложения совместно с отладчиками, какие существуют отладчики, а также овладеете техникой реализации эффективной отладки.

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

Примечание: При написании этой статьи использовались ОС Windows 2000, Java Debugger (JDB) и GNU Debugger (GDB). Однако все рассмотренные в этой статье приемы могут быть с легкостью применены к UNIX/Linux-платформам. Более подробную информацию о каждом из упомянутых отладчиков можно получить из списка ресурсов в конце статьи.

В чем, собственно, состоит проблема? Операционная система воспринимает виртуальную Java-машину (JVM) как еще одно приложение. Следовательно, подход у операционной системы к JVM такой, как и к обычному приложению. Любой процесс отладки, при котором используется Java-отладчик (JDB), — это, с точки зрения операционной системы, процесс, происходящий на уровне приложения. Другое дело — родные приложения, для отладки которых используются более привилегированные операции. (Для простоты далее будем употреблять native вместо слова "родной").

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


Что такое JNI?


Изначально люди, проектировавшие язык программирования Java, понимали, что возможность создания на 100% чистых Java-приложений — благородная цель. Но они также понимали, что будет очень трудно обойтись только лишь средствами языка Java. По мере того, как разрабатываются все новые Java-приложения, разработчикам часто очень не хочется выбрасывать старый native-код, который может быть достаточно дорогим и который было нелегко написать.

JNI (Java Native Interface) предоставляет разработчику возможность выйти за рамки, определенные виртуальной Java-машиной, и получить доступ ко всем возможностям компьютера, на котором запущена Java-машина. Таким образом, разработчики могут использовать свой старый код или писать новый, который невозможно было бы реализовать средствами языка Java. Главный недостаток JNI состоит в том, что при его использовании теряется суть идеи WORA (Write Once, Run Anywhere), поскольку любой native-код прочно связывает ваше Java-приложение с какой-либо платформой. Чтобы получить более подробную информацию о JNI, посмотрите список ресурсов в конце этой статьи.

Существует по крайней мере две основных структуры построения приложений с использованием JNI. Первый из этих способов — стандартный, а второй, соответственно, нестандартный, основывающийся на вызове Java-машины после отработки некоторого native-приложения.

Сущность стандартного подхода к написанию JNI-приложений состоит в том, что вы создаете некоторую native-библиотеку и позже используете ее в своем Java-приложении. При таком подходе ваше Java-приложение может быть запущено привычным для всех образом, например, следующей командой: java myApplication.

Чтобы посмотреть на примере, как работает этот стандартный метод использования JNI, скачайте архив с кодом по адресу, который указан в списке ресурсов в конце статьи. Файл называется std.zip и содержит следующие файлы:

  • jnitest.c
  • JNITest.class
  • jnitest.dll
  • JNITest.h
  • JNITest.java
  • jnitest.o
  • libtstdll.a
  • Непосредственно Java-код содержится в файле JNITest.java. В нем содержится метод main(), который загружает narive-библиотеки, а также методы instance() и static(). Помимо этого здесь также определяется метод native() следующим образом: public native int native_doubleInt(int value).

    Файл jnitest.c содержит исходный C-код. Здесь определена реализация метода native_doubleInt().

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

    Для реализации такого подхода необходимо воспользоваться Invocation API, который позволяет вам загружать JVM в произвольное native-приложение. Вообще каждое Java-приложение запускается с использованием Invocation API. Сначала java.exe, программа-загрузчик, использует Invocation API для того, чтобы запустить виртуальную машину, после чего запускает основной класс. То, насколько правильное у вас представление о запуске Java-программ, определит глубину восприятия материала, изложенного в этой статье.

    В разделе Ресурсы в конце статьи вы также можете найти адрес архива с примером для такого invocation-подхода. Архив называется invocation.zip и содержит следующие файлы:

  • jvm.def
  • libjvm.a
  • main.c
  • main.exe
  • main.o
  • В этом примере используется тот же Java-код, что и в примере стандартного подхода. main.c — это C-код, который компилируется в исполняемый файл, в нашем случае — main.exe.

    Метод goTest() вызывает статический метод Java-класса. (Он был создан статическим, с тем чтобы сделать JNI-код несколько проще). Непосредственно перед вызовом метода goTest() выполняется бесконечный цикл. Этот цикл был введен для отладочных целей. Зачем? Это станет понятно позже.


    Инструменты


    Здесь приведено несколько примеров отладчиков Java и C/C++ (как open-source, так и коммерческих), которые вы можете использовать для отладки ваших Java/C-приложений. Заметьте, что как Java-, так и C/C++-приложения должны быть скомпилированы вместе с отладочной информацией. Для этого вам нужно при компиляции добавить несколько ключей. В случае с Java это ключ —g для компилятора javac. Для C вам необходимо ознакомиться с опциями используемого вами компилятора: посмотрите, например, раздел этой статьи "GCC-компиляция", в которой описан процесс компиляции с помощью GCC.

    Java-отладчики:

  • JDB — это стандартный отладчик, включенный в поставку JDK, который был использован также при написании этой статьи.
  • IBM VisualAge for Java поставляется со своим отладчиком. Мощный графический отладчик может использоваться отдельно от основного инструмента.
  • JSwat — великолепный open-source графический Java-отладчик.
  • C/C++- отладчики:

  • GDB — GNU Debugger — именно он использовался при написании данной статьи.
  • GCC и смежные с ним инструменты содержат множество портированных вещей для Windows. Один из них — Cygwin, который предоставляет большое количество UNIX-подобных инструментов, которыми вы можете воспользоваться. Другой — MinGW (Minimalist GNU for Windows), небольшой инструментарий, но уже без абстрактного слоя, который предоставляет Cygwin.
  • Множество коммерческих инструментов, например, VisualAge, Microsoft Visual C++, Borland, Code Warrior и др., предлагают удобный графический интерфейс для отладчиков, что делает их более простыми в использовании. Отладка программы первого типа

    Итак, давайте приступим к работе. Представьте, что вам нужно запустить вашу Java-программу и после присоединить к этому процессу C-отладчик. Такое можно организовать с помощью отладчиков, как, например, Visual C++, запустив исполняемый файл Java напрямую. В этом случае вам необходимо добавить вашу DLL в список дополнительных DLL-библиотек; в противном случае, когда ваше приложение будет запущено, точка прерывания установлена не будет.

    Java-приложение содержит вызов System.loadLibrary, который будет динамически загружать нашу DLL. (Замечание: Этот пример демонстрирует, как производить отладку с совместным использованием JDB и GDB. Если вы не хотите использовать JDB, то можете убрать режим ожидания пользовательского ввода из Java-приложение после вызова System.loadLibrary). Итак, процесс отладки укладывается в следующие 7 шагов:

    1. Запуск Java-приложения в отладочном режиме.
    2. Из другого окна привязываем JDB к JVM.
    3. Устанавливаем точку прерывания.
    4. Привязываем GDB к JVM.
    5. Используем JDB, чтобы продолжить работу JVM.
    6. Выпускаем JVM через окно GDB.
    7. Проходимся отладчиком по native-коду.
    Давайте рассмотрим каждый из этих шагов более подробно.

    Шаг 1. Первый шаг заключается в том, чтобы запустить Java-программу в отладочном режиме и присоединить к этому процессу Java-отладчик. В следующих примерах мы использовали Java Platform Debugger Architecture (JPDA) вместо более старого отладочного Java-интерфейса.

    Листинг 1. Использование JPDA для запуска Java-приложения в отладочном режиме


    C:\_jni\std>java —Xdebug -Xnoagent -Djava.compiler=none

    -Xrunjdwp:transport=dt_socket,server=y,suspend=y JNITest

    Listening for transport dt_socket at address: 1060

    Команда suspend=y остановит виртуальную Java-машину, как только та подготовит порт, к которому может подключиться Java-отладчик.

    Шаг 2. Из другого окна присоединяем JDB к только что запущенной JVM. JDB должен иметь доступ к исходному коду и файлам классов отлаживаемого приложения. Пути к ним могут быть определены в командной строке JDB с помощью ключей —sourcepath и —classpath. Как альтернативный вариант можно запускать JDB из директории, где находятся все эти файлы, тогда отпадает необходимость определять все эти опции с указанием путей.

    Листинг 2. Привязка JDB к JVM

    C:\_jni\std>jdb -attach 1060

    Initializing jdb...

    main[1]

    VM Started: No frames on the current call stack

    main[1]

    Шаг 3. Теперь мы можем установить точку прерывания в строке сразу после вхождения вызова System.loadLibrary.

    Листинг 3. Устанавливаем точку прерывания

    main[1] stop at JNITest:22

    Deferring breakpoint JNITest:22.

    It will be set after the class is loaded.

    main[1] run

    > Set deferred breakpoint JNITest:22

    Breakpoint hit: thread="main", JNITest.main(), line=22, bci=21

    22 System.out.println(" ##### About to call native method ");

    main[1]

    Заметьте, что существует два пути установки точек прерывания в JDB. Первый — при помощи команды stop in, а второй — командой stop at. Командой stop at вы можете устанавливать точку прерывания в определенной строке; stop in же используется для установки точек прерывания в определенных методах и классах.

    На этом этапе JVM временно остановила свою работу, но с точки зрения операционной системы это приложение продолжает нормально функционировать. ОС (операционная система) также заметила связь, установленную между процессом JDB и процессом виртуальной Java-машины.

    Шаг 4. Теперь мы можем присоединить GDB к процессу JVM. Для того чтобы это сделать, нам нужно узнать идентификатор этого процесса (PID). Чтобы его получить, вы можете воспользоваться менеджером задач или одним из инструментов, предназначенных для этих целей, например, listdlls (из SysInternals.com). Опции —nw и —quiet позволяют запустить GDB без графического интерфейса и без вывода информации о его версии.

    Листинг 4. Привязка GDB к процессу JVM

    C:\_jni\std>gdb -nw -quiet

    (gdb) attach 1304

    Attaching to process 1304

    [Switching to thread 1304.0x2b8]

    (gdb) break jnitest.c:15

    Breakpoint 1 at 0x100010cf: file jnitest.c, line 15.

    (gdb)

    Шаг 5. Теперь в GDB установлена точка прерывания. Воспользовавшись JDB, нам нужно дать возможность JVM продолжать свою работу. (Не забывайте, что мы также установили точку прерывания и в GDB) Наберите cont в окне с JDB. Теперь JDB позволил JVM продолжить свое нормальное функционирование.

    Шаг 6. Наберите cont в окне GDB, чтобы "отпустить" JVM. Заметьте: мы достигли установленной точки прерывания.

    Листинг 5. "Отпускаем" JVM

    (gdb) cont

    Continuing.

    [Switching to thread 1304.0x550]

    Breakpoint 1, Java_JNITest_native_1doubleInt (pEnv=0x2346c8, obj=0x6fe20,

    value=42) at jnitest.c:15

    15 printf(" ***** Entering C code\n");

    (gdb)

    Шаг 7. Теперь вы можете проходиться отладчиком по native-коду, что, собственно, и требовалось. Если вы вернетесь в окно с JDB и введете "stop at JNITest:26", чтобы приостановить приложение до того, как оно вызовет native-метод во второй раз, оно не будет отвечать. Чтобы это сделать, вам следует сначала сделать это в GDB.

    Проход по native-коду и JNI-вызовам в GDB может показаться сложным. Вам может понадобиться дважды вводить next, чтобы проходить по вызовам.

    Лучше всего расставлять точки прерывания, что позволит заметно ускорить процесс отладки, поскольку JNI-вызовы могут занимать некоторое время на выполнение при работе в GDB-окружении.

    Отладка программы второго типа

    В случае использования второго метода написания JNI-приложений вам будет гораздо проще отлаживать C-код. Вы можете запускать приложение прямо в своем отладчике. Однако для отладки Java-кода, вам понадобится установить некоторые опции виртуальной Java-машины. Вы можете сделать это в тот момент, когда запускаете приложение с использованием Invocation API.

    Заметьте, что опции в этом случае такие же, как и в предыдущем примере в листинге 1. Если вы напрямую запустите эту программу, то получите что-то похожее:

    Листинг 6. C-код вызывает Java-метод, чтобы утроить число

    Listening for transport dt_socket at address: 1068

    #### java

    В этом примере мы использовали C-код, чтобы вызвать Java-метод, который утраивает числа. Теперь нам нужно привязать JDB к этому методу (порт, к которому нужно привязывать JDB, мы уже получили). Самое сложное в таком стиле отладки — позволить JDB обеспечить полную взаимосвязь с JVM, которая находится под контролем GDB.

    Самый простой способ решить эту проблему — бесконечный цикл. Вы можете написать этот цикл без выполнения каких-либо дополнительных команд и логики — главное, чтобы он позволил JVM делать что-нибудь в тот момент, когда JDB будет устанавливать соединение. Для этого нам понадобится выполнить следующие шаги:

    1. Загрузка приложения в GDB; установка точки прерывания.
    2. Повторять проход по циклу в GDB.
    3. С помощью GDB прервать цикл и продолжить отладку.
    4. Проследить, как JDB остановится в точке прерывания.
    Рассмотрим каждый из этих шагов более подробно.

    Шаг 1. Загружаем приложение в GDB и устанавливаем точку прерывания. Таким образом мы сможем перемещаться по циклу. Хотя этот цикл не должен присутствовать в программах: он введен лишь в отладочных целях. Основная идея состоит в том, чтобы дать время JVM соединиться с JDB.

    Шаг 2. Продолжаем проходить цикл в GDB, чтобы установить точку прерывания на Java-методе, который утраивает число. Просто продолжаем выполнять цикл, пока JDB отвечает. Мы могли бы сделать этот процесс более сложным, используя дополнительные sleep, например.

    Листинг 7. Пока JDB отвечает — продолжаем проход по циклу

    main[1] main[1] stop at JNITest:58

    Deferring breakpoint JNITest:58.

    It will be set after the class is loaded.

    main[1]

    Шаг 3. Как только вы выполнили второй шаг, можете прерывать цикл в GDB и продолжать отладку. Здесь мы устанавливаем точку прерывания в конец метода goTest(), чтобы дать возможность выполниться всем JNI-вызовам.

    Листинг 8. Точка прерывания позволяет выполнить все JNI-вызовы быстрее

    (gdb) print loop

    $1 = 0

    (gdb) set loop=1

    (gdb) next

    42 goTest(pEnv);

    (gdb) step

    goTest (pEnv=0x3f4e58) at main.c:53

    53 javaClass = (*pEnv)->FindClass(pEnv,"JNITest");

    (gdb) list

    48

    49 jclass javaClass;

    50 jmethodID mid;

    51 jint result;

    52

    53 javaClass = (*pEnv)->FindClass(pEnv,"JNITest");

    54

    55 mid = (*pEnv)->GetStaticMethodID(pEnv,javaClass,"java_tripleNumber","(I)I");

    56

    57 result = (*pEnv)->CallStaticIntMethod(pEnv,javaClass,mid,42);

    (gdb) break main.c:58

    Breakpoint 2 at 0x401474: file main.c, line 58.

    (gdb) c

    Continuing.

    Шаг 4. Когда выполняется код, приведенный в предыдущем шаге, в окне JDB произойдет останов на точке прерывания.

    Листинг 9. JDB останавливается в точке прерывания

    Set deferred breakpoint JNITest:58

    Breakpoint hit: thread="main", JNITest.java_tripleNumber(), line=58, bci=0

    58 System.out.println(" #### java ");

    main[1]

    Непонятные JNI-ссылки

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

    В этом куске мы остановили C-код сразу после вызова Java-метода. result содержит ссылку на jstring.

    Листинг 10. JNI-ссылка на jstring

    Breakpoint 3, goTest (pEnv=0x3f4e50) at main.c:60

    60 }

    (gdb) print result

    $1 = 0x3fda44

    В листинге 10 показан адрес переменной в памяти (0x00ad1ac8). Если вы распечатаете содержимое памяти начиная с этой позиции, то увидите начало строки. GDB из поставки Cygwin предлагает графический интерфейс, в котором есть окно для редактирования памяти — оно значительно упростит вам просмотр этой строки.

    Листинг 11. Наблюдаем строку в памяти

    (gdb) x 0x3fda44

    0x3fda44: 0x00ad1ac8

    (gdb) x/30s 0x00ad1ac8

    0xad1ac8: "0021"

    0xad1acc: ""

    0xad1acd: ""

    0xad1ace: ""

    0xad1acf: ""

    0xad1ad0: "032-"

    0xad1ad4: ""

    0xad1ad5: ""

    0xad1ad6: ""

    0xad1ad7: ""

    0xad1ad8: "\022"

    0xad1ada: ""

    0xad1adb: ""

    0xad1adc: "0"

    0xad1ade: ""

    0xad1adf: ""

    0xad1ae0: "\022"

    0xad1ae2: ""

    0xad1ae3: ""

    0xad1ae4: "*"

    0xad1ae6: ""

    0xad1ae7: ""

    0xad1ae8: "C"

    0xad1aea: "a"

    0xad1aec: "t"

    0xad1aee: " "

    0xad1af0: "s"

    0xad1af2: "a"

    0xad1af4: "t"

    0xad1af6: " "

    (gdb)

    Компиляция с помощью GCC

    В стандартную поставку Java не входят библиотеки для GCC, поэтому процесс компиляции несколько усложняется. Например, в приведенном ниже примере, воспользовавшись инструментарием MinGW, мы используем следующую последовательность команд:

    1. Собираем библиотеку для GCC, базирующуюся на def-файле. Создаем файл с именем jvm.def со следующим содержимым:

    2. EXPORTS

      JNI_CreateJavaVM@12

      Если вы запустите одну из утилит, например, dumpbin или nm для libjvm-файла, то сможете извлечь ссылки на другие Invocation API.

    3. Создаем библиотеку для вашего приложения.
      dlltool -k --input-def jvm.def --dll jvm.dll --output-lib libjvm.a
    4. Компилируем ваше C-приложение
      gcc -Ic:\_jdk\include -g -c main.c
    5. Связываем вместе основное приложение и JVM
      gcc -o main.exe main.o -L./ -ljvm
    Несколько приемов программирования JNI-приложений напоследок

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

  • Всегда проверяйте возвращаемые значения и возникающие исключения при JNI-вызовах.
  • Когда вы проверяете на факт возникновения исключения, важно знать, что, если вы вызываете метод ExceptionDescribe(), то можете получить описание исключения, которое возникло несколько раньше, чем только что произошедшее.
  • Всегда вызывайте метод threadDetach(), прежде чем уничтожать какой-нибудь из ваших потоков. Не руководствуясь этим правилом, вы можете свалить на свою голову множество серьезных проблем, связанных со "сборщиком мусора".
  • Когда вы запускаетесь посредством JPDA, всегда используйте java.compiler=NONE. Если вы используете java.compiler=fred, это остановит JIT, но фактически не будет работать.
  • За более детальными инструкциями вы можете обращаться к книге "The Java Native Interface" (Sheng Liang) или на JNI-раздел java.sun.com. Если вы предпочтете книгу "Essential JNI" (которая очень хорошо освещает основы JNI), учтите, что в ней приведены функции для Java 2, которые основываются на бета-версии JDK.

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

    Ресурсы

    1. Архив с примером для приложения первого типа (std.zip): ftp://www6.software.ibm.com/software/developer/library/j-jnidebug/std.zip .
    2. Архив с примером для приложения второго типа (invocation.zip): ftp://www6.software.ibm.com/software/developer/library/j-jnidebug/invocation.zip .
    3. Следующее руководство "шаг за шагом" IBM Distributed Debugger (http://www7.software.ibm.com/vad.nsf/Data/Document2346?OpenDocument&p=1&BCT=3&Footer=1&origin=j) содержит множество сценариев использования различных отладчиков, примеры локальной и удаленной отладки, а также инструкции, как привязать отладчик к работающей виртуальной машине.
    4. JNI-форум на jGuru (http://www.jguru.com/faq/JNI) (управляемый человеком по имени Davanum Srinivas) является местом активных бесед на любые темы, касающиеся JNI и его применения.
    5. http://gcc.gnu.org/ — здесь вы можете получить много полезной информации о GCC и смежных инструментах и утилитах.
    6. SysInternals.com — сайт бесплатного программного обеспечения для разработки под Windows; содержит такие великолепные утилиты, как Process Explorer (GUI-утилита, которая позволяет пользователям видеть DLL-библиотеки и загруженные процессы) и ListDLLs (утилита, которая отображает DLL-библиотеки, используемые загруженным процессом).
    7. MinGW (http://www.mingw.org/) — это коллекция header-файлов и библиотек, которые позволяют разработчикам использовать GCC и получить native-код Windows32-программ, которые не зависят от DLL-библиотек третьих производителей.
    8. Cygwin (http://www.cygwin.com/) — это UNIX-среда для Windows, которая состоит из DLL-библиотек, которые эмулируют UNIX API, а также набор портированных инструментов, позволяющих приблизить эмуляцию к оригиналу.
    9. http://www-106.ibm.com/developerworks/java/ — содержит много полезной информации (статей и инструментов) для отладки Java-приложений, использующих JNI и не только.

    По материалам Matthew B. White
    Подготовил Алексей Литвинюк,
    http://www.litvinuke.hut.ru


    Компьютерная газета. Статья была опубликована в номере 14 за 2003 год в рубрике программирование :: java

    ©1997-2024 Компьютерная газета