JNA(Java Native Access)基础入门

990 阅读6分钟

什么是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核心优势

  1. 简化开发:无需编写或维护JNI代码
  2. 减少出错可能:自动处理类型转换和内存管理
  3. 提高可移植性:同一套Java代码可以在不同平台上工作
  4. 减少开发时间:不需要编写、编译和加载本地代码

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本地浮点类型
charwchar_t (unicode)
Stringconst char* (自动转为UTF-8)
WStringconst 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的局限性

  1. 性能开销:相比JNI,JNA调用有额外的开销
  2. 类型安全:类型映射错误可能不会在编译时被发现
  3. 平台依赖:某些平台特定的API可能需要特殊处理
  4. 复杂数据结构:处理复杂类型和数据结构可能较为困难

何时使用JNA

JNA适合以下场景:

  1. 需要访问系统级API或第三方库

  2. 性能要求不是特别苛刻

  3. 希望避免编写和维护JNI代码

  4. 需要跨平台调用本地库

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