JAVA一分钟游戏编程-游戏库改造之JNI(07)

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玄铁剑,退到一旁,大口喘着粗气:“张师弟,靠你了!”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值