JNI实现Java调用C/C++代码及对C/C++动态库的单步调试

JNI标准作为Java平台的一部分,提供了与编译型语言进行交互的手段,尤其是对C/C++的交互。如果你有一段现成的C/C++代码想在java中调用,就可以通过JNI来完成。

假如有一段C代码,这段代码如下:

int say_hello()
{
    printf("Hello world!\n");
    return 0;
}

如果想在java中实现这段代码的调用,打印出“Hello world!“,该如何做呢?如何实现对C/C++代码实现单步调试呢?

一、新建一个java工程

我使用的是Idea,建一个这样的工程非常简单。

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第1张图片

hello.java中的代码:

package com.company;

public class hello {
    static {
        System.loadLibrary("hello");
    }
    public native int hello();
    public static void main(String[] args) {
        hello h = new hello();
        h.hello();
    }

二、生成.h

在较老版本的java中,我们可以通过javah命令来生成与java文件对应的C/C++头文件:

javah -classpath . src.com.company.hello

注意关键的一点,一定要在工程的根目录下运行这个命令,否则报错。

但是在较新版本的Java中,你可以通过javac -h来代替这个命令。

javac -h . hello.java

你不必再关注路径的问题,直接到java文件所在的路径下,运行这个命令,就会再-h后面的路径的位置下生成一个头文件。

三、编写C代码

将生成的.h文件放到C/C++工程下面,编写对应的.cpp/.c文件。我用的是Clion,目录如下:

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第2张图片

你也可以再目录中加个main文件,写一个main函数,然后在CMakeList.txt中加上相应的配置,这样就可以通过run来运行这个程序了,但是我们要做的是生成一个lib,可以不需要。

创建cpp文件,取一个跟你的.h文件名对应的文件名,然后在里面实现.h中自动定义好的函数。

这里我们的文件是com_company_hello.cpp

#include "com_company_hello.h"

extern int say_hello();
JNIEXPORT jint JNICALL Java_com_company_hello_hello(JNIEnv *, jobject)
{
    say_hello();
    return 0;
}

在实现的函数中就可以调用你需要的C函数了,这里就是int say_hello().

Clion会为你写好基本的Cmake文件,你只要稍作调整,甚至也可以不用调整:

cmake_minimum_required(VERSION 3.0)
project(hello)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BUILD_TYPE "Release")
#set(CMAKE_BUILD_TYPE "Debug")

set(CMAKE_CXX_STANDARD_REQUIRED on)
include_directories(include)
add_library(hello SHARED
        com_company_hello.cpp
        )

点击build按钮,你想要的dll就出来了。

把它放到你的java库文件对应的位置就可以运行上面已经写好的hello.java文件了。

四、一个小技巧

在上面实现的java文件中,调用动态库的语句是这样的:

System.loadLibrary("hello");

你也可以改成绝对路径,以减少调试过程中的dll的移动。

System.load("D:\\develop\\hello\\cmake-build-debug\\libhello.dll");

这样每次build C\C++程序之后,就可以直接运行java程序,而不用再多一道移动dll的手续。

五、遇到的一个问题

我在windows下进行调试的时候,用的是Clion和Idea两个工具分别编译和运行C/C++和Java代码。在这个过程中遇到一个问题:如果C/C++中引用了C++的头文件,像等,打包dll后被java引用的时候就会报错Can't find dependent libraries,而只引用C头文件时就没问题。显然Idea中在加载dll的时候发现找不到某个只有C++才依赖的动态库。那么如何确认少什么库呢?可以通过depends工具来检测,可在官网下载对应的版本:

Dependency Walker (depends.exe) Home Page

当把dll加载到工具中以后,可能会出现一堆黄色的,也就是依赖的库,但是可能缺少的没有这么多。

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第3张图片

 因为我的环境下,当不引用C++头文件的时候,java可调用所生成的动态库没有问题,所以可以用来对比一下二者到底有什么区别。将两种情况下依赖的库都拷贝出来,通过文本对比工具可以看出,差别就是在引用C++的头文件以后,会多出两个dll的依赖:

LIBGCC_S_SEH-1.DLL

LIBSTDC++-6.DLL

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第4张图片

 这就说明,在Idea工具中运行的Java找不到这两个动态库。

搜索C盘会发现,在Clion中和Cygwin64下,甚至git下面都有这两个库文件,但是它们所在的路径都没有被加入到环境变量中。因此你可以将它们所在的路径加到环境变量,或者将它们拷贝出来,放到系统的环境变量目录下,这样Idea就会找到这两个文件。然后重启Idea,重新运行程序调用dll,错误就没了。

当然也可以通过静态加载来解决这个问题:

target_link_libraries(hello -static-libgcc; -static-libstdc++)

六、动态库代码的单步调试

首先需要知道调用动态库的进程号。

Debug java程序,将断点设置在调用动态库中的程序之前,被断住以后,查看运行程序的进程号,查看进程号的方式比较多,但是有时候不好区分哪个对应的是你的程序。可以通过代码在程序运行时获取。在程序适当的位置添加下面的方法,并在主程序中调用它,就可以在调试中获取本进程的ID:

public static final int getProcessID() {
    RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
    System.out.println(runtimeMXBean.getName());
    return Integer.valueOf(runtimeMXBean.getName().split("@")[0])
            .intValue();
}

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第5张图片

 可以看到我们的进程号是55316,好,暂时让它停在这里吧,回到Clion。

在Clion中选择Run->Attach to Process

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第6张图片 在弹出框找到相应的进程号,也就是55316。

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第7张图片

这里要注意一点,有时候由于安装不全或者版本不兼容的原因,调试器可能会出现断不住的问题,这时候就需要GDB或LLDB都试一下,哪个可以就用哪一个,我的环境就是LLDB有问题,但是GDB是可以的。

JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_第8张图片

Attach之后,设置好断点。

再回到Idea,让java进程下的代码运行调用动态库函数函数的地方,我们这里就是h.hello()。

再回到Clion观察,如果没有问题,程序就会自动运行至断点处,接下来就跟普通程序的调试一样了。

有一点要注意,你编译的动态库一定得是Debug版本。除了build的选项中可以选择构建Debug版本外,在Cmake中也是可以设置的,这一点一定要注意,当你的程序无论如何都断不住的时候,应该看一下你的Cmake文件,是不是被设置成了Release。

#set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_BUILD_TYPE "Debug")

你可能感兴趣的:(c++,开发语言,c语言)