先說JNI(Java Native Interface)吧,有過不同語言間通信經(jīng)歷的一般都知道,它允許Java代碼和其他語言(尤其C/C++)寫的代碼進行交互,只要遵守調(diào)用約定即可。首先看下JNI調(diào)用C/C++的過程,注意寫程序時自下而上,調(diào)用時自上而下。
可 見步驟非常的多,很麻煩,使用JNI調(diào)用.dll/.so共享庫都能體會到這個痛苦的過程。如果已有一個編譯好的.dll/.so文件,如果使用JNI技 術(shù)調(diào)用,我們首先需要使用C語言另外寫一個.dll/.so共享庫,使用SUN規(guī)定的數(shù)據(jù)結(jié)構(gòu)替代C語言的數(shù)據(jù)結(jié)構(gòu),調(diào)用已有的 dll/so中公布的函 數(shù)。然后再在Java中載入這個庫dll/so,最后編寫Java native函數(shù)作為鏈接庫中函數(shù)的代理。經(jīng)過這些繁瑣的步驟才能在Java中調(diào)用 本地代碼。因此,很少有Java程序員愿意編寫調(diào)用dll/.so庫中原生函數(shù)的java程序。這也使Java語言在客戶端上乏善可陳,可以說JNI是 Java的一大弱點!
那么JNA是什么呢?
JNA(Java Native Access)是一個開源的Java框架,是Sun公司推出的一種調(diào)用本地方法的技術(shù),是建立在經(jīng)典的JNI基礎(chǔ)之上的一個框架。之所以說它是JNI的替 代者,是因為JNA大大簡化了調(diào)用本地方法的過程,使用很方便,基本上不需要脫離Java環(huán)境就可以完成。
如果要和上圖做個比較,那么JNA調(diào)用C/C++的過程大致如下:
可以看到步驟減少了很多,最重要的是我們不需要重寫我們的動態(tài)鏈接庫文件,而是有直接調(diào)用的API,大大簡化了我們的工作量。
JNA只需要我們寫Java代碼而不用寫JNI或本地代碼。功能相對于Windows的Platform/Invoke和Python的ctypes。
JNA使用一個小型的JNI庫插樁程序來動態(tài)調(diào)用本地代碼。開發(fā)者使用Java接口描述目標(biāo)本地庫的功能和結(jié)構(gòu),這使得它很容易利用本機平臺的功能,而不會產(chǎn)生多平臺配置和生成JNI代碼的高開銷。這樣的性能、準(zhǔn)確性和易用性顯然受到很大的重視。
此外,JNA包括一個已與許多本地函數(shù)映射的平臺庫,以及一組簡化本地訪問的公用接口。
注意:
JNA是建立在JNI技術(shù)基礎(chǔ)之上的一個Java類庫,它使您可以方便地使用java直接訪問動態(tài)鏈接庫中的函數(shù)。
原來使用JNI,你必須手工用C寫一個動態(tài)鏈接庫,在C語言中映射Java的數(shù)據(jù)類型。
JNA中,它提供了一個動態(tài)的C語言編寫的轉(zhuǎn)發(fā)器,可以自動實現(xiàn)Java和C的數(shù)據(jù)類型映射,你不再需要編寫C動態(tài)鏈接庫。
也許這也意味著,使用JNA技術(shù)比使用JNI技術(shù)調(diào)用動態(tài)鏈接庫會有些微的性能損失。但總體影響不大,因為JNA也避免了JNI的一些平臺配置的開銷。
JNA的項目已遷移至Github,目前最新版本是4.1.0,已有打包好的jar文件可供下載。
JNA把一個.dll/.so文件看做是一個Java接口,下面以一個簡單的實例來說明怎么使用。
當(dāng)然要從最經(jīng)典的HelloWorld開始,我們調(diào)用C的printf函數(shù)打印出“HelloWorld”(官方的例子),前提是已將jar包加入你的classpath。
package com.sun.jna.examples;import com.sun.jna.Library;import com.sun.jna.Native;import com.sun.jna.Platform;/** Simple example of JNA interface mapping and usage. */public class HelloWorld { // This is the standard, stable way of mapping, which supports extensive // customization and mapping of Java to native types. public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary) Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class); void printf(String format, Object... args); } public static void main(String[] args) { CLibrary.INSTANCE.printf("Hello, World\n"); for (int i=0;i < args.length;i++) { CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]); } }}
運行程序,如果沒有帶參數(shù)則只打印出“Hello, World”,如果帶了參數(shù),則會打印出所有的參數(shù)。
很簡單,不需要寫一行C代碼,就可以直接在Java中調(diào)用外部動態(tài)鏈接庫中的函數(shù)!
下面來解釋下這個程序。
Library
或StdCallLibrary
默認(rèn)的是繼承
Library
,如果動態(tài)鏈接庫里的函數(shù)是以stdcall方式輸出的,那么就繼承StdCallLibrary
,比如眾所周知的kernel32庫。比如上例中的接口定義:
public interface CLibrary extends Library {}
接口內(nèi)部需要一個公共靜態(tài)常量:INSTANCE,
通過這個常量,就可以獲得這個接口的實例,從而使用接口的方法,也就是調(diào)用外部dll/so的函數(shù)。
該常量通過Native.loadLibrary()這個API函數(shù)獲得,該函數(shù)有2個參數(shù):
CLibrary INSTANCE = (CLibrary) Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class);
接口中只需要定義你要用到的函數(shù)或者公共變量,不需要的可以不定義,如上例只定義printf函數(shù):
void printf(String format, Object... args);
注意參數(shù)和返回值的類型,應(yīng)該和鏈接庫中的函數(shù)類型保持一致。
定義好接口后,就可以使用接口中的函數(shù)即相應(yīng)dll/so中的函數(shù)了,前面說過調(diào)用方法就是通過接口中的實例進行調(diào)用,非常簡單,如上例中:
CLibrary.INSTANCE.printf("Hello, World\n"); for (int i=0;i < args.length;i++) { CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]); }
這就是JNA使用的簡單例子,可能有人認(rèn)為這個例子太簡單了,因為使用的是系統(tǒng)自帶的動態(tài)鏈接庫,應(yīng)該還給出一個自己實現(xiàn)的庫函數(shù)例子。其實我覺得這個完全沒有必要,這也是JNA的方便之處,不像JNI使用用戶自定義庫時還得定義一大堆配置信息,對于JNA來說,使用用戶自定義庫與使用系統(tǒng)自帶的庫是完全一樣的方法,不需要額外配置什么信息。比如我在Windows下建立一個動態(tài)庫程序:
#include "stdafx.h"extern "C"_declspec(dllexport) int add(int a, int b);int add(int a, int b) { return a + b;}
然后編譯成一個dll文件(比如CDLL.dll),放到當(dāng)前目錄下,然后編寫JNA程序調(diào)用即可:
public class DllTest { public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary)Native.loadLibrary("CDLL", CLibrary.class); int add(int a, int b); } public static void main(String[] args) { int sum = CLibrary.INSTANCE.add(3, 6); System.out.println(sum); }}
有過跨語言、跨平臺開發(fā)的程序員都知道,跨平臺、語言調(diào)用的難點,就是不同語言之間數(shù)據(jù)類型不一致造成的問題。絕大部分跨平臺調(diào)用的失敗,都是這個問題造成的。關(guān)于這一點,不論何種語言,何種技術(shù)方案,都無法解決這個問題。JNA也不例外。
上面說到接口中使用的函數(shù)必須與鏈接庫中的函數(shù)原型保持一致,這是JNA甚至所有跨平臺調(diào)用的難點,因為C/C++的類型與Java的類型是不一樣的,你必須轉(zhuǎn)換類型讓它們保持一致,比如printf函數(shù)在C中的原型為:
void printf(const char *format, [argument]);
你不可能在Java中也這么寫,Java中是沒有char *指針類型的,因此const char *轉(zhuǎn)到Java下就是String類型了。
這就是類型映射(Type Mappings),JNA官方給出的默認(rèn)類型映射表如下:
還有很多其它的類型映射,需要的請到JNA官網(wǎng)查看。
另外,JNA還支持類型映射定制,比如有的Java中可能找不到對應(yīng)的類型(在Windows API中可能會有很多類型,在Java中找不到其對應(yīng)的類型)
,JNA中TypeMapper類和相關(guān)的接口就提供了這樣的功能。
這可能是大家比較關(guān)心的問題,但是遺憾的是,JNA是不能完全替代JNI的,因為有些需求還是必須求助于JNI。
使用JNI技術(shù),不僅可以實現(xiàn)Java訪問C函數(shù),也可以實現(xiàn)C語言調(diào)用Java代碼。
而JNA只能實現(xiàn)Java訪問C函數(shù),作為一個Java框架,自然不能實現(xiàn)C語言調(diào)用Java代碼。此時,你還是需要使用JNI技術(shù)。
JNI是JNA的基礎(chǔ),是Java和C互操作的技術(shù)基礎(chǔ)。有時候,你必須回歸到基礎(chǔ)上來。
(2)C++DLL編程詳解