1.目标
一分钟实现游戏库改造之JNI
2. 序章
云顶山,开源仙门。
一间不起眼的茅草屋内,王富贵正呼呼大睡。
忽然,传来急促的敲门声。
“王师兄,快醒醒,大事不好了,李师弟失踪了!”
“什么?”
王富贵从睡梦中惊醒,一边穿着衣服,一边去开门。
门打开了那一瞬间,王富贵便瞧见了神情慌张的张平安,不由问道:“张师弟,怎么回事?”
张平安狠狠咽了口唾沫,语气中似乎还带着几分颤抖:“刚、刚才李师弟被一个神秘的黑衣人带走了!”
王富贵一愣:“神秘黑衣人?”
“是啊,从头黑到脚,看不清面容,但我记得他的衣服后背有一个白色的醒目图案,好像是C草。”
“emm...,你说的是C++吧!”
“是的,现在怎么办?”
“先别急,张师弟,你先描述一下事情的经过。”
于是,张平安开始回忆起来。
十分钟前。
李吉祥:“张师兄,我们现在用Swing实现了Pinea库的基本逻辑,但总觉得心里有块石头放不下,你说现在还有人用Swing这玩意吗?”
张平安:“确实用的少啊,Swing那玩意在00年、10年还有些热度,现在都20年了,各种桌面客户端库五花八门,要不是身处开源仙门,我压根都不想理会。”
“开源仙门……早期应该不叫这个名字吧?”
“是啊,那个时候还是一个不入流的小宗门,无人问津的那种,可自从春天(Spring)到来,它如沉眠巨兽猛然睁眼,蚕食万物,日益壮大,现在已为庞然大物,以至于像你我这般才华横溢之辈,却也只能在外门做一个小小的练气期修士。”
李吉祥叹了一口气:“据说宗门里的那些元婴老怪们不久前研究出了新玩意,号称下一代客户端应用平台系统的内功心法JavaFX。”
张平安:“那些老怪物们轴的狠,隔壁伪开源仙门(dotnet)的WinForm、WPF、WinUI3、MAUI还有各种三方框架狂炫酷扎吊炸天,我们开源仙门还研究个屁啊,我认为还是得有自己的发展方向。”
“自己的发展方向……”
李吉祥目光微凝,脑海中忽然萌生出一个想法:一定要改变,那些上古宗派内有很强的功法和武技。
看到李吉祥神色异样,张平安似乎预料到了什么:“李师弟,你不会是想……加入上古邪宗C/C++吧?”
“他们的功法虽然难修,有很多陷进,还容易发生内存泄漏,但胜在特性多,性能好,一旦练成……”说到这里,李吉祥稍微停顿了一下,转过头看向张平安,脸色凝重地说:“便能瞬秒同阶修士,甚至能越阶一战!”
张平安心头一惊,立刻打断:“小心,你这话要是被老怪物们听见,定要被送到仙门思过崖,面壁上百年,到时候非但功不成,寿元还会耗尽,毕生辉绩,堪堪化为一地白骨啊!”
就在这里,不知何处传来一道阴恻恻的声音。
“哈哈哈……看来老夫又遇上了一位志存高远的小辈,所谓相遇即是缘分,老夫今日便带你回到宗门,只要你刻苦修行,不忘初心,以后定有一番大成就,哪怕进阶大能也不是不可能!”
话落,从树林中闪出一道黑影,只是随手一抓,便将李吉祥抓起,旋即又没入林中,片刻功夫消失得无影无踪。
“李师弟!”
张平安想要追上前去,却发现双脚像是被禁锢一样,无法向前挪移分毫。
……
听完张平安的描述后,王富贵陷入了沉思。
“黑衣人,C++,不会有错,那肯定是邪宗C/C++的爪牙,迟则生变,走,我们一起去救李师弟。”
张平安脸色苍白:“怎么救?”
“我带上JNI打造的玄铁剑,你带上JNA符宝。”
“我知道了,JNI是Java Native Interface,是Java官方提供的原生接口标准,允许Java代码调用C/C++原生代码,也允许原生代码调用 Java 方法,实现了Java与原生代码的双向交互;”
“JNA是一个开源Java库(基于Apache协议),封装了JNI的复杂操作,允许Java代码直接调用动态链接库(如.dll、.so)中的函数,无需编写原生代码。”
张平安激动万分道:“有这两大神器,就能打开进入邪宗的通道了。”
说完,两人不再停留,朝云顶山山下而去。
序章完。
为了方便,我们后续只与C++打交道,因为C++是C的超集。
熟知Java语法的你,对C++的语法也会一目了然。
3. C++环境
C++环境:Visual Studio 2022社区版 或者Mac/Linux环境下使用 GCC
跨平台编译工具:CMake
开发工具:Visual Studio Code + C++插件
这时你又疑惑了,开发工具不是有Visual Studio吗,Debug能力这么强,为啥不用Visual Studio,反而多此一举,用Visual Studio Code?
问得好,只是习惯问题,你当然可以用Visual Studio,甚至还可以使用CLion,这些都没关系,我只想用到Visual Studio的MSVC编译器而已。
如果你的电脑是Mac或Linux,无法下载Visual Studio,那么下载GNU C++ Compiler,查看一下g++版本,如果不是最新,请升级到最新版本,避免部分语法不兼容的问题。
到此为止,一切准备就绪。
4. JNI调用C++库
4.1 JNI调用步骤
上述,我们通过小说剧情形式介绍了JNI是什么,这里再说明一下使用JNI调用的步骤和方法:
(1)在Java中定义Native方法
(2)使用javac编译Java类(或者使用Maven编译),然后再用javah生成C++头文件
(3)新建C++源码文件,实现头文件中的方法
(4)编译C++源码、头文件为动态库比如Pinea.dll
(5)加载本地库Pinea.dll,调用Native方法
4.2 C++动态库编译
4.2.1 C++代码目录结构
在使用JNI之前,我们先尝试编译一下C++的动态库:
首先,在项目根目录创建一个空目录csrc,这个目录用来存储C++相关代码,然后创建以下目录结构和文件:
(1)bin存储三方动态库xx.dll,以及编译后的动态库Pinea.dll
(2)include存储.h头文件
(3)lib存储三方动态链接库xx.lib,以及编译好的动态链接库Pinea.lib(JNI调用不需要链接库,可以忽略)
(4)src用来存储.cpp源文件
(5)CMakeLists.txt定义项目的构建规则和依赖关系
(6)run.bat执行构建的脚本
4.2.2 CMakeLists.txt配置
# 指定最低支持的 CMake 版本为 3.20
cmake_minimum_required(VERSION 3.20)
# 定义项目名称为 "Pinea"
project(Pinea)
# 设置 C++ 标准为 C++20
set(CMAKE_CXX_STANDARD 20)
# 强制要求 C++20 标准,如果编译器不支持则会报错
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 设置 Release 构建类型的可执行文件输出目录
# 适用于 .exe、.dll 等运行时文件(Windows 下 DLL 属于运行时输出)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/bin)
# 设置 Release 构建类型的静态库输出目录
# 适用于 .lib(Windows 静态库)、.a(Linux 静态库)等归档文件
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/lib)
# 创建一个名为 ${PROJECT_NAME}(即 "Pinea")的共享库(动态链接库)
# SHARED 关键字指定生成动态库(Windows 下为 .dll,Linux 下为 .so)
# 源文件为 src 目录下的 Pinea.cpp
add_library(${PROJECT_NAME} SHARED src/Pinea.cpp)
# 为目标库设置私有头文件搜索目录
# PRIVATE 表示该目录仅用于当前目标的编译,不传递给依赖它的其他目标
# ${CMAKE_SOURCE_DIR}/include 指向项目根目录下的 include 文件夹
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include)
# 为目标库设置链接目录
# 指定链接阶段查找库文件(.lib、.a 等)的路径
# 这里指向项目根目录下的 lib 文件夹
target_link_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib)
4.2.3 run.bat编译脚本
@echo off
:: 关闭命令行的默认回显功能,使输出更简洁(仅显示执行结果,不重复显示命令本身)
:: 检查当前目录下是否存在 "build" 文件夹
if not exist build (
:: 如果不存在,则创建 "build" 文件夹(用于存放 CMake 生成的中间文件和构建产物)
mkdir build
)
:: 进入刚刚创建或已存在的 "build" 文件夹(后续操作将在该目录下执行)
cd build
:: 使用 CMake 生成 Visual Studio 2022 项目文件
:: -G "Visual Studio 17 2022":指定生成器为 Visual Studio 2022
:: -A x64:指定目标架构为 64 位(避免默认生成 32 位项目)
:: ..:表示 CMakeLists.txt 所在的路径(当前 build 文件夹的父目录,即项目根目录)
cmake -G "Visual Studio 17 2022" -A x64 ..
:: 使用 CMake 调用构建工具编译项目
:: --build .:表示编译当前目录(build 文件夹)下的项目
:: --config=Release:指定构建类型为 Release(生成优化后的发布版本)
cmake --build . --config=Release
:: 编译完成后,从 build 文件夹返回到项目根目录
cd ..
4.2.4 C++头文件
在include目录下创建头文件Pinea.h:
#pragma once
// 仅定义一个方法
void hello();
4.2.5 C++源文件
在src目录下创建源文件Pinea.cpp
#include "Pinea.h"
#include <iostream>
void hello() {
// 简单打印
std::cout << "Hello Pinea!" << std::endl;
}
Pinea.h的hello定义主要用于声明作用,Pinea.cpp的hello才是具体逻辑,有点类似Java中的接口和实现的概念对吧,实际上却不是一回事,这里就不细说了,不是重点。
保存好上述代码后,打开终端控制台,执行run.bat脚本。
编译成功,而且bin目录下生成了Pinea.dll,代表我们的C++代码环境搭建没有问题了,可喜可贺。
4.3 JNI具体实现
4.3.1 Java Native方法定义
上述我们测试了编译C++动态库,下面就让我们正式编写JNI吧。
第一步,在Java类中定义Native方法:
package com.jni;
public class Pinea {
// 定义一个Java Native方法
public static native void pineaInit();
}
4.3.2 生成JNI头文件
第二步,先使用javac编译java(或者maven编译),我这里选择的Maven编译,因为Maven能自动管理依赖关系。
IDEA中使用快捷键Ctrl+B编译后,在生成target目录下可以看到class二进制文件:
接着,使用javah生成头文件:
看一下生成的com_jni_Pinea.h都有些啥吧:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_Pinea */
#ifndef _Included_com_jni_Pinea
#define _Included_com_jni_Pinea
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_jni_Pinea
* Method: pineaInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_Pinea_pineaInit
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
如果没有接触过C/C++的哥们肯定是一脸懵逼,这都是啥玩意啊,心里琢磨着开始换赛道了。。。
别急,还没有放弃的时机呢,后面有大把的机会放弃😏,开玩笑嘿嘿🤪。
张平安:“我们还没救出吉祥老弟,怎可轻言放弃?”
4.3.3 实现JNI头文件方法
第三步,我们在Pinea.cpp中实现头文件方法:
先不考虑头文件啥含义,跟着依葫芦画瓢就完事了:
将target下的com_jni_Pinea.h文件拷贝到csrc/include目录下,像这样:
接着,原封不动拷贝Java_com_jni_Pinea_pineaInit方法到Pinea.cpp,并将头文件include进来,最后修改如下:
#include "Pinea.h"
#include <iostream>
#include "com_jni_Pinea.h"
// 原封不动拷贝
JNIEXPORT void JNICALL Java_com_jni_Pinea_pineaInit
(JNIEnv *, jclass) {
// 调用hello()方法
hello();
}
void hello() {
// 简单打印
std::cout << "Hello Pinea!" << std::endl;
}
4.3.4 编译动态库
第四步,编译动态库,直接运行run.bat即可:
编译出错了,为啥,因为没有jni.h文件,那么它在哪呢?
王富贵:“JDK安装目录下有一个include,就在这。”
谨慎一点,相关文件一并拷贝过来:
张平安:“include目录结构真乱,jni头文件和自定义头文件混在一起,难看,能不能有点美感?”
说得在理,我们在include新建额外的目录jni区分一下:
修改CMakeLists.txt配置,添加jni为include目录:
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/include/jni
)
再次编译,依然报错🥲:
张平安:“哥们,别急,我知道你离放弃就差一个念头了,你且看看jni/win32目录下面。”
果然,这里有jni_md.h文件,再次修改CMakeLists.txt配置,并添加include/jni/win32为include目录:
再次编译,没有任何报错,我们的本地库Pinea.dll算是编译好了。
4.3.5 使用动态库
第五部,新建测试类,加载csrc/bin/Pinea,注意这里不需要后缀.dll。
千难万难,总算成功了。
5. 思考:JNA如何调用呢?
邪宗C/C++后山不知名防护结界处。
王富贵咬着牙,挥动JNI玄铁剑,一下又一下轰击在结界光幕上。
每轰击一次,结界破开一个拳头大小的口子,但又立马自动闭合。
半个时辰后,王富贵额头青筋直冒,汗流浃背,却不见结界有任何松动迹象。
张平安双臂环抱着站在一块巨石上。
“王师兄,你那JNI玄铁宝剑攻势虽然很强,但是平A一下的CD着实有些过长,破阵讲究快准狠,你且闪在一旁,看我祭出结丹期修士炼制出的超强威能符宝——JNA!”
王富贵收回JNI玄铁剑,退到一旁,大口喘着粗气:“张师弟,靠你了!”