调试 Java 应用程序中的 C++ 代码






4.40/5 (4投票s)
如何在 Java 和 C++ 调试器中同时调试 Java/C++ 混合代码
引言
有很多应用程序由两部分组成。主要的 GUI 部分用 Java 编写,而性能导向的部分用 C++ 编写。GUI 部分响应用户输入,并将请求传递给通常实现应用程序核心功能的 C++ 部分。在应用程序开发过程中,可以从 Java 调试器(例如,在 Eclipse IDE 中)调试 Java GUI 部分。然而,在 Java 应用程序执行期间协同调试 Java 和 C++ 部分并不那么简单。通常,C++ 部分通过编写额外的 C++ 测试代码来调用 C++ 部分的 API,从而在 C++ 调试器中与 Java 部分分开进行调试。这是一种非常不方便且效率不高的方法。如果能在同一个应用程序运行时,同时在 Java 调试器和 C++ 调试器中调试应用程序代码,那就更好了。
在本文中,我将展示一种简单的解决方案,允许在 Unix/Linux 环境中,当应用程序流程到达 C++ 代码时,从 Java 调试器调用 C++ 调试器。
Java 和 C++ 交互
Java 允许使用所谓的 Java Native Interface (JNI
) 从 Java 代码调用 C/C++(以及其他语言)的 API。在 JNI
中,原生函数实现在单独的 *.c 或 *.cpp 文件中。(C++ 使用 JNI
提供了稍微更简洁的接口。)当 JVM
调用原生函数时,它会传递一个 JNIEnv
指针、一个 jobject
指针以及 Java 方法声明的任何 Java 参数。JNI
接口的细节超出了本文的范围。对我们来说,重要的是要提到,您想从 Java 调用的每个 C/C++ 函数/方法都必须由 C 函数(JNI
包装器)包装,该函数将直接从 Java 调用,然后调用所需的 C/C++ API。
// JNI wrapper for void MyClass::MyClassMethod(const char*) C++ method
JNIEXPORT void JNICALL Java_JNI_MyClass_MyClassMethod
(JNIEnv *env, jobject obj, jstring javaString)
{
//Get the native string from javaString
const char *nativeString = env->GetStringUTFChars(javaString, 0);
// Invoke the native C/C++ function here
MyClass *myobj = *(MyClass **) &obj;
myobj->MyClassMethod(nativeString);
//DON'T FORGET THIS LINE!!!
env->ReleaseStringUTFChars(javaString, nativeString);
}
JNI
包装器可以手动编写,也可以通过 Swig
工具从 C/C++ API 函数声明自动生成。
运行时调试 C++
我们的目标是能够在 Java 应用程序的执行流程通过 JNI
包装器进入 C++ 代码时,按需调用 C++ 调试器。在 Linux 中,可以使用进程标识符 (pid
) 将 C++ 调试器 (gdb
) 附加到已运行的进程(在本例中为 Java 应用程序)。每个 Linux 进程都有自己唯一的整数标识符,可以通过从进程内部调用 getpid()
C 系统函数来获取。此外,还可以通过此进程的 C/C++ 代码中的某个点,即时地将 C++ 调试器附加到正在运行的进程。这允许从代码中的特定 C/C++ 代码位置开始调试。下面的 C 代码从已运行的进程内部附加调试器。该代码使用 fork()
C 系统函数,该函数会生成一个额外的子进程,用于运行 C++ 调试器。
int gdb_process_pid = 0;
void exec_gdb()
{
// Create child process for running GDB debugger
int pid = fork();
if (pid < 0) /* error */
{
abort();
}
else if (pid) /* parent */
{
// Application process
gdb_process_pid = pid; // save debugger pid
sleep(10); /* Give GDB time to attach */
// Continue the application execution controlled by GDB
}
else /* child */
{
// GDB process. We run DDD GUI wrapper around GDB debugger
stringstream args;
// Pass parent process id to the debugger
args << "--pid=" << getppid();
// Invoke DDD debugger
execl("ddd", "ddd", "--debugger", "gdb", args.str().c_str(), (char *) 0);
// Get here only in case of DDD invocation failure
cerr << "\nFailed to exec GDB (DDD)\n" << endl;
}
}
上述代码首先创建一个子进程,然后在此子进程中执行 DDD
图形调试器。DDD
调试器将应用程序(父进程)的 pid
作为其调用标志之一接收,并将其自身附加到应用程序进程。从此时开始,调试器将控制应用程序的执行。现在您可以在调试器中调试 C++ 代码了。
从 Java 触发 C++ 调试器
让我们回到 Java。我们将从 Java 代码调用某个 C/C++ 函数,并希望在 C++ 调试器中调试此函数的执行。为了实现这一点,我们可以通过 JNI
将 C++ 原生函数 trigger_gdb()
暴露给 Java。trigger_gdb()
函数将在执行流程到达 C++ 代码后立即发出运行 C++ 调试器的请求。trigger_gdb()
函数仅标记运行 C++ 调试器的请求。调试器本身将从 C++ 函数 JNI
包装器中执行,如下文所示。Java 代码可以在调用需要调试的 C++ 函数之前立即调用 trigger_gdb()
。为了更方便,您可以从 GUI 元素(如按钮或菜单项)触发 C++ 调试器。
// C++ code that triggers C++ debugger
bool do_trigger_gdb = false;
void trigger_gdb()
{
do_trigger_gdb = true;
}
// Java code that triggers C++ debugger
public void some_function
{
// Trigger debugger invocation as soon as the application control
// reaches cpp_api_function() C++ code
trigger_gdb();
// Call C++ JNI wrapper
cpp_api_function();
}
剩下要做的就是在每个从 Java 调用的 C++ API JNI
包装器中添加条件调试器调用代码。可以使用 perl
或其他脚本语言轻松地自动化此任务。
// JNI wrapper for void MyClass::MyClassMethod(const char*) C++ method
JNIEXPORT void JNICALL Java_JNI_MyClass_MyClassMethod
(JNIEnv *env, jobject obj, jstring javaString)
{
//Get the native string from javaString
const char *nativeString = env->GetStringUTFChars(javaString, 0);
// Attach C++ debugger to the application if requested
if (do_trigger_gdb)
{
exec_gdb();
do_trigger_gdb = false;
}
// Invoke the native C/C++ function here
MyClass *myobj = *(MyClass **) &obj;
myobj->MyClassMethod(nativeString);
//DON'T FORGET THIS LINE!!!
env->ReleaseStringUTFChars(javaString, nativeString);
}
从 Java 调用的每个 C++ API JNI
包装器都会检查是否要运行由调试器控制的 C++ API 代码。如果请求了调试器,它将使用上面讨论的 exec_gdb()
函数将调试器附加到正在运行的进程。
如有不必要,勿增实体
最后说明。为了避免每次进入触发调试器调用的 Java 代码时都产生 C++ 调试器进程的激增,我们可以检查是否已激活调试器进程(使用其进程 id)。如果它仍在运行,我们则不执行另一个。以下代码执行此任务
bool is_gdb_running()
{
return kill(gdb_process_pid, 0) == 0;
}
C 系统调用 kill
在使用信号 0
(第二个参数)调用时,会检查进程是否存在。
摘要
在本文中,我展示了如何以方便高效的方式同时在 Java 和 C++ 调试器中调试 Java/C++ 混合代码。所展示的方法可以轻松地扩展到其他能够运行 C++ 代码的非 Java 语言。
历史
- 2009 年 2 月 9 日:初始发布