JNI学习笔记

发布时间 2023-10-09 21:29:51作者: 星流残阳

1. 使用Java程序调用C++函数步骤

  1. 创建包含本地方法的Java类:

    package org.example;
    public class HelloWorld {
        static {
            System.loadLibrary("HelloWorld");
        }
    
        public native void print();
    
        public static void main(String[] args) {
            new HelloWorld().print();
        }
    }
    
  2. 使用javac编译生成HelloWorld.class文件:

    javac HelloWorld
    
  3. 使用2生成的HelloWorld.class,通过javah在src/main/java目录下生成C++头文件,详见以下2,3节:

    javah org.example.HelloWorld
    

    此时生成了org_example_HelloWorld.h头文件。

  4. 编写本地方法实现,创建org_example_HelloWorld.cpp,在其中实现HelloWorld.print方法:

    #include <jni.h>
    #include <stdio.h>
    #include <org_example_HelloWorld.h>
    
    JNIEXPORT void JNICALL Java_org_example_HelloWorld_print(JNIEnv *env, jobject obj){
        printf("Hello World!\n");
        return ;
    }
    
  5. 编译C++源码并生成一个本地库,用于Java代码中的System.loadLibrary("HelloWorld");,这里我使用g++/gcc:

    g++ -fno-pie -fPIC -no-pie -shared -I ${JAVA_HOME}/include -I ${JAVA_HOME}/include/linux -I . -o libHelloWorld.so org_example_HelloWorld.cpp
    

    这里 -I后面的是指头文件所在目录,-o指明导出的本地库位置,-shared说明导出动态库。

  6. 在java目录下运行,命令如下:

    java -Djava.library.path=./org/example/ org.example.HelloWorld
    

2. 踩坑

2.1 javah无法生成头文件

需要到src/main/java目录下使用javah,命令是:javah java类的完整名(包名+类名)

例如:javah org.example.HelloWorld

2.2 g++ 报错

报错:/usr/bin/ld: /tmp/ccEtEkwZ.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC

原因:需要在编译命令里加入 -fPIC。

2.3 运行报错

报错:找不到HelloWorld库

这里比较坑,原因有两个,一个是我编译本地库时名字是HelloWorld.so,而正确的名字应该是libHelloWorld.so;第二个问题是我运行的是org.example.HelloWorld,所以library需要设置为libHelloWorld.so所在的目录,即./org/example/(当前目录是src/main/java)

3. javah生成的.h文件解析

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

#ifndef _Included_org_example_HelloWorld
#define _Included_org_example_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_example_HelloWorld
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_example_HelloWorld_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

#include:引用头文件

#ifndef:宏命令,后接标识符 意为:如果不包含标识符

#define:宏命令,后接标识符 意为:定义标识符

#ifdef:宏命令,后接标识符 意为:如果包含标识符;此文件中,后接的__cplusplus用于识别编译器,即将当前代码编译的时候,是否将代码作为C++进行编译,如果是,则定义了__cplusplus

extern "C"{:实现C++和C以及其他语言的混合编程。

3.1 extern "C"{ 说明

举例如下:

  • C++引用C函数:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif

/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
    return x + y;
}

// c++实现文件,调用add:cppFile.cpp
extern "C"
{
    #include "cExample.h"
}
int main(int argc, char* argv[])
{
    add(2,3);
    return 0;
}

cExample.h 是C语言的头文件,在C++中使用extern "C"包含,这样C++在链接C库时,采用C的方式进行链接(即寻找_add而不是_add_int_int)。

  • C引用C++函数:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
    return x + y;
}

/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );

int main( int argc, char* argv[] )
{
    add( 2, 3 );   
    return 0;
}

此时,C不能直接使用#include "cppExample.h",因为C不支持extern "C",这里C++文件使用extern "C"目的是让C++编译时生成C形式的符号,将其添加到C++实现库中,以便C能找到。

总结:

  • extern "C" 只是 C++ 的关键字,不是 C 的;

    如果在 C 程序中引入了 extern "C" 会导致编译错误。

  • 被 extern "C" 修饰的目标一般是对一个全局C或者 C++ 函数的声明

    从源码上看 extern "C" 一般对头文件中函数声明进行修饰。 Ccpp 中头文件函数声明的形式都是一样的(因为两者语法基本一样),对应声明的实现却可能由于语言特性而不同了( C 库和 C++ 库里面当然会不同)。

  • extern "C" 这个关键字声明的真实目的,就是实现 C++ 与C及其它语言的混合编程

    一旦被 extern "C" 修饰之后,它便以 C 的方式工作(编译阶段:以C的方式编译,链接阶段:寻找C方式编译生成的符号), C 中引用 C++ 库的函数,或 C++ 中引用 C 库的函数,都可以通过这个方式(即在C++文件中用extern "C" 声明,实现兼容。

3.2 本地方法

生成的本地方法C实现接受两个参数,尽管Java中没有接受任何参数。第一个参数事JNIEnv的接口指针,第二个参数是HelloWorld对象本身,类似C++中的this指针。

4.数据类型映射

4.1 String类型使用和转换

转换

Java中的String在本地方法中变为jstring,而jstring和C++中的字符串char*并不等价,不能替换使用,此时需要使用JNI方法GetStringUTFChars:

const char* model_Dir = env->GetStringUTFChars(modelDir, NULL);

不要忘记检查 GetStringUTFChars 的返回值,这是因为 Java 虚拟机的实现决定内部需要申请内存来容纳 UTF-8 字符串,内存的申请是有可能会失败的。如果内存申请失败,那么 GetStringUTFChars 将会返回 NULL 并且会抛出 OutOfMemoryError 异常。

释放

在使用完之后需要释放本地字符串:

env->ReleaseStringUTFChars(modelDir, model_Dir);
创建

通过调用JNI函数NewStringUTF,可以在本地代码中创建一个新的Java.lang.String 实例。同样,需要检查返回值是否为NULL。

其他

以Unicode格式获取和释放字符串:GetStringChars和ReleaseStringChars

统计字符串长度:GetStringUTFLength/GetStringLength

以 Unicode 格式将字符串的内容复制到预分配的 C 缓冲器到或从预分配的 C 缓冲区中复制:GetStringRegion\SetStringRegion.