65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (4投票s)

2009 年 2 月 9 日

GPL3

4分钟阅读

viewsIcon

43314

如何在 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 日:初始发布
© . All rights reserved.