JavaCPP探索(1) – 使用JavaCPP包装Live2D的本地库Live2DCubism

JavaCPP探索(1) – 使用JavaCPP包装Live2D的本地库Live2DCubism

我在过去一阵子尝试使用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了,它可以在这里找到:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Fantastic Soft

风铃之书是个人的工作和生活的总结和分享的站点,欢迎来访和留言,有时也会提供自家软件的发布版本和开源项目。