JNI标准作为Java平台的一部分,提供了与编译型语言进行交互的手段,尤其是对C/C++的交互。如果你有一段现成的C/C++代码想在java中调用,就可以通过JNI来完成。
假如有一段C代码,这段代码如下:
int say_hello()
{
printf("Hello world!\n");
return 0;
}
如果想在java中实现这段代码的调用,打印出“Hello world!“,该如何做呢?如何实现对C/C++代码实现单步调试呢?
我使用的是Idea,建一个这样的工程非常简单。
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();
}
在较老版本的java中,我们可以通过javah命令来生成与java文件对应的C/C++头文件:
javah -classpath . src.com.company.hello
注意关键的一点,一定要在工程的根目录下运行这个命令,否则报错。
但是在较新版本的Java中,你可以通过javac -h来代替这个命令。
javac -h . hello.java
你不必再关注路径的问题,直接到java文件所在的路径下,运行这个命令,就会再-h后面的路径的位置下生成一个头文件。
将生成的.h文件放到C/C++工程下面,编写对应的.cpp/.c文件。我用的是Clion,目录如下:
你也可以再目录中加个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++的头文件,像
Dependency Walker (depends.exe) Home Page
当把dll加载到工具中以后,可能会出现一堆黄色的,也就是依赖的库,但是可能缺少的没有这么多。
因为我的环境下,当不引用C++头文件的时候,java可调用所生成的动态库没有问题,所以可以用来对比一下二者到底有什么区别。将两种情况下依赖的库都拷贝出来,通过文本对比工具可以看出,差别就是在引用C++的头文件以后,会多出两个dll的依赖:
LIBGCC_S_SEH-1.DLL
LIBSTDC++-6.DLL
这就说明,在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();
}
可以看到我们的进程号是55316,好,暂时让它停在这里吧,回到Clion。
在Clion中选择Run->Attach to Process
这里要注意一点,有时候由于安装不全或者版本不兼容的原因,调试器可能会出现断不住的问题,这时候就需要GDB或LLDB都试一下,哪个可以就用哪一个,我的环境就是LLDB有问题,但是GDB是可以的。
Attach之后,设置好断点。
再回到Idea,让java进程下的代码运行调用动态库函数函数的地方,我们这里就是h.hello()。
再回到Clion观察,如果没有问题,程序就会自动运行至断点处,接下来就跟普通程序的调试一样了。
有一点要注意,你编译的动态库一定得是Debug版本。除了build的选项中可以选择构建Debug版本外,在Cmake中也是可以设置的,这一点一定要注意,当你的程序无论如何都断不住的时候,应该看一下你的Cmake文件,是不是被设置成了Release。
#set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_BUILD_TYPE "Debug")