vimer linux kernel 爱好者

java之jni过程

2020-06-21

这是一篇参考的笔记 原文然后通过自己的实验记录下来.

start with java and c

这个库函数应该包含在java的库的路径中(这里有一个专门的java系统变量是处理这个的 java.library.path),对于我们这种自定义的library,在编译java程序时可以传递过去。 -Djava.library.path=/path/to/lib,如果java找不到的话,则会throw一个 UnsatisfiedLinkError的错误。

public class myJni { // save as JNI.java
	static {
		System.loadLibrary("hello"); // load library libhello.so on UBIX
									// at runtime, the library contains
	}								// a native method called sayHello()
	private native void sayHello();

	public static void main(String[] args){
		new Jni().sayHello(); // Create an instance and invoke the native method
	}
}

Compile the java program myJni.java

javac -h . myJni.java:我们来看一下javac -h.

-h <directory>               指定放置生成的本机标头文件的位置

ls:

vimer@host:~/src/test/java$ ls
myJni.class  myJni.h  myJni.java

hit: 我这是OpenJdk9.其他版本的请自己寻找一下相关的用法。

我们来看一下会生成的相关的头文件。

vimer@host:~/src/test/java$ cat myJni.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class myJni */

#ifndef _Included_myJni
#define _Included_myJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     myJni
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_myJni_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

其中最主要的就是下面这个:

JNIEXPORT void JNICALL Java_myJni_sayHello
  (JNIEnv *, jobject);

这个命名规则是Java_{package-and-classname}_{function-name}(JNI-argument) 这里就有一个大名鼎鼎的dot代替下划线的替换。 这两个参数是:

  1. JNIEnv\*

    : JNI环境,请注意这个JNI是广泛的特指,而不是 本例的JNI.java中的JNI。

  2. jobject

    : “this” object in java.

“extern c”是针对c++编译器而言的。它通知c++的编译器这些函数使用c的命名规则而不是c++的 命名规则。

implement c program myJni.c

我们首先对应的c程序,不然就算写出来了h文件也没有什么用途。

#include <stdlib.h>
#include <unistd.h>
#include <jni.h> // JNI header provided by JDK
#include "myJni.h" // Generated file from javac

JNIEXPORT void JNICALL Java_myJni_sayHello(JNIEnv *env, jobject thisObj){
	printf("Hello, Jni world\n");
	return;
}

使用命令去编译这个c程序生成动态加载库:

vimer@host:~/src/test/java$ gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so myJni.c
vimer@host:~/src/test/java$ ls
libhello.so  myJni.c  myJni.class  myJni.h  myJni.java
vimer@host:~/src/test/java$ nm libhello.so | grep say
000000000000061a T Java_myJni_sayHello
vimer@host:~/src/test/java$ java -Djava.library.path=. myJni
Hello, Jni world

可以使用tool nm查看一下动态库有没有定义这个符号,我们可以清楚地看到,为T(定义)。 然后使用java命令就可以将动态库的方法加载进去了。

c++

将上面的java代码保持不变,还是使用这个。唯一的不同是使用:

#include <iostream>
#include <jni.h>
#include "myJni.h"
using namespace std;

// Implementions of native method sayHello()

JNIEXPORT void JNICALL Java_myJni_sayHello(JNIEnv *env, jobject thisObj){
	cout << "Hello jni world from c++" << endl;
	return;
}

接下来不变:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so myJni.cpp
vimer@host:~/src/test/java$ java -Djava.library.path=. myJni
Hello jni world from c++

JNI with C/C++ mixture

这一个例子是揭示如何利用一个java代码同时使用c或者c++的程序。这次的java代码就有几处 不同。

public class myJniCpp { // save as JNI.java
	static {
		System.loadLibrary("hello"); // load library libhello.so on UBIX
									// at runtime, the library contains
	}								// a native method called sayHello()
	private native void sayHello();

	public static void main(String[] args){
		new myJniCpp().sayHello(); // Create an instance and invoke the native method
	}
}

接着产生myJniCpp.h文件。

vimer@host:~/src/test/java$ javac -h . myJniCpp.java

详细看一下这个文件的内容。

cat myJniCpp.h 
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class myJniCpp */

#ifndef _Included_myJniCpp
#define _Included_myJniCpp
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     myJniCpp
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_myJniCpp_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

当然,我们最关心的应该还是sayHello()方法的实现。

然后我们利用这个头文件分别实现对应的c和c++的头文件。什么?需要吗?按照我的理解是这样的 一个java服务程序,当然支持c或者c++的程序最好了,那既然上面的方法我们分别实现了对应的 native代码,说明肯定有冲突的地方。

myJniCppImpl.h myJniCppImpl.cpp myJniCpp.c

这三个文件各自有自己的用途,我们先看一下myJniCppImpl.h, 这是一个C++的头文件。

#ifndef _MY_JNI_CPP_IMPL_H
#define _MY_JNI_CPP_IMPL_H

#ifdef __cplusplus
		extern "C" {

#endif
			void sayHello();
#ifdef __cplusplus
		}
#endif
#endif

cat myJniCppImpl.cpp:

#include <iostream>
#include "myJniCppImpl.h"
using namespace std;

void sayHello(){
	cout << "hello wrold from c++!" << endl;
	return;
}

很明显这里就是在c++的程序中实现了c库的函数,其实这里很复杂的,涉及到了编译器 对c++ c函数的编译声明方式。 cat myJniCpp.c:

#include <jni.h>
#include "myJniCpp.h"
#include "myJniCppImpl.h"

JNIEXPORT void JNICALL Java_myJniCpp_sayHello(JNIEnv *env, jobject thisObj){
	sayHello();// invoke c++ function
	return;
}

这个c程序负责与java程序的交互。下面是编译参数:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so myJniCpp.c myJniCppImpl.cpp 

如果不注意的话则会出错,比如下面这个。

In file included from myJniCpp.c:3:0:
myJniCppImpl.h:5:10: error: language string ‘"c"’ not recognized
   extern "c" {
          ^~~
In file included from myJniCppImpl.cpp:2:0:
myJniCppImpl.h:5:10: error: language string ‘"c"’ not recognized
   extern "c" {
          ^~~

记住,调用c库的函数时一定注意这个错误,这个错误很牛逼,就是: 你把该大写的”C”写成了小写”c”.

Run java

java -Djava.library.path=. myJniCpp
hello wrold from c++!

我们总结一下这个过程,首先在java程序中使用native关键词说明本java方法使用底层库 的一个函数。然后我们使用c++实现了具体的方法,后面使用c程序invoke c++的实现。


Comments

Content