我在过去一阵子尝试使用JNI制作本地类库的Java接口,尝试拓展Java应用程序的边界,在这个过程中,我注意到目前Java存在的几种常见的本地接口形式:
标准JNI,也就是非常标准的也是相对来说非常古老的接口形式,通过C语言作为桥梁,在本地调用其他的C或者C++类库,并且通过JVM在JNI中的指针操作Java对象,从而让两者可以相互协作的技术,它的能力非常全面,也非常繁琐。
JNA,一种更简化的Java本地接口技术,它通过Java层面的声明让我们可以不再编写JNI层,直接加载和使用本地类库,但是这种做法也存在一个非常大的问题,它无法做到从C语言层调用Java的层的能力,无法进行回调,因此更适用于简单的接口开发,他很多时候都需要和Java自身的JNI接口协同使用,虽然不完美,但是也可以减少一定的复杂性,从某种层面来说,它看起来已经有一点FFI API的影子了。
SWIG项目,它是一种代码生成器,通过这个工具可以快速的把本地类库的功能导出到各种语言,例如Java的JNI,Python,C#等都是可以的,不过这个项目需要用户通过DSL编写映射的规则,所以繁琐性不比JNI更少,但它胜在通用性。
JavaCPP,也就是接下来我主要使用的,通过Java Cpp的可执行jar,我们可以把特定的source生成为JNI的中间代码,它提供了Maven插件,可以相当方便的嵌入到Java的编译过程中,由于它会缓存一些Java的类型信息,所以效率会相对较高。
我之所以注意到Java Cpp这个项目,是因为最近一段时间在折腾FFMPEG,我发现它是使用一个JavaCPP的技术方案构建的,这个JavaCPP框架可以很方便的为本地类库创建Java的绑定,而且目前已经有了不少本地类库的Java版本,因此,我认为通过这种技术构建一个Live2D的Java类库应该是非常可行的。
而且我想在JavaFx使用Live2D,偏偏这玩意就没有Java的版本,虽然官方表示存在一个“JavaSDK”,可这个实际上却是“Android SDK”,不过好在它提供了一个Native SDK,我想通过这个SDK并且参考其他语言的实现方式,制作一个Java版本的应该问题不大。
准备工作
由于最终是要编译JNI类库,所以肯定是需要本地的编译环境的,对于Windows,那自然是Visual Studio,下载一个最新的社区版Visual Studio,安装C/C++相关的组件,在这之后,可以在开始菜单找到“Developer Command Prompt for VS 2022”,当然现在安装的肯定不是2022了。

能够找到这个,就说明编译环境基本准备完毕,此时,还需要一个Maven,可以去Maven官网下载(通常我们使用的都是IDE内置的Maven,但是这个不行,得在Developer Command Prompt环境里面执行Maven,所以需要外部的Maven)。
Maven解压到合适的位置即可,需要把Maven的bin目录加入系统的Path环境变量里面,然后需要检查系统是否具备JavaHome,以及Java是否存在于Path环境变量里面(Maven需要一个JAVA_HOME环境变量,有些时候可以直接通过IDE启动Java,所有没有配置这个)。
到此为止,准备工作就已经完成了。
配置合适的POM
想要使用JavaCPP构建一个本地类库的Java版本,首先需要引入JavaCPP的类库,我们可以通过properties的tag决定使用的版本号,当前最新版本的Maven坐标如下:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javacpp.version>1.5.10</javacpp.version>
<javacpp.platform>windows-x86_64</javacpp.platform>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.bytedeco/javacpp -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${javacpp.version}</version>
</dependency>
</dependencies>javacpp.platform在不同操作系统中进行编译的时候,需要修改为操作系统的platform值,除此以外,还需要一个Maven的插件,这个插件可以用来生成Java源码和编译JNI类库:
<build>
<plugins>
<plugin>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${javacpp.version}</version>
<configuration>
<properties>${javacpp.platform}</properties>
<classPath>${project.build.outputDirectory}</classPath>
</configuration>
<executions>
<execution>
<id>javacpp.parser</id>
<phase>generate-sources</phase>
<goals>
<goal>build</goal>
</goals>
<configuration>
<!-- 是否跳过生成过程 -->
<skip>false</skip>
<outputDirectory>${project.build.sourceDirectory}</outputDirectory>
<!-- 这里是我的Live2d包名,如果打包其他的类库,请自行修改 -->
<classOrPackageName>org.swdc.live2d.core.*</classOrPackageName>
</configuration>
</execution>
<execution>
<id>javacpp.compiler</id>
<phase>process-classes</phase>
<goals>
<goal>build</goal>
</goals>
<configuration>
<!-- 是否跳过生成Class的过程 -->
<skip>false</skip>
<!-- 输出位置,务必添加此配置。 -->
<outputDirectory>${project.build.sourceDirectory}</outputDirectory>
<!-- 这里是我的Live2d包名,如果打包其他的类库,请自行修改 -->
<classOrPackageName>org.swdc.live2d.core.*</classOrPackageName>
<copyLibs>true</copyLibs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>JavaCpp的配置类
JavaCpp通过配置类和标注在上面的注解来决定它在编译中的行为,所以,为了能够正确的编译Live2dCubismSDK,需要一个继承了JavaCpp的InfoMapper类型的配置类:
package org.swdc.live2d.core;
import org.bytedeco.javacpp.annotation.Platform;
import org.bytedeco.javacpp.annotation.Properties;
import org.bytedeco.javacpp.tools.Info;
import org.bytedeco.javacpp.tools.InfoMap;
import org.bytedeco.javacpp.tools.InfoMapper;
@Properties(value = {
@Platform(
// 平台为Windows-x86_64,也就是普通的64为Windows系统。
value = "windows-x86_64",
// 本地类库的Include路径,也就是本类库的头文件路径
includepath = "platforms/Core/include",
// 本地类库的头文件,只写需要生成Java接口的头文件即可
include = "Live2DCubismCore.h",
// 本地类库的lib文件路径,这些文件提供了dll的导出函数列表
linkpath = "platforms/Core/dll/windows/x86_64/",
// 需要链接的lib文件名(不要加.lib)
link = "Live2DCubismCore"
)
},
// 导出的Java类,得写全限定名,带包名那种。
global = "org.swdc.live2d.core.Live2dCore",
// 这个也是。
target = "org.swdc.live2d.core.Live2dCore"
)
public class Live2dCoreConfigure implements InfoMapper {
@Override
public void map(InfoMap infoMap) {
// 解释一下这里的处理,因为“csmCallingConvention”,
// 它们是没有实际作用的宏定义,出现在这里是为了标记接口的调用类型,
// Windows下是“__stdcall”,这个会导致JavaCPP的识别出现问题,所以,需要想办法清除掉它。
// CppText方法用于修改在JavaCPP解析header过程中找到的c++定义,所以使用新的define语句覆盖掉
// 原本define csmCallingConvention位stdcall的声明即可。
infoMap.put(new Info("csmCallingConvention").cppText("#define csmCallingConvention"));
}
}让我们关注类上面额度注解,Properties注解是一个总体的注解,通常每一个JavaCpp的配置类上都需要该注解,它的Value是一个Platform注解构成的数组,而target的作用是指定导出的Java包名,如果你需要JavaCpp分别导出Header里面的Struct,Class到Java,那么你需要在target填写一个包名,如果你想把它们全部导出到一个Class里面,不拆分为多个Class,那么这里应该填写为类名,global为这些Header所包含的函数所在的Java类型,所有的定义在Header里面的函数都会被导出到global指定的类里面。
Platform注解用于标注的,是与操作系统紧密相关的内容,它的Value是操作系统的platform类型,例如64位的Windows系统,应当是“Windows-x86_64”,这里的“x86_64”指的是处理器的架构,所以platform的正确写法一般是操作系统名称 + “-” + “处理器架构”,includepath指的是存放第三方库——那个我们想要包装位Java接口的类库的头文件所在的位置,填写的是相对路径,相对于项目的根目录,include指的是本配置类包含的头文件名称,可以是一个也可以是多个,linkpath是包含第三方库的dll的目录,而link则是第三方库的文件名,通常来说,第三方库应当至少包含一个静态库,静态库至少包含该库导出的符号表。
当然,动态库也是很常用的,但是动态库通常也会带一个仅包含符号表的静态的库文件用于链接。
至于map方法,这里面我们可以对Header内部的符号进行更全面更细致的控制,例如忽略一部分内容,重新定义一部分内容,映射C++和C语言的类型为Java类型等,由于Live2DCubismSDK的功能相对简单,你可以看到这里面内容非常少。

这个项目的本地类库目录结构是这样的,结合配置类就能明白为什么需要这样配置它们。
编译类库
找到开始菜单的Developer Command Prompt附近的x64 Native Tools Command Prompt for VS 2022,注意,不是那个“Developer Command Prompt”。
如果系统是64位的,在Developer Command Prompt下执行的时候,很有可能会默认构建x86的类库,x86的类库是不能链接到x64的类库的,当然,反过来也不行,所以64位类库构建的时候,需要使用64位的Prompt,位数要一致,位数不一致导致的链接失败如下图所示:

在这个命令行窗口中进入项目的目录,然后执行mvn package即可,注意,如果刚刚执行过Maven的Clean,或者首次执行package,那么此时应当执行两次Package指令,第一次执行JavaCPP有可能无法找到他需要的Class,如果一切顺利,那么此时一个包含本地类库的JNI包就会出现在target里面:


打包结束,应该可以得到内容如上图所示的jar包,本项目当然也上传Github了,它可以在这里找到:





发表回复