Skip to content

JNI 调用约定

源:JNI Types and Data Structures - Oracle

JNI 调用约定定义了 Java 与 Native 代码之间的接口规范。理解调用约定对编写正确的 JNI 代码至关重要。

函数命名规范

标准命名格式

Native 方法的函数名遵循固定模式:

Java_包名_类名_方法名

包名中的点 (.) 和类名中的下划线 (_) 需要特殊处理:

  • . 替换为 _
  • _ 替换为 _1
  • $ 替换为 _00024
java
package com.example.app;

public class NativeLib {
    static {
        System.loadLibrary("native-lib");
    }
    
    // 简单方法
    public native void simpleMethod();
    
    // 嵌套类
    public static class Inner {
        public native int calculate(int x);
    }
}
kotlin
@OptIn(ExperimentalForeignApi::class)
// 包名: com.example.app → com_example_app
// 类名: NativeLib
@CName("Java_com_example_app_NativeLib_simpleMethod")
fun simpleMethod(env: CPointer<JNIEnvVar>, thiz: jobject) {
    println("Simple method called")
}

// 嵌套类: NativeLib$Inner → NativeLib_00024Inner
@CName("Java_com_example_app_NativeLib_00024Inner_calculate")
fun innerCalculate(
    env: CPointer<JNIEnvVar>,
    thiz: jobject,
    x: jint
): jint {
    return x * 2
}

特殊字符编码

kotlin
// Java: com.my_company.MyClass
// Native: Java_com_my_1company_MyClass_method

// Java: com.example.Test$Inner
// Native: Java_com_example_Test_00024Inner_method

方法重载处理

签名编码

当 Java 方法重载时,需要在函数名后追加编码后的方法签名

Java_包名_类名_方法名__签名

注意两个下划线 (__) 分隔方法名和签名。

java
public class Calculator {
    // 重载方法
    public native int add(int a, int b);
    public native double add(double a, double b);
    public native String add(String a, String b);
}
kotlin
@OptIn(ExperimentalForeignApi::class)
// add(int, int) → (II)I
@CName("Java_com_example_Calculator_add__II")
fun addInt(
    env: CPointer<JNIEnvVar>,
    thiz: jobject,
    a: jint,
    b: jint
): jint {
    return a + b
}

// add(double, double) → (DD)D
@CName("Java_com_example_Calculator_add__DD")
fun addDouble(
    env: CPointer<JNIEnvVar>,
    thiz: jobject,
    a: jdouble,
    b: jdouble
): jdouble {
    return a + b
}

// add(String, String) → (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
// 特殊字符编码: ; → _2, / → _
@CName("Java_com_example_Calculator_add__Ljava_lang_String_2Ljava_lang_String_2")
fun addString(
    env: CPointer<JNIEnvVar>,
    thiz: jobject,
    a: jstring?,
    b: jstring?
): jstring? {
    val jniEnv = env.pointed.pointed!!
    val aStr = a?.toKString(env) ?: ""
    val bStr = b?.toKString(env) ?: ""
    return (aStr + bStr).toJString(env)
}

方法签名规范

签名格式

(参数类型列表)返回类型

基本类型签名

Java 类型签名说明
voidV仅用于返回类型
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD

对象类型签名

对象类型格式:L完全限定类名;

kotlin
// java.lang.String
Ljava/lang/String;

// java.util.List
Ljava/util/List;

// com.example.User
Lcom/example/User;

###数组签名

数组类型前缀 [

kotlin
// int[]
[I

// String[]
[Ljava/lang/String;

// int[][]
[[I

// Object[]
[Ljava/lang/Object;

完整示例

java
// Java 方法声明
void method1();                              // ()V
int method2(int a);                          // (I)I
String method3(int a, String b);             // (ILjava/lang/String;)Ljava/lang/String;
boolean method4(int[] arr);                  // ([I)Z
void method5(String[] strs, Object obj);     // ([Ljava/lang/String;Ljava/lang/Object;)V
List<String> method6(Map<String, Integer> map); // (Ljava/util/Map;)Ljava/util/List;

WARNING

泛型信息在 JNI 签名中会被擦除。List<String>List<Integer> 都是 Ljava/util/List;

签名编码规则

特殊字符转义

签名中的特殊字符需要转义:

字符转义
/_
;_2
[_3

示例:

kotlin
// 原始签名: (Ljava/lang/String;)V
// 编码后: __Ljava_lang_String_2

// Native 函数名
Java_com_example_Test_method__Ljava_lang_String_2

完整命名示例

java
package com.example;

public class Demo {
    // 方法1:无参数
    public native void foo();
    
    // 方法2:单个 int 参数
    public native int bar(int x);
    
    // 方法3:重载 - String 参数
    public native int bar(String s);
    
    // 方法4:数组参数
    public native String process(int[] data, String[] names);
}
kotlin
@OptIn(ExperimentalForeignApi::class)
import kotlinx.cinterop.*

// 方法1
@CName("Java_com_example_Demo_foo")
fun foo(env: CPointer<JNIEnvVar>, thiz: jobject) {
    // 实现
}

// 方法2
@CName("Java_com_example_Demo_bar__I")
fun barInt(env: CPointer<JNIEnvVar>, thiz: jobject, x: jint): jint {
    return x * 2
}

// 方法3: 签名 (Ljava/lang/String;)I
// 编码: __Ljava_lang_String_2
@CName("Java_com_example_Demo_bar__Ljava_lang_String_2")
fun barString(env: CPointer<JNIEnvVar>, thiz: jobject, s: jstring?): jint {
    return s?.toKString(env)?.length ?: 0
}

// 方法4: 签名 ([I[Ljava/lang/String;)Ljava/lang/String;
// 编码: __3I_3Ljava_lang_String_2
@CName("Java_com_example_Demo_process___3I_3Ljava_lang_String_2")
fun process(
    env: CPointer<JNIEnvVar>,
    thiz: jobject,
    data: jintArray?,
    names: jobjectArray?
): jstring? {
    // 实现
    return "Processed".toJString(env)
}

静态方法与实例方法

参数差异

kotlin
@OptIn(ExperimentalForeignApi::class)
// 实例方法:第二个参数是 jobject (this)
@CName("Java_com_example_Test_instanceMethod")
fun instanceMethod(
    env: CPointer<JNIEnvVar>,
    thiz: jobject  // 实例引用
) { }

// 静态方法:第二个参数是 jclass (类引用)
@CName("Java_com_example_Test_staticMethod")
fun staticMethod(
    env: CPointer<JNIEnvVar>,
    clazz: jclass  // 类引用
) { }

动态注册

RegisterNatives

除了使用命名约定,还可以动态注册:

kotlin
@OptIn(ExperimentalForeignApi::class)
import kotlinx.cinterop.*

// 实际的 native 函数(任意命名)
val nativeAdd = staticCFunction<CPointer<JNIEnvVar>, jobject, jint, jint, jint> { 
    env, thiz, a, b ->
    a + b
}

// JNI_OnLoad 中注册
@CName("JNI_OnLoad")
fun onLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer?): jint {
    memScoped {
        val env = allocPointerTo<JNIEnvVar>()
        vm.pointed.pointed!!.GetEnv!!(vm, env.ptr.reinterpret(), JNI_VERSION_1_6)
        
        val jniEnv = env.value!!.pointed.pointed!!
        
        // 查找类
        val clazz = jniEnv.FindClass!!(env.value, "com/example/Calculator")
        
        // 定义方法映射
        val methods = allocArray<JNINativeMethod>(1)
        methods[0].apply {
            name = "add".cstr.ptr
            signature = "(II)I".cstr.ptr
            fnPtr = nativeAdd.reinterpret()
        }
        
        // 注册
        jniEnv.RegisterNatives!!(env.value, clazz, methods, 1)
    }
    
    return JNI_VERSION_1_6
}

动态注册的优势推荐

  • 解耦命名:Native 函数名不受 Java 包名/类名限制
  • 易维护:重构 Java 代码时无需修改 Native 函数名
  • 灵活性:可在运行时决定绑定关系

工具辅助

javap 查看签名

bash
# 编译 Java 文件
javac Calculator.java

# 查看方法签名
javap -s -p Calculator

# 输出
public int add(int,int);
  descriptor: (II)I

public java.lang.String process(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)Ljava/lang/String;

javah 生成头文件

bash
# 生成 JNI 头文件(包含正确的函数名)
javah -jni com.example.Calculator

# 生成 com_example_Calculator.h

掌握 JNI 调用约定能避免 UnsatisfiedLinkError 等链接错误。推荐使用动态注册提升代码可维护性。