JavaCPP 技術使用經驗總結
在 IBM Bluemix 雲平臺上開發並部署您的下一個應用。
JavaCPP 簡介
JavaCPP 是一個開源庫,它提供了在 Java 中高效訪問本地 C++的方法。採用 JNI 技術實現,所以支援所有 Java 實現包括 Android 系統,Avian 和 RoboVM。
- Android
一種基於 Linux 的自由及開放原始碼的作業系統,主要使用於移動裝置,如智慧手機和平板電腦,由 Google 公司和開放手機聯盟領導及開發。
- Avian
Avian 是一個輕量級的 Java 虛擬機器和類庫,提供了 Java 特性的一個有用的子集,適合開發跨平臺、自包容的應用程式。它實現非常快速而且體積小,主要特性包括如下四點:
- 類似於 HotSpot JVM 的 JIT 編譯器,支援快速方法執行;
- 採用 JVM 的複製演算法,即將現有的記憶體空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。這樣可以確保記憶體回收過程中記憶體暫停服務的時間較短,並且記憶體的使用空間侷限性較小;
- JVM 記憶體區域裡面的本地棧快速分配,沒有同步開銷;
- 作業系統訊號量方式解決了空指標問題,避免了不必要的分支。
- RoboVM
RoboVM 編譯器可以將 Java 位元組碼翻譯成 ARM 或者 x86 平臺上的原生程式碼,應用可直接在 CPU 上執行,無需其他直譯器或者虛擬機器。RoboVM 同時包含一個 Java 到 Objective-C 的橋,可像其他 Java 物件一樣來使用 Objective-C 物件。大多數 UIKit 已經支援,而且將會支援更多的框架。
總的來說,JavaCPP 提供了一系列的 Annotation 將 Java 程式碼對映到 C++程式碼,並使用一個可執行的 jar 包將 C++程式碼轉化為可以從 JVM 內呼叫的動態連結庫檔案。
與其他技術相比,特性總結如下表 1 所示。
表 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 示例
為了呼叫本地方法,JavaCPP 生成了對應的 JNI 程式碼,並且把這些程式碼輸入到 C++編譯器,用來構建本地庫。使用了 Annotations 特性的 Java 程式碼在執行時會自動呼叫 Loader.load() 方法從 Java 資源裡載入本地庫,這裡指的資源是工程構建過程中配置好的。
我們先來演示一個例子,這是一個簡單的注入/讀出方法,類似於 JavaBean 的工作方式。清單 1 所示的 LegacyLibrary.h 包含了 C++類。
清單 1. LegacyLibrary.h
#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++程式碼。
清單 2. LegacyLibrary.java
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 所示。
清單 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 檔案。
清單 4. 資料夾內容變化
/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-Presents 簡介
為了方便使用者使用 JavaCPP,該專案下屬有一個 presets 專案,它將一些常用的專案,例如 OpenCV、FFMpeg 等,都編譯好了讓使用者通過呼叫 Jar 包的方式直接使用。當然,它也允許使用者通過簡便的方式上傳自己做的本地庫檔案,通過將 jar 包上傳到 Maven 倉庫的方式共享給其他使用者。
如果我們想要使用 JavaCPP-presents,我們需要下載 presets 原始碼或者已經編譯好的 jar 檔案。
編譯好的 jar 檔案有很多,主要是 JavaCPP 支援的專案,如圖 1 所示。
圖 1. JavaCPP-presents binary 目錄
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 所示。
清單 5. Linux 上配置 Maven
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 所示,這樣您可以下載到所有需要的程式碼。
清單 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 包的過程輸出。
清單 7. 編譯 ffmpeg 庫
[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
JavaCPP-Presents 示例
講解程式前,我們先來了解一下 cppbuild.sh 這個檔案,這個指令碼在根目錄下面,它主要功能是被用來構建和建立本地 C++庫。例如清單 8 所示,我們編譯 ffmpeg 庫,
清單 8. 編譯 ffmpeg 庫
./cppbuild.sh -platform linux-x86_64 install ffmpeg
指令碼的第一個引數是-platform,值是 linux-x86_64 判斷程式碼如清單 9 所示,如果第一個引數是-platform,那麼初始化變數 PLATFORM,然後引數位移動一位到第二個引數,如果第二個引數是 install,那麼初始化變數 OPERATION 為 install,如果第二個引數是 clean,初始化變數 OPERATION 為 clean。這裡是 install。
清單 9. 判斷平臺
while [[ $# > 0 ]]; do case "$1" in -platform) shift PLATFORM="$1" ;; install) OPERATION=install ;; clean) OPERATION=clean ;; *) PROJECTS+=("$1") ;; esac shift done
確定了需要 install 操作後,程式進入實際執行階段,如清單 10 所示。
清單 10. install 執行階段
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 檔案。
示例程式 1:自己寫一個簡單的 FFmpeg 庫
我們這裡所舉例的例子是一個呼叫 FFmpeg 多媒體庫的示例。FFmpeg 是一套可以用來記錄、轉換數字音訊、視訊,並能將其轉化為流的開源計算機程式。採用 LGPL 或 GPL 許可證。它提供了錄製、轉換以及流化音視訊的完整解決方案。
如果想要下載 FFmpeg 原始碼或者庫檔案,可以在這裡查詢:http://ffmpeg.org/。
執行整個程式,我們需要三個檔案,大家把這三個檔案放在同一個目錄下面。
首先是 C 的原始碼,這裡只引用一小部分,剩餘的可以到 github 上看,作者是 Stephen Dranger。
清單 11. C 原始碼
// // This tutorial was written by Stephen Dranger ([email protected]). // // Code based on a tutorial by Martin Bohme ([email protected]) // 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 所示。
清單 12. pom.xml 檔案
<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 所示。
清單 13. Java 檔案原始碼
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 所示。
清單 14. 執行命令
mvn package exec:java -Dexec.mainClass=testJavaCPP -Dexec.args="您的視訊檔案"
示例程式 2:加入一個新的庫
從清單 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 所示。
清單 15. zlib 的 cppbuild.sh 檔案原始碼
#!/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 包。
清單 16. zlib 的 pom.xml 原始碼
<?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 目錄下面。
清單 17. zlib 的 Java 原始碼
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="[email protected]"), @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,現在我們開始嘗試針對 JavaCpp 的測試。
我們這個實驗基於一個人臉演算法庫,該人臉演算法庫具備檢測、建模、比對功能,網上有很多開源的人臉識別演算法庫,大家可以自行下載。當我們使用單執行緒時,本地預先載入人臉特徵值資料,分別使用 C++程式碼和 Java 呼叫 JNI 庫的方式,在記憶體中迴圈比對 1000 萬次,比對測試結果如表 2 所示。
表 2. JNI 庫和 C++庫單執行緒效能比較
方式 | 比對次數 (萬次) | 耗時/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 左右。
圖 2. JNI 庫和 C++庫多執行緒效能比較
結束語
我們發現,採用 JavaCPP 方式在程式設計上較 JNI 方式簡單很多,另外,效率也比 JNI 高,所以建議多采用 JavaCPP 技術。當然,如果是開源專案,也可以通過 JavaCPP presets 子專案來分享自己做的庫檔案,讓其他人快速使用。最後,通過一個有針對性的效能測試案例,讀者也可以瞭解較 JNI 技術相比 JavaCPP 的優勢所在。