JavaCPP 是一个开源库,它提供了在 Java 中高效访问本地 C++的方法。采用 JNI 技术实现,所以支持所有 Java 实现包括 Android 系统,Avian 和 RoboVM。
一种基于 Linux 的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由 Google 公司和开放手机联盟领导及开发。
Avian 是一个轻量级的 Java 虚拟机和类库,提供了 Java 特性的一个有用的子集,适合开发跨平台、自包容的应用程序。它实现非常快速而且体积小,主要特性包括如下四点:
RoboVM 编译器可以将 Java 字节码翻译成 ARM 或者 x86 平台上的原生代码,应用可直接在 CPU 上运行,无需其他解释器或者虚拟机。RoboVM 同时包含一个 Java 到 Objective-C 的桥,可像其他 Java 对象一样来使用 Objective-C 对象。大多数 UIKit 已经支持,而且将会支持更多的框架。
总的来说,JavaCPP 提供了一系列的 Annotation 将 Java 代码映射到 C++代码,并使用一个可执行的 jar 包将 C++代码转化为可以从 JVM 内调用的动态链接库文件。
与其他技术相比,特性总结如下表 1 所示。
技术名称 | 技术介绍 |
---|---|
CableSwig | 用于针对 Tcl 和 Python 语言创建接口 |
JNIGeneratorApp | 所有用于 SWT 的 C 代码都是通过它来创建的 |
cxxwrap | 用于生成针对 C++的 Java JNI 包、HTML 文档、用户手册 |
JNIWrapper | 商业版本,可以帮助实现 Java 和本地代码之间的无缝结合 |
Platform Invoke | 微软发布的一个工具 |
GlueGen | 针对 C 语言的一个工具,帮助生成 JNI 代码 |
LWJGL Generator | JNI 代码生成器 |
ctypes | 针对 Python 的接口代码生成器 |
JNA | JNA(Java Native Access)提供一组 Java 工具类用于在运行期动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。开发人员只要在一个 Java 接口中描述目标 native library 的函数与结构,JNA 将自动实现 Java 接口到 native function 的映射。 |
JNIEasy | 替换 JNA 的一种技术 |
JNative | Windows 版本的库 (DLL),提供了 JNI 代码生成 |
fficxx | 针对 haskell 模型的代码生成器,主要生成 C 语言 |
JavaCPP | 更加自然高效,它支持大部分的 C++语法特性。目前已经能成功封装 OpenCV, FFmpeg, libdc1394, PGR FlyCapture, OpenKinect, videoInput, and ARToolKitPlus。除此之外,它还能直接把 C/C++的头文件转化成 Java 类,能自动生成 JNI 代码,编译成本地库,开发人员无需编写繁琐的 C++、JNI 代码,从而提高开发效率。 |
为了调用本地方法,JavaCPP 生成了对应的 JNI 代码,并且把这些代码输入到 C++编译器,用来构建本地库。使用了 Annotations 特性的 Java 代码在运行时会自动调用 Loader.load() 方法从 Java 资源里载入本地库,这里指的资源是工程构建过程中配置好的。
我们先来演示一个例子,这是一个简单的注入/读出方法,类似于 JavaBean 的工作方式。清单 1 所示的 LegacyLibrary.h 包含了 C++类。
#include <string> namespace LegacyLibrary { class LegacyClass { public: const std::string& get_property() { return property; } void set_property(const std::string& property) { this->property = property; } std::string property; }; }
接下来定义一个 Java 类,驱动 JavaCPP 来完成调用 C++代码。
import org.bytedeco.javacpp.*; import org.bytedeco.javacpp.annotation.*; @Platform(include="LegacyLibrary.h") @Namespace("LegacyLibrary") public class LegacyLibrary { public static class LegacyClass extends Pointer { static { Loader.load(); } public LegacyClass() { allocate(); } private native void allocate(); // to call the getter and setter functions public native @StdString String get_property(); public native void set_property(String property); // to access the member variable directly public native @StdString String property(); public native void property(String property); } public static void main(String[] args) { // Pointer objects allocated in Java get deallocated once they become unreachable, // but C++ destructors can still be called in a timely fashion with Pointer.deallocate() LegacyClass l = new LegacyClass(); l.set_property("Hello World!"); System.out.println(l.property()); } }
以上两个类放在一个目录下面,接下来运行一系列编译指令,如清单 3 所示。
$ javac -cp javacpp.jar LegacyLibrary.java $ java -jar javacpp.jar LegacyLibrary $ java -cp javacpp.jar LegacyLibrary Hello World!
我们看到清单 3 最后运行输出了一行“Hello World!”,这是 LegacyLibrary 类里面定义好的,通过一个 setter 方法注入字符串,getter 方法读出字符串。
我们可以看到文件夹里面内容的变化,刚开始的时候只有.h、.java 两个文件,清单 3 所示的 3 个命令运行过后,生成了 class 文件及本地方法 (native method) 对应的.so 文件。
/home/zhoumingyao/javacpp-1.0-bin/javacpp-bin [root@node1:2 javacpp-bin]# ls -lrt 总用量 348 -rw-r--r-- 1 root root 30984 7 月 11 00:59 LICENSE.txt -rw-r--r-- 1 root root 21986 7 月 11 08:52 README.md -rw-r--r-- 1 root root 31955 7 月 11 08:53 CHANGELOG.md -rw-r--r-- 1 root root 243318 7 月 11 12:20 javacpp.jar -rw-r--r-- 1 root root 285 8 月 11 16:07 LegacyLibrary.h -rw-r--r-- 1 root root 1026 8 月 11 16:13 LegacyLibrary.java -rw-r--r-- 1 root root 643 8 月 11 16:13 LegacyLibrary$LegacyClass.class -rw-r--r-- 1 root root 794 8 月 11 16:13 LegacyLibrary.class drwxr-xr-x 2 root root 4096 8 月 11 16:13 linux-x86_64 [root@node1:2 javacpp-bin]# ls -lrt linux-x86_64 总用量 36 -rwxr-xr-x 1 root root 35784 8 月 11 16:13 libjniLegacyLibrary.so
为了方便用户使用 JavaCPP,该项目下属有一个 presets 项目,它将一些常用的项目,例如 OpenCV、FFMpeg 等,都编译好了让用户通过调用 Jar 包的方式直接使用。当然,它也允许用户通过简便的方式上传自己做的本地库文件,通过将 jar 包上传到 Maven 仓库的方式共享给其他用户。
如果我们想要使用 JavaCPP-presents,我们需要下载 presets 源代码或者已经编译好的 jar 文件。
具体下载地址:https://github.com/bytedeco/javacpp-presets。
编译好的 jar 文件有很多,主要是 JavaCPP 支持的项目,如图 1 所示。
JavaCPP Presets 模型包括了很多广泛被使用到的 C/C++类库的 Java 配置和接口类。编译器结合 C/C++的头文件,使用 org.bytedeco.javacpp.presets 包里面的配置文件来创建 Java 接口文件,这样就可以产生类似于 JNI 的库,Java 程序可以调用底层的 C/C++库。它的机制较为方便,可以被用在 Java 平台、Android 平台。
这个项目提供了两种下载方式,一种是集成了常用库的 jar 包,支持 Android、Linux Fedora、Mac OS X、Windows 等操作系统,另一种是该项目的源代码,您可以自己编译适用于自己开发环境的 jar 包,当然如果您希望针对自己的 c++工程制作 jar 包,可以采用其他方式。
如果下载的是 JavaCPP-presets 源代码包,则 Centos 环境(该环境默认不被支持)需要安装 JDK、Maven、GCC,这样才能编译项目成为需要的 Jar 包。
配置 Maven 的方式如清单 5 所示。
1。下载 Maven 并上传到服务器,这里上传到了/root 目录下面; 2。vi /etc/profile 在最后两行加上代码: export MAVEN_HOME=/root/apache-maven-3.1.1 export PATH=${MAVEN_HOME}/bin:${PATH} 3。source /etc/profile 4。[root@node1:2 bin]# mvn -v Apache Maven 3.1.1 (0728685237757ffbf44136acec0402957f723d9a; 2013-09-17 23:22:22+0800) Maven home: /root/apache-maven-3.1.1 Java version: 1.8.0_45, vendor: Oracle Corporation Java home: /usr/share/jdk1.8.0_45/jre Default locale: zh_CN, platform encoding: UTF-8 OS name: "linux", version: "2.6.32-504.el6.x86_64", arch: "amd64", family: "unix"
如果您想要尝试完全动手编译 presets 项目,那您可以在 Maven 的 pom.xml 文件里面配置,如清单 6 所示,这样您可以下载到所有需要的代码。
<dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>${moduleName}</artifactId> <version>${moduleVersion}-1.0</version> </dependency>
JavaCPP-Presents 已经默认包含了一些开源库,例如 OpenCV、FFmpeg、FlyCapture、GSL、CUDA、Tesseract 等等,我们可以通过运行 Maven 命令来编译、构建.so 文件和 jar 文件,命令是$ mvn install --projects .,opencv,ffmpeg,flycapture,libdc1394,libfreenect,videoinput,artoolkitplus,etc.
如清单 7 所示,我们编译 ffmpeg 库,我这里只截取小部分的打印输出,都是到 Maven 仓库下载 jar 包的过程输出。
[root@localhost javacpp-presets]# mvn install --projects ffmpeg [INFO] Scanning for projects... Downloading: http://repo.maven.apache.org/maven2/org/sonatype/plugins/ nexus-staging-maven-plugin/1.6/nexus-staging-maven-plugin-1.6.pom Downloaded: http://repo.maven.apache.org/maven2/org/sonatype/plugins/ nexus-staging-maven-plugin/1.6/nexus-staging-maven-plugin-1.6.pom (12 KB at 0.7 KB/sec) Downloading: http://repo.maven.apache.org/maven2/org/sonatype/ nexus/maven/nexus-staging/1.6/nexus-staging-1.6.pom Downloaded: http://repo.maven.apache.org/maven2/org/sonatype/ nexus/maven/nexus-staging/1.6/nexus-staging-1.6.pom (3 KB at 3.8 KB/sec) Downloading: http://repo.maven.apache.org/maven2/org/sonatype/ nexus/maven/nexus-maven-plugins/1.6/nexus-maven-plugins-1.6.pom Downloaded: http://repo.maven.apache.org/maven2/org/sonatype/ nexus/maven/nexus-maven-plugins/1.6/nexus-maven-plugins-1.6.pom (17 KB at 7.8 KB/sec) Downloading: http://repo.maven.apache.org/maven2/org/sonatype/ buildsupport/public-parent/5/public-parent-5.pom
讲解程序前,我们先来了解一下 cppbuild.sh 这个文件,这个脚本在根目录下面,它主要功能是被用来构建和创建本地 C++库。例如清单 8 所示,我们编译 ffmpeg 库,
./cppbuild.sh -platform linux-x86_64 install ffmpeg
脚本的第一个参数是-platform,值是 linux-x86_64 判断代码如清单 9 所示,如果第一个参数是-platform,那么初始化变量 PLATFORM,然后参数位移动一位到第二个参数,如果第二个参数是 install,那么初始化变量 OPERATION 为 install,如果第二个参数是 clean,初始化变量 OPERATION 为 clean。这里是 install。
while [[ $# > 0 ]]; do case "$1" in -platform) shift PLATFORM="$1" ;; install) OPERATION=install ;; clean) OPERATION=clean ;; *) PROJECTS+=("$1") ;; esac shift done
确定了需要 install 操作后,程序进入实际执行阶段,如清单 10 所示。
case $OPERATION in install) if [[ ! -d $PROJECT ]]; then echo "Warning: Project /"$PROJECT/" not found" else echo "Installing /"$PROJECT/"" mkdir -p $PROJECT/cppbuild pushd $PROJECT/cppbuild source ../cppbuild.sh popd fi
清单 10 所示代码,创建 ffmpeg/cpubuild 目录,接下来将目录压入目录栈,在当前 bash 环境下读取并执行 ffmpeg 目录下的 cpubuild.sh 中的命令,最后将目录弹出目录栈。从这里可以看出,真实执行的是 ffmpeg 目录下面的 cpubuild.sh 命令,它的代码在这里不做展开,主要执行的是一连串的 make 命令,编译 C++代码、生成.so 文件。
我们这里所举例的例子是一个调用 FFmpeg 多媒体库的示例。FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用 LGPL 或 GPL 许可证。它提供了录制、转换以及流化音视频的完整解决方案。
如果想要下载 FFmpeg 源代码或者库文件,可以在这里查找:http://ffmpeg.org/。
如果想要查看 FFmpeg 的 API,可以在这里查找:http://bytedeco.org/javacpp-presets/ffmpeg/apidocs/。
运行整个程序,我们需要三个文件,大家把这三个文件放在同一个目录下面。
首先是 C 的源代码,这里只引用一小部分,剩余的可以到 github 上看,作者是 Stephen Dranger。
// // This tutorial was written by Stephen Dranger (dranger@gmail.com). // // Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de) // Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1 // A small sample program that shows how to use libavformat and libavcodec to // read video from a file. // // Use the Makefile to build all examples. // // Run using // // tutorial01 myvideofile.mpg // // to write the first five frames from "myvideofile.mpg" to disk in PPM // format. #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <stdio.h> void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) { FILE *pFile; char szFilename[32]; int y; // Open file sprintf(szFilename, "frame%d.ppm", iFrame); pFile=fopen(szFilename, "wb"); if(pFile==NULL) return; // Write header fprintf(pFile, "P6/n%d %d/n255/n", width, height); // Write pixel data for(y=0; y<height; y++) fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile); // Close file fclose(pFile); } int main(int argc, char *argv[]) { AVFormatContext *pFormatCtx = NULL; int i, videoStream; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVFrame *pFrame = NULL; AVFrame *pFrameRGB = NULL; AVPacket packet; int frameFinished; int numBytes; uint8_t *buffer = NULL; AVDictionary *optionsDict = NULL; struct SwsContext *sws_ctx = NULL;
接下来,需要创建一个 pom.xml 文件,这样可以利用 Maven 仓库下载我们需要的 FFmpeg 库文件。pom.xml 文件内容如清单 12 所示。
<project> <modelVersion>4.0.0</modelVersion> <groupId>org.bytedeco.javacpp-presets.ffmpeg</groupId> <artifactId>tutorial01</artifactId> <version>1.0</version> <dependencies> <dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>ffmpeg</artifactId> <version>2.7.1-1.0</version> </dependency> </dependencies> </project>
最后是 Java 代码的实现,在这个 Java 代码里面,它会调用 FFmpeg 的库函数进行针对视频的转换,如清单 13 所示。
import java.io.*; import org.bytedeco.javacpp.*; import static org.bytedeco.javacpp.avcodec.*; import static org.bytedeco.javacpp.avformat.*; import static org.bytedeco.javacpp.avutil.*; import static org.bytedeco.javacpp.swscale.*; public class testJavaCPP { static void SaveFrame(AVFrame pFrame, int width, int height, int iFrame) throws IOException { // Open file OutputStream stream = new FileOutputStream("frame" + iFrame + ".ppm"); // Write header stream.write(("P6/n" + width + " " + height + "/n255/n").getBytes()); // Write pixel data BytePointer data = pFrame.data(0); byte[] bytes = new byte[width * 3]; int l = pFrame.linesize(0); for(int y = 0; y < height; y++) { data.position(y * l).get(bytes); stream.write(bytes); } // Close file stream.close(); } public static void main(String[] args) throws IOException { AVFormatContext pFormatCtx = new AVFormatContext(null); int i, videoStream; AVCodecContext pCodecCtx = null; AVCodec pCodec = null; AVFrame pFrame = null; AVFrame pFrameRGB = null; AVPacket packet = new AVPacket(); int[] frameFinished = new int[1]; int numBytes; BytePointer buffer = null; AVDictionary optionsDict = null; SwsContext sws_ctx = null; if (args.length < 1) { System.out.println("Please provide a movie file"); System.exit(-1); } // Register all formats and codecs av_register_all(); // Open video file if (avformat_open_input(pFormatCtx, args[0], null, null) != 0) { System.exit(-1); // Couldn't open file } // Retrieve stream information if (avformat_find_stream_info(pFormatCtx, (PointerPointer)null) < 0) { System.exit(-1); // Couldn't find stream information } // Dump information about file onto standard error av_dump_format(pFormatCtx, 0, args[0], 0); // Find the first video stream videoStream = -1; for (i = 0; i < pFormatCtx.nb_streams(); i++) { if (pFormatCtx.streams(i).codec().codec_type() == AVMEDIA_TYPE_VIDEO) { videoStream = i; break; } } if (videoStream == -1) { System.exit(-1); // Didn't find a video stream } // Get a pointer to the codec context for the video stream pCodecCtx = pFormatCtx.streams(videoStream).codec(); // Find the decoder for the video stream pCodec = avcodec_find_decoder(pCodecCtx.codec_id()); if (pCodec == null) { System.err.println("Unsupported codec!"); System.exit(-1); // Codec not found } // Open codec if (avcodec_open2(pCodecCtx, pCodec, optionsDict) < 0) { System.exit(-1); // Could not open codec } // Allocate video frame pFrame = av_frame_alloc(); // Allocate an AVFrame structure pFrameRGB = av_frame_alloc(); if(pFrameRGB == null) { System.exit(-1); } // Determine required buffer size and allocate buffer numBytes = avpicture_get_size(AV_PIX_FMT_RGB24, pCodecCtx.width(), pCodecCtx.height()); buffer = new BytePointer(av_malloc(numBytes)); sws_ctx = sws_getContext(pCodecCtx.width(), pCodecCtx.height(), pCodecCtx.pix_fmt(), pCodecCtx.width(), pCodecCtx.height(), AV_PIX_FMT_RGB24, SWS_BILINEAR, null, null, (DoublePointer)null); // Assign appropriate parts of buffer to image planes in pFrameRGB // Note that pFrameRGB is an AVFrame, but AVFrame is a superset // of AVPicture avpicture_fill(new AVPicture(pFrameRGB), buffer, AV_PIX_FMT_RGB24, pCodecCtx.width(), pCodecCtx.height()); // Read frames and save first five frames to disk i = 0; while (av_read_frame(pFormatCtx, packet) >= 0) { // Is this a packet from the video stream? if (packet.stream_index() == videoStream) { // Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet); // Did we get a video frame? if (frameFinished[0] != 0) { // Convert the image from its native format to RGB sws_scale(sws_ctx, pFrame.data(), pFrame.linesize(), 0, pCodecCtx.height(), pFrameRGB.data(), pFrameRGB.linesize()); // Save the frame to disk if (++i<=5) { SaveFrame(pFrameRGB, pCodecCtx.width(), pCodecCtx.height(), i); } } } // Free the packet that was allocated by av_read_frame av_free_packet(packet); } // Free the RGB image av_free(buffer); av_free(pFrameRGB); // Free the YUV frame av_free(pFrame); // Close the codec avcodec_close(pCodecCtx); // Close the video file avformat_close_input(pFormatCtx); System.exit(0); }
所需要的文件创建完毕以后,可以通过 maven 命令来编译、执行程序,如清单 14 所示。
mvn package exec:java -Dexec.mainClass=testJavaCPP -Dexec.args="您的视频文件"
从清单 10 我们知道,最终调用的是自己 C++代码文件夹里面的 cppbuild.sh 文件,所以我们如果想要增加一个新的库,势必也需要创建该文件。总的来说,我们需要注意三点: 1. 创建一个全部小写字母组成的文件夹,这个文件夹的名称和最终生成的 JAR 包的文件名,以及 Maven 的 attifact 名称会完全一致,例如 testc++; 2. 在这个文件夹下,创建新的工程,这个工程需要包括 cppbuild.sh 文件和 pom.xml 文件,以及属于 org.bytedeco.javacpp.presets 包的 Java 的配置文件; 3. 上述 2 步到位后,发起一个请求,编译自己的代码,然后上传二进制库文件到 Maven 中央仓库,这样其他用户也可以调用您的库实现本地方法操作了。 我们以 java.util.zip 包为例,里面包含了一个 zlib 库,首先需要创建 cppbuild.sh,代码里面需要下载 zlib 源代码,如清单 15 所示。
#!/bin/bash # This file is meant to be included by the parent cppbuild.sh script if [[ -z "$PLATFORM" ]]; then pushd .. bash cppbuild.sh "$@" zlib popd exit fi if [[ $PLATFORM == windows* ]]; then ZLIB_VERSION=128 download http://zlib.net/zlib$ZLIB_VERSION-dll.zip zlib$ZLIB_VERSION-dll.zip mkdir -p $PLATFORM cd $PLATFORM unzip ../zlib$ZLIB_VERSION-dll.zip -d zlib$ZLIB_VERSION-dll cd zlib$ZLIB_VERSION-dll else ZLIB_VERSION=1.2.8 download http://zlib.net/zlib-$ZLIB_VERSION.tar.gz zlib-$ZLIB_VERSION.tar.gz mkdir -p $PLATFORM cd $PLATFORM tar -xzvf ../zlib-$ZLIB_VERSION.tar.gz cd zlib-$ZLIB_VERSION fi case $PLATFORM in linux-x86) CC="gcc -m32 -fPIC" ./configure --prefix=.. --static make -j4 make install ;; *) echo "Error: Platform /"$PLATFORM/" is not supported" ;; esac cd ../.. pom.xml 如清单 16 所示,最终生成 zlib 的 jar 包。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.bytedeco</groupId> <artifactId>javacpp-presets</artifactId> <version>0.10</version> </parent> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>zlib</artifactId> <version>1.2.8-${project.parent.version}</version> <packaging>jar</packaging> <name>JavaCPP Presets for zlib</name> <dependencies> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-resources-plugin</artifactId> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> </plugin> <plugin> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> </plugin> <plugin> <artifactId>maven-dependency-plugin</artifactId> </plugin> <plugin> <artifactId>maven-source-plugin</artifactId> </plugin> <plugin> <artifactId>maven-javadoc-plugin</artifactId> </plugin> </plugins> </build> </project>
清单 17 所示是 Java 配置文件,文件需要被放在 src/main/java/org/bytedeco/javacpp/presets 目录下面。
package org.bytedeco.javacpp.presets; import org.bytedeco.javacpp.annotation.*; import org.bytedeco.javacpp.tools.*; @Properties(target="org.bytedeco.javacpp.zlib", value={ @Platform(include="<zlib.h>", link="z@.1"), @Platform(value="windows", link="zdll", preload="zlib1")}) public class zlib implements InfoMapper { public void map(InfoMap infoMap) { infoMap.put(new Info("ZEXTERN", "ZEXPORT", "z_const", "zlib_version").cppTypes().annotations()) .put(new Info("FAR").cppText("#define FAR")) .put(new Info("OF").cppText("#define OF(args) args")) .put(new Info("Z_ARG").cppText("#define Z_ARG(args) args")) .put(new Info("Byte", "Bytef", "charf").cast().valueTypes("byte").pointerTypes("BytePointer")) .put(new Info("uInt", "uIntf").cast().valueTypes("int").pointerTypes("IntPointer")) .put(new Info("uLong", "uLongf", "z_crc_t", "z_off_t").cast().valueTypes( "long").pointerTypes("CLongPointer")) .put(new Info("z_off64_t").cast().valueTypes("long").pointerTypes("LongPointer")) .put(new Info("voidp", "voidpc", "voidpf").valueTypes("Pointer")) .put(new Info("gzFile_s").pointerTypes("gzFile")) .put(new Info("gzFile").valueTypes("gzFile")) .put(new Info("Z_LARGE64", "!defined(ZLIB_INTERNAL) && defined(Z_WANT64)").define(false)) .put(new Info("inflateGetDictionary", "gzopen_w", "gzvprintf").skip()); } }
在我们的父目录 javacpp-presets 里面,我们需要把 zlib 这个模块的名称加入到 pom.xml 的模块列表里面,这样我们就可以像前面示例代码一样运行程序来生成.so 包和 jar 包,mvn install –projects zlib。
通过上面实验的实现,我们掌握了如何使用 JavaCpp,现在我们开始尝试针对 JavaCpp 的测试。
我们这个实验基于一个人脸算法库,该人脸算法库具备检测、建模、比对功能,网上有很多开源的人脸识别算法库,大家可以自行下载。当我们使用单线程时,本地预先加载人脸特征值数据,分别使用 C++代码和 Java 调用 JNI 库的方式,在内存中循环比对 1000 万次,比对测试结果如表 2 所示。
方式 | 比对次数 (万次) | 耗时/ms | 比对速率/rps(records per second) |
---|---|---|---|
C++ | 1000 | 11055 | 904322 |
Java 调用 JNI 库 | 1000 | 14732 | 702592 |
Javacpp 调用算法库 | 1000 | 13066 | 765345 |
从上面的数据可以看出,直接用 C++调用算法库效率最高,其次是 JavaCPP 方式,JNI 方式耗时最长。当然,这里没有列举的 JNA 技术,它的效率会更差。这些效率差距主要在底层字节码的编译形式上的区别。
表 2 的方式是单线程方式,我们采用多线程方式再来做一次测试,测试结果如图 2 所示。我们可以看出,多线程环境下,C++和 JavaCPP 的优势更加明显,整体效率系统接近 0.95-1,JNI 方式的效率则平均在 0.81 左右。
我们发现,采用 JavaCPP 方式在编程上较 JNI 方式简单很多,另外,效率也比 JNI 高,所以建议多采用 JavaCPP 技术。当然,如果是开源项目,也可以通过 JavaCPP presets 子项目来分享自己做的库文件,让其他人快速使用。最后,通过一个有针对性的性能测试案例,读者也可以了解较 JNI 技术相比 JavaCPP 的优势所在。