java native and netty

概述

这篇主要讲讲 java native

java native 可以让 java 虚拟机 直接和底层交互, 一般用于比如系统相关的资源(比如 netty-native-epoll),或者一些别的目的(将加解密lib封装到底层,增加破解难度)



JNI 教程

JNI with c

1. 一个用了 java native 的 HelloJNI.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloJNI {  // Save as HelloJNI.java
static {
System.loadLibrary("hello"); // Load native library hello.dll (Windows) or libhello.so (Unixes)
// at runtime
// This library contains a native method called sayHello()
}

// Declare an instance native method sayHello() which receives no parameter and returns void
private native void sayHello();

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

2. 编译java 文件,生成native 头文件

java8 之后可以直接

1
javac -h . HelloJNI.java

java8 之前需要按照以前步骤

1
2
javac HelloJNI.java
javah HelloJNI

HelloJNI.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* 注: 该方法对应了 java 代码里面 native 的接口,c代码需要实现他
*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/

JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject);


#ifdef __cplusplus
}
#endif
#endif

3. 实现c 程序 HelloJNI.c

1
2
3
4
5
6
7
8
9
10
// Save as "HelloJNI.c"
#include <jni.h> // JNI header provided by JDK
#include <stdio.h> // C Standard IO Header
#include "HelloJNI.h" // Generated

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}

4. 编译 c程序 HelloJNI.c

mac/linux
1
2
3
4
5
6
7
8
9
10
11
12
## 设置 JAVA_HOME
export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

## 编译 on mac
gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c

## 编译 on linux
gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c

## 运行

java -cp . -Djava.library.path=. HelloJNI

java.library.path 是什么?

System.loadLibrary() 默认会从 ‘java.library.path’ 这个java参数指定的路径下寻找动态链接库, 这个值一般为当前执行java 进程的环境的 PATH 的复制。

在我的mac下默认值是(注: 最后一个. 表示应用根目录):

1
/Users/caorong/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.

java 的native 方法 如何对应到对应的动态链接库?

通过包名

比如, 我的代码是在 默认包下

1
private native void sayHello();

那么生成的 native 接口为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Header for class org_cr_HelloJNI */

#ifndef _Included_org_cr_HelloJNI
#define _Included_org_cr_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_cr_HelloJNI
* Method: sayHello
* Signature: ()V
*/

JNIEXPORT void JNICALL Java_org_cr_HelloJNI_sayHello
(JNIEnv *, jobject);

如果在 org.cr 这个包下的话

1
2
3
package org.cr;

private native void sayHello();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Header for class org_cr_HelloJNI */

#ifndef _Included_org_cr_HelloJNI
#define _Included_org_cr_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_cr_HelloJNI
* Method: sayHello
* Signature: ()V
*/

JNIEXPORT void JNICALL Java_org_cr_HelloJNI_sayHello
(JNIEnv *, jobject);

注意,生成的 jni 的方法名上带有java端的包名。

所以,其实native的动态链接库,并没有外部索引,都是通过方法名来映射的。

java 端 native 方法对应的 包名和 native 的方法名一一对应,不能乱改。

java 和 c 的类型映射

java的 jni.h 头文件路径位于 $JAVA_HOME/include/jni.h

java 的native 函数需要传递参数,以下是 他们的类型映射关系

基本类型: java 类型和c 一一对应, 栈上分配内存,无需手动释放

java类型 native类型 描述
void void
byte jbyte 8位 有符号
int jint 32位 有符号
float jfloat 32位 有符号
double jdouble 64位 有符号
char jchar 16位 无符号
long jlong 64位 有符号
short jshort 16位 有符号
boolean jboolean 8位 无符号

基本类型在c代码中可以直接和c类型一起使用.

1
private native double average(int n1, int n2);
1
2
3
4
5
6
7
8
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {

jdouble result;
printf("In C, the numbers are %d and %d\n", n1, n2);
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
}

引用类型: 需要多做一些转换,以及需要手动释放java容器对象

java类型 native类型 描述
java.lang.String jobject 所有java对象
java.lang.object jstring string
java.lang.Class jclass java class 对象
java.lang.Throwable jthrowable java throwable 对象

string 相关函数

主要是一些转换,手动释放函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jstring (JNICALL *NewString)
(JNIEnv *env, const jchar *unicode, jsize len);
jsize (JNICALL *GetStringLength)
(JNIEnv *env, jstring str);
const jchar *(JNICALL *GetStringChars)
(JNIEnv *env, jstring str, jboolean *isCopy);
void (JNICALL *ReleaseStringChars)
(JNIEnv *env, jstring str, const jchar *chars);

jstring (JNICALL *NewStringUTF)
(JNIEnv *env, const char *utf);
jsize (JNICALL *GetStringUTFLength)
(JNIEnv *env, jstring str);
const char* (JNICALL *GetStringUTFChars

代码例子

1
private native String sayHelloString(String str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHelloString(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
if (NULL == inCStr) return NULL;

// Step 2: Perform its intended operations
printf("In C, the received string is: %s\n", inCStr);
(*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // release resources

// Prompt user for a C-string
char outCStr[128];
printf("Enter a String: ");
scanf("%s", outCStr); // not more than 127 characters

// Step 3: Convert the C-string (char*) into JNI String (jstring) and return
return (*env)->NewStringUTF(env, outCStr);
}

object 相关函数

主要是通过反射来读写 object 对象的field

1
2
3
4
5
6
7
8
jclass GetObjectClass(JNIEnv *env, jobject obj);
// Returns the class of an object.

jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the field ID for an instance variable of a class.

NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
1
private native void modifyInstanceVariable();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
(JNIEnv *env, jobject thisObj) {

// Get a reference to this object's class
jclass thisClass = (*env)->GetObjectClass(env, thisObj);

// int
// Get the Field ID of the instance variables "number"
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
if (NULL == fidNumber) return;

// Get the int given the Field ID
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
printf("In C, the int is %d\n", number);

// Change the variable
number = 99;
(*env)->SetIntField(env, thisObj, fidNumber, number);

// Get the Field ID of the instance variables "message"
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
if (NULL == fidMessage) return;

// String
// Get the object given the Field ID
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);

// Create a C-string with the JNI String
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
if (NULL == cStr) return;

printf("In C, the string is %s\n", cStr);
(*env)->ReleaseStringUTFChars(env, message, cStr);

// Create a new C-string and assign to the JNI string
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;

// modify the instance variables
(*env)->SetObjectField(env, thisObj, fidMessage, message);
}

所有基本类型的 数组类型: 数组容器 也需要手动释放

包括以下类型

jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray and jbooleanArray

相关函数

1
2
3
4
5
6
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
v

例子

1
private native double[] sumAndAverage(int[] numbers);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {

// Step 1: Convert the incoming JNI jintarray to C's jint[]
jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
if (NULL == inCArray) return NULL;
jsize length = (*env)->GetArrayLength(env, inJNIArray);

// Step 2: Perform its intended operations
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += inCArray[i];
}
jdouble average = (jdouble)sum / length;
(*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources

jdouble outCArray[] = {sum, average};

// Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // allocate
if (NULL == outJNIArray) return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // copy
return outJNIArray;
}

实际使用案例

netty-native 如何设计并使用动态链接库

netty-native 如果进行使用一个jar支持多个环境?

对于不同环境对不同链接库不同的命名

netty_transport_native_epoll_{os.arch}

如果对 nettynative 的动态链接库做了一些自定义,如何不和官方提供的作区分?

动态链接库名字上带上包名

官方的 netty_transport_native_epoll_{os.arch}

自己shaded 的package为 com.xx.shaded.io.netty.xx

那么 动态链接库名字则需要补上 自定义的前缀 libcom_xx_shaded_netty_transport_native_epoll_{os.arch}

如何读取jar 里面的 动态链接库?

会从jar内读取当前系统的动态链接库,copy到系统某个临时目录下

jar:META-INF/native/libcom_xx_shaded_netty_transport_native_epoll_{os.arch}

copy to

/tmp/libcom_xx_shaded_netty_transport_native_epoll_{os.arch}_{random number}.so

注: 在 netty 中该copy的方式是作为最后的保底方案。

因为这个动态链接库是机器相关的,不同系统,可能会有不兼容,所以,最好的方案还是在每台机器上自己编译动态链接库后放到 java.library.path

附 netty中 寻找动态库的顺序

  1. java.library.path 中找 libcom_xx_shaded_netty_transport_nativeepoll{os.arch}, 用 AccessController 在满足权限的条件下,load
  2. 同上,但是不会通过 AccessController 以授权方式加载
  3. 寻找jar包内部 jar:META-INF/native/libcom_xx_shaded_netty_transport_native_epoll_{os.arch} 的动态库,copy到 临时文件后, load 该临时生成的动态链接库

netty 如何保证java代码包名被 shaded (修改) 后 还能索引到原来的方法?

netty native 的c代码,利用了动态实现JNI 的方法。

JNI 在加载时, 会调用 JNI_OnLoad 卸载时会调用 JNI_UnLoad

所以,他利用 JNI_OnLoad 时,获取了当前动态连接路的 path, 由于动态链接库有着上面的规则,所以他会把上面自己添加的 shaded 的包名(packagePrefix),存在内存里面,然后, 依次动态注册函数名字(前缀会添加 packagePrefix).

reference

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

https://www.developer.com/java/data/jni-data-type-mapping-to-cc.html

avatar

lelouchcr's blog