打通Java和C 之间的传送门,JNI从0 到1的保姆级教程

2021-07-15 10:03:41 浏览数 (1)

之前我们的游戏服务端战斗和客户端战斗是分开写的,经常会出现 一些莫名其妙的bug,原因是前后端实现的细节不一致,这种问题很难解决,隐蔽性很高,测试的时候也很难测试,只有到了线上才会发现问题,而且处理的周期比较长,为了解决这样的问题,我们的项目出现了前后端战斗统一实现的需求,因为我们的客户端是用unity xlua 的解决方案,这样客户端在写战斗的时候只要把逻辑和表现进行剥离,将战斗逻辑部分放到服务器进行验证,这样只要在服务端起一个验证服务器执行lua就好了。

因此封装了lua 的战斗接口,将lua 封装成可以java调用的动态链接库。这样的解决方案使用了JNI的技术。今天来聊下JNI的一些知识点。因为有一段时间没搞C 了,还是得从头开始。

JNI是java native interface的缩写,是用来从java调用C /C代码,也可以从C /C调用Java代码。

1、环境安装

1、下载MinGW压缩包

下载地址:https://sourceforge.net/projects/mingw-w64/files/Toolchains targetting Win64/Personal Builds/mingw-builds/

下载后解压文件 出现 mingw64文件夹。

2、下载clion

因为Visual studio 的安装包实在太大了,懒得下,所以选择了clion.

下载地址:https://www.jetbrains.com/zh-cn/clion/download/#section=windows

注:安装的时候 一步一步next 就可以了了,破解教程可以在网上搜索。

3、配置C 开发环境

这样基本的环境就算完成了,下面开始搞个例子吧。

2、走个例子

2.1 新建C程序

Clion ->file ->新建项目。

下图中填入项目名,并且选择 shared 动态链接库。

注:动态库根据系统的不同会生成同的链接库,win下生成.dll,linux 下生成.so

2.2 拷贝 jni.h 和 jni_md.h 到目录下

文件所在地址:

代码语言:javascript复制
C:Program FilesJavajdk1.8.0_291includejni.h
C:Program FilesJavajdk1.8.0_291includewin32jni_md.h

注:在上面两个目录直接找到两个文件,拷贝(因为你还可能开发其他的工程)到项目根目录就可以了

2.3 输入代码

头文件

代码语言:javascript复制
#ifndef TESTJNI_LIBRARY_H
#define TESTJNI_LIBRARY_H


#include "jni.h"
#include "jni_md.h"

JNIEXPORT void JNICALL Java_com_pdool_Main_sayHello(JNIEnv * jobject);

#endif //TESTJNI_LIBRARY_H

.c 文件

代码语言:javascript复制
#include "library.h"

#include <stdio.h>

void JNICALL Java_com_pdool_Main_sayHello(JNIEnv * jobject) {
    printf("this is C print");
}

2.4 执行构建

生成对应的libxxxx.dll,路径为 D:clionTestJnicmake-build-debuglibTestJni.dll,也就是上图红色的那个目录下

2.4 新建java 项目,可以新建一个Maven 或者普通的Java项目都可以,我这里是选择了普通的项目。

2.5 Java 代码

代码语言:javascript复制
package com.pdool;

public class Main {

    public static native void sayHello();

    static {
        System.loadLibrary("libTestJni");   // 加载实现了native函数的动态库,只需要写动态库的名字
    }

    public static void main(String[] args) {
        sayHello();
//        String arch = System.getProperty("sun.arch.data.model");
//        System.out.println("arch:" arch);

    }

}

2.6 配置 library path

library path 是告诉 虚拟机 去哪里查找链接库,在使用System.loadLibrary("libTestJni");的搜索路径

2.7 执行Java代码,看下调用效果

恭喜你,项目跑起来了。下面一起理解下技术细节吧。

3、技术分析

3.1 函数定义
代码语言:javascript复制
JNIEXPORT void JNICALL Java_com_pdool_Main_sayHello(JNIEnv * jobject);

extern "C“: JNI函数声明声明代码是用C 语言写的,所以需要添加extern "C"声明;如果源代码是C语言声明,则不需要添加这个声明

JNIEXPORT:这个关键字表明这个函数是一个可导出函数。类似Java的public函数,每一个C/C 库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用。

JNICALL:说明这个函数是一个JNI函数,用来和普通的C/C 函数进行区别。

Void: 返回值类型

JNI函数名原型:Java_ JNI方法所在的完整的类名,把类名里面的”.”替换成”_” 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样。

env 参数:是一个执行JNIENV函数表的指针。

3.2 JNIEnv 解析

JNIEnv 概念 : 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ;

调用 Java 函数 : JNIEnv 代表 Java 运行环境, 可以使用 JNIEnv 调用 Java 中的代码;操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象;

JNIEnv只在当前线程中有效。本地方法不能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。

4、这次测试中遇到的问题

1、找不到dll

配置的参数为 vm option ,不是程序参数

-Djava.library.path=D:clionTestJnicmake-build-debug

路径仅仅只到最后dll 所在的目录

2、找不到jni.h,jni_md.h

拷贝jni.h 到 c工程的目录。

3、打出来dll 无法运行,版本不匹配

因为我使用的MinGW 是64 的版本,但是我jdk 安装的版本是 32 的位的,导致运行报错

可以在控制台 使用java -version,如果没写64-Bit 就是32的,调用dll 会报错

重新安装64 位的jdk解决。

4、修改函数名导致不匹配
代码语言:javascript复制
Exception in thread "main" java.lang.UnsatisfiedLinkError: xxx()V

因为在测试期间,我修改了一次函数的名字,我只在.h 中同步修改了函数的名字,但是.c 中没有同步修改,导致调用报错。

修改函数名一致就可以了。

5、Java 和 C 数据类型的对照表

Java 和C 之间有很多类型不是相同的,下面列举一下数据类型的对照关系,在使用的时候对照就可以了,不用记。

1、基本类型的对应

代码语言:javascript复制
# include <inttypes.h>      /* C99 */
typedef uint8_t         jboolean;       /* unsigned 8 bits */
typedef int8_t          jbyte;          /* signed 8 bits */
typedef uint16_t        jchar;          /* unsigned 16 bits */
typedef int16_t         jshort;         /* signed 16 bits */
typedef int32_t         jint;           /* signed 32 bits */
typedef int64_t         jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#else
typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#endif
2、引用类型

6、总结

java 和C,C 之间的调用主要是函数格式的定义,然后加载动态链接库,直接访问就好了。记住规则就好了,没什么难的。

0 人点赞