什么是JNA
JNA (Java Native Access) 是一个Java库,它提供了一种简单的方式来访问本地共享库(native shared libraries)的API,无需编写JNI(Java Native Interface)代码。JNA使用了动态的运行时库加载和本地函数查找,使Java代码能够直接调用本地库函数。
JNA vs JNI
- JNI (Java Native Interface):是Java平台提供的标准机制,允许Java代码调用本地应用程序和库。但JNI需要编写大量的C/C++代码和特殊的Java代码。
- JNA (Java Native Access):提供了更简单的方式来访问本地库,不需要编写任何本地代码(C/C++),只需定义一个Java接口,就可以直接映射到本地库的函数。
JNA核心优势
- 简化开发:无需编写或维护JNI代码
- 减少出错可能:自动处理类型转换和内存管理
- 提高可移植性:同一套Java代码可以在不同平台上工作
- 减少开发时间:不需要编写、编译和加载本地代码
JNA核心概念
1. 映射接口
在JNA中,我们通过定义一个继承自com.sun.jna.Library的Java接口,并在接口中声明与本地库中同名的方法,来创建本地库的映射。例如:
// 定义C标准库接口
public interface CLibrary extends Library {
// 在Windows上使用msvcrt,在其他平台使用c
CLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CLibrary.class);
// 声明C标准库中的函数
void printf(String format, Object... args);
int strlen(String str);
}
测试
public static void main(String[] args) {
// 使用JNA调用C标准库函数
// TODO 输出内容存在乱码的情况,需要调整
CLibrary.INSTANCE.printf("JNA基础示例: 字符串 '%s' 的长度是 %d\n", "Hello JNA", CLibrary.INSTANCE.strlen("Hello JNA"));
System.out.println("基础JNA示例运行完成");
}
下图的程序输出存在乱码,需要设置输出的格式。
2. 数据类型映射
JNA自动处理Java和本地代码之间的数据类型转换:
| Java类型 | 本地类型 |
|---|---|
| boolean, byte, short, int, long | 相应的本地整数类型 |
| float, double | 本地浮点类型 |
| char | wchar_t (unicode) |
| String | const char* (自动转为UTF-8) |
| WString | const wchar_t* |
| byte[] | byte数组 |
| Pointer | 指针类型 |
| Structure | 结构体 |
| Callback | 函数指针 |
3. 结构体
JNA允许将Java类映射到C语言的结构体,只需继承com.sun.jna.Structure并实现getFieldOrder()方法:
public class TimeVal extends Structure {
public long tv_sec;
public long tv_usec;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("tv_sec", "tv_usec");
}
}
示例代码
// 定义一个对应C语言时间结构体的Java类
public static class TimeVal extends Structure {
public long tv_sec; // 秒
public long tv_usec; // 微秒
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("tv_sec", "tv_usec");
}
public TimeVal() {}
public TimeVal(long sec, long usec) {
this.tv_sec = sec;
this.tv_usec = usec;
}
}
// 定义包含时间相关函数的C库接口
public interface TimeLibrary extends Library {
TimeLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", TimeLibrary.class);
// gettimeofday函数在类Unix系统上可用
int gettimeofday(TimeVal tv, TimeVal tz);
// Windows平台可以使用time函数
long time(long[] time);
}
public static void main(String[] args) {
if (Platform.isWindows()) {
// Windows平台使用time函数
long[] timeByRef = new long[1];
long timeVal = TimeLibrary.INSTANCE.time(timeByRef);
System.out.println("当前时间(秒): " + timeVal);
System.out.println("通过引用获取的时间(秒): " + timeByRef[0]);
} else {
// 类Unix平台使用gettimeofday
TimeVal timeVal = new TimeVal();
TimeLibrary.INSTANCE.gettimeofday(timeVal, null);
System.out.println("当前时间: " + timeVal.tv_sec + " 秒, " + timeVal.tv_usec + " 微秒");
}
System.out.println("结构体示例运行完成");
}
在Windows上的输出内容
4. 回调函数
JNA通过实现com.sun.jna.Callback接口来支持将Java方法作为回调函数传递给本地代码:
public interface CompareCallback extends Callback {
int compare(int a, int b);
}
5. 指针和内存操作
JNA提供了Pointer类和Memory类来进行原生内存操作,可以分配、读写和释放内存。
// 分配内存
Memory mem = new Memory(1024);
// 写入数据
mem.setString(0, "Hello");
// 读取数据
String str = mem.getString(0);
其中 Memory 类可以处理内存,还可以使用C库函数来手动管理内存。
```java
// 使用C库函数手动管理
Pointer ptr = CLibrary.INSTANCE.malloc(1024);
try {
// 使用内存...
} finally {
CLibrary.INSTANCE.free(ptr); // 务必释放
}
综合案例示例代码
// 定义C标准库接口
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CLibrary.class);
// 内存分配相关函数
Pointer malloc(long size);
void free(Pointer ptr);
// 内存操作函数
void memset(Pointer ptr, int value, long size);
void memcpy(Pointer dest, Pointer src, long size);
// 字符串操作函数
Pointer strcpy(Pointer dest, String src);
String strchr(Pointer str, int c);
}
public static void main(String[] args) {
// 1. JNA直接内存分配
System.out.println("1. JNA直接内存分配");
Memory directMemory = new Memory(100);
directMemory.setString(0, "这是JNA直接分配的内存");
System.out.println("内存内容: " + directMemory.getString(0));
// 2. 使用C库的malloc分配内存
System.out.println("\n2. 使用C库的malloc分配内存");
Pointer mallocMemory = CLibrary.INSTANCE.malloc(100);
try {
CLibrary.INSTANCE.strcpy(mallocMemory, "这是通过malloc分配的内存");
System.out.println("内存内容: " + mallocMemory.getString(0));
// 使用memset填充内存
CLibrary.INSTANCE.memset(mallocMemory, 65, 10); // 用'A'(ASCII 65)填充前10个字节
System.out.println("memset后内容: " + mallocMemory.getString(0));
} finally {
// 重要:需要释放malloc分配的内存
CLibrary.INSTANCE.free(mallocMemory);
}
// 3. ByReference参数示例(模拟指针引用)
System.out.println("\n3. ByReference参数示例");
IntByReference intRef = new IntByReference(42);
System.out.println("初始值: " + intRef.getValue());
// 修改引用的值
intRef.setValue(100);
System.out.println("修改后的值: " + intRef.getValue());
// 4. 内存复制操作
System.out.println("\n4. 内存复制操作");
Memory srcMemory = new Memory(100);
Memory destMemory = new Memory(100);
srcMemory.setString(0, "这是源内存");
System.out.println("复制前目标内存: " + (destMemory.getString(0) != null ? destMemory.getString(0) : "空"));
CLibrary.INSTANCE.memcpy(destMemory, srcMemory, srcMemory.getString(0).length() + 1);
System.out.println("复制后目标内存: " + destMemory.getString(0));
// 5. 字符查找示例
System.out.println("\n5. 字符查找示例");
Memory strMemory = new Memory(100);
strMemory.setString(0, "Hello, JNA World!");
try {
String result = CLibrary.INSTANCE.strchr(strMemory, 'W');
System.out.println("查找 'W' 的结果: " + (result != null ? result : "未找到"));
} catch (Exception e) {
System.out.println("字符查找函数调用异常: " + e.getMessage());
}
System.out.println("\n内存和指针示例运行完成");
}
运行结果
JNA的局限性
- 性能开销:相比JNI,JNA调用有额外的开销
- 类型安全:类型映射错误可能不会在编译时被发现
- 平台依赖:某些平台特定的API可能需要特殊处理
- 复杂数据结构:处理复杂类型和数据结构可能较为困难
何时使用JNA
JNA适合以下场景:
-
需要访问系统级API或第三方库
-
性能要求不是特别苛刻
-
希望避免编写和维护JNI代码
-
需要跨平台调用本地库
package cn.srw;
import com.sun.jna.Native;
import com.sun.jna.Platform;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinDef.RECT;
import com.sun.jna.win32.StdCallLibrary;
/**
* Windows API示例,展示如何使用JNA调用Windows特定的API
* 注意:此示例仅适用于Windows平台
*/
public class WindowsApiExample {
// User32库接口,包含Windows GUI相关函数
public interface User32 extends StdCallLibrary {
User32 INSTANCE = Native.load("user32", User32.class);
int GetSystemMetrics(int nIndex);
// 定义用于操作窗口的函数
HWND FindWindowA(String lpClassName, String lpWindowName);
int GetWindowTextA(HWND hWnd, byte[] lpString, int nMaxCount);
boolean GetWindowRect(HWND hWnd, RECT rect);
HWND GetForegroundWindow();
int GetWindowTextLengthA(HWND hWnd);
boolean MessageBeep(int uType);
}
// 系统信息常量
private static final int SM_CXSCREEN = 0; // 主屏幕宽度
private static final int SM_CYSCREEN = 1; // 主屏幕高度
public static void main(String[] args) {
if (!Platform.isWindows()) {
System.out.println("此示例仅适用于Windows平台");
return;
}
// 1. 获取屏幕分辨率
System.out.println("1. 获取屏幕分辨率");
int screenWidth = User32.INSTANCE.GetSystemMetrics(SM_CXSCREEN);
int screenHeight = User32.INSTANCE.GetSystemMetrics(SM_CYSCREEN);
System.out.println("屏幕分辨率: " + screenWidth + " x " + screenHeight);
// 2. 获取当前活动窗口
System.out.println("\n2. 获取当前活动窗口信息");
HWND foregroundWindow = User32.INSTANCE.GetForegroundWindow();
if (foregroundWindow != null) {
// 获取窗口标题长度
int length = User32.INSTANCE.GetWindowTextLengthA(foregroundWindow);
if (length > 0) {
// 准备缓冲区并获取窗口标题
byte[] buffer = new byte[length + 1];
User32.INSTANCE.GetWindowTextA(foregroundWindow, buffer, buffer.length);
String windowTitle = Native.toString(buffer);
System.out.println("当前活动窗口标题: " + windowTitle);
// 获取窗口位置和大小
RECT rect = new RECT();
User32.INSTANCE.GetWindowRect(foregroundWindow, rect);
System.out.println("窗口位置: 左=" + rect.left + ", 上=" + rect.top +
", 右=" + rect.right + ", 下=" + rect.bottom);
System.out.println("窗口尺寸: 宽=" + (rect.right - rect.left) +
", 高=" + (rect.bottom - rect.top));
} else {
System.out.println("无法获取窗口标题");
}
} else {
System.out.println("无法获取当前活动窗口");
}
// 3. 查找特定窗口
System.out.println("\n3. 查找Edge浏览器");
HWND edgeWindow = User32.INSTANCE.FindWindowA(null, "百度"); // 需要是网页的完整title
if (edgeWindow != null) {
System.out.println("找到Edge浏览器窗口");
} else {
System.out.println("未找到Edge浏览器窗口 (请确保Edge正在运行)");
}
// 4. 播放系统声音
System.out.println("\n4. 播放系统声音");
boolean beepResult = User32.INSTANCE.MessageBeep(0);
System.out.println("系统声音播放" + (beepResult ? "成功" : "失败"));
System.out.println("\nWindows API示例运行完成");
}
}
运行结果
声明:封面图为作者微风的图,来源网站 Unsplash