文章目录
JNA 调用 Windows API 真的非常简单
环境
<!-- Java Native Access -->
<!-- https://github.com/java-native-access/jna -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.11.0</version>
</dependency>
<!-- jna-platform 引用了 jna, 所以引入该包即可 -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.11.0</version>
</dependency>
jna 包是核心库, jna-platform 是对 Win32 常用 api 的大量封装, 我们常用的 api 在 jna-platform 里基本上都能找到, 一般引入该包即可
使用
- JNA: This is the core artifact of JNA and contains only the binding library and the core helper classes.
- JNA Platform: This artifact holds cross-platform mappings and mappings for a number of commonly used platform functions, including a large number of Win32 mappings as well as a set of utility classes that simplify native access. The code is tested and the utility interfaces ensure that native memory management is taken care of correctly.
通过阅读 GettingStarted.md
了解了 JNA 大概的用法
- 定义一个继承自
com.sun.jna.Library
的接口(如 MyInterface) - 在该接口里定义一个自身类型的属性(如 INSTANCE), 通过 MyInterface INSTANCE =
Native.load("库名字", MyInterface.class)
的方式赋值 - 在该接口里定义要使用的库的方法函数(如 foo()), 注意参数对应
- 在其他地方通过
MyInterface.INSTANCE.foo();
来调用对应方法
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.load((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]);
}
}
}
通过阅读 PlatformLibrary.md
了解了 JNA Platform 大概的用法, 其实就是对上面方法的封装, 需要掌握如下印射规则
- 数据结构印射在 header 中, 如
ShlObj.h
, 在 jna 的com.sun.jna.platform.win32.ShlObj
里可以找到对应的数据结构和常量等 - 函数方法印射在 library 中, 如
Advapi32.dll
, 在 jna 的com.sun.jna.platform.win32.Advapi32
里可以找到对应的函数方法 - 针对
Advapi32.dll
里的方法做的一些简单组合封装则在com.sun.jna.platform.win32.Advapi32Util
里
Demo
JNA Platform Demo, CreateToolhelp32Snapshot function (tlhelp32.h)
通过 Microsoft 文档, 可以查到函数的具体信息, 使用方法, 代码样例, header / library / dll 等信息
我想实现一个功能, 通过进程名获取进程ID, 其 c++ 代码如下
#include "iostream"
#include "windows.h"
#include "tlhelp32.h"
#include "toolkit.h"
using namespace std;
DWORD GetProcessIDByProcessName(const char *processName) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == hSnap) {
return 0;
}
PROCESSENTRY32 pe = {sizeof(PROCESSENTRY32)};
for (BOOL ret = Process32First(hSnap, &pe); ret; ret = Process32Next(hSnap, &pe)) {
if (strcmp(pe.szExeFile, processName) == 0) {
CloseHandle(hSnap);
return pe.th32ProcessID;
}
// cout << pe.th32ProcessID << "\t\t" << pe.szExeFile << endl;
}
CloseHandle(hSnap);
return 0;
}
我们开始使用 JNA 复写该功能
通过微软文档可知, CreateToolhelp32Snapshot
函数的相关的 header 和 dll 分别是 tlhelp32.h
和 Kernel32.dll
, 首先全局搜索 Kernel32
和 Tlhelp32
, 发现 JNA Platform 已经封装过了, 我们直接使用即可
// HANDLE CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
public static int getProcessIdByProcessName(String processName) {
Kernel32.INSTANCE.CreateToolhelp32Snapshot();
}
我们发现, 该方法的两个参数都是 DWORD 类型, 即 com.sun.jna.platform.win32.WinDef.DWORD, 是一个内部类, 具体的值存在其父类 IntegerType 的 long value 属性中, 可以认为这里的 DWORD 就是一个 long
然后我们找 TH32CS_SNAPPROCESS 这个定义, 根据文档猜测应该是定义在 tlhelp32.h 中, 我们试着找一下, 真有
// com.sun.jna.platform.win32.Tlhelp32
WinDef.DWORD TH32CS_SNAPPROCESS = new WinDef.DWORD(0x00000002);
然后写功能, 发现其返回值类型是 com.sun.jna.platform.win32.WinNT.HANDLE, 全局搜索 INVALID_HANDLE_VALUE, 找到了
// com.sun.jna.platform.win32.WinBase
HANDLE INVALID_HANDLE_VALUE = new HANDLE(Pointer.createConstant(Native.POINTER_SIZE == 8 ? -1 : 0xFFFFFFFFL));
public static int getProcessIdByProcessName(String processName) {
WinNT.HANDLE handle = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0));
if (INVALID_HANDLE_VALUE == handle) {
return 0;
}
return 0;
}
两个 HANDLE 对象, 不是简单数据类型, 不能通过 == 来判断是否相同, 全局搜索发现好多 Utils 里面有如下的用法
HANDLE h = Kernel32.INSTANCE.FindFirstVolume(volumeUUID, 50);
if (h == null || h.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new Win32Exception(Native.getLastError());
}
我们也学一学
public static int getProcessIdByProcessName(String processName) {
WinNT.HANDLE handle = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0));
if (null == handle || handle.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
return 0;
}
继续, 发现 c/c++ 里有使用 sizeof 函数, java 里该怎么做呢, 经过一番百度和全局搜索, 发现在 Kernel32Util 里有 CreateToolhelp32Snapshot 函数的封装, 是用来获取进程中加载的模块的, 正好, 我们来参考下
// com.sun.jna.platform.win32.Kernel32Util#getModules
public static List<Tlhelp32.MODULEENTRY32W> getModules(int processID) {
HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPMODULE, new DWORD(processID));
if (snapshot == null) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
Win32Exception we = null;
try {
Tlhelp32.MODULEENTRY32W first = new Tlhelp32.MODULEENTRY32W();
if (!Kernel32.INSTANCE.Module32FirstW(snapshot, first)) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
List<Tlhelp32.MODULEENTRY32W> modules = new ArrayList<Tlhelp32.MODULEENTRY32W>();
modules.add(first);
Tlhelp32.MODULEENTRY32W next = new Tlhelp32.MODULEENTRY32W();
while (Kernel32.INSTANCE.Module32NextW(snapshot, next)) {
modules.add(next);
next = new Tlhelp32.MODULEENTRY32W();
}
int lastError = Kernel32.INSTANCE.GetLastError();
// if we got a false from Module32Next,
// check to see if it returned false because we're genuinely done
// or if something went wrong.
if (lastError != W32Errors.ERROR_SUCCESS && lastError != W32Errors.ERROR_NO_MORE_FILES) {
throw new Win32Exception(lastError);
}
return modules;
} catch (Win32Exception e) {
we = e;
throw we; // re-throw so finally block is executed
} finally {
try {
closeHandle(snapshot);
} catch(Win32Exception e) {
if (we == null) {
we = e;
} else {
we.addSuppressedReflected(e);
}
}
if (we != null) {
throw we;
}
}
}
修改我们的代码并做优化如下
public static int getProcessIdByProcessName(String processName) {
WinNT.HANDLE handle = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0));
if (null == handle || handle.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
Tlhelp32.PROCESSENTRY32 pe = new Tlhelp32.PROCESSENTRY32();
try {
for (boolean success = Kernel32.INSTANCE.Process32First(handle, pe); success; success = Kernel32.INSTANCE.Process32Next(handle, pe)) {
if (processName.equals(pe.szExeFile)) {
Kernel32.INSTANCE.CloseHandle(handle);
return pe.th32ProcessID.intValue();
}
}
} catch (Throwable cause) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
Kernel32.INSTANCE.CloseHandle(handle);
throw new RuntimeException("Process Not Exist");
}
发现 pe.szExeFile 不是 String, 而是 char[], 所以做一下转换 processName.equals(new String(pe.szExeFile))
, 尝试运行发现全报 进程不存在, 不科学, 打印进程名字, 发现转换后的 String 有问题, 系统进程名字已经拿到了, 只是显示有问题, 再查代码发现, pe.szExeFile 的定义居然是长度 260, 怪不得, 看来还得处理下
// com.sun.jna.platform.win32.Tlhelp32.PROCESSENTRY32#szExeFile
public char[] szExeFile = new char[WinDef.MAX_PATH];
// com.sun.jna.platform.win32.WinDef#MAX_PATH
int MAX_PATH = 260;
最终的方法如下, 找到 pe.szExeFile 中的第一个 0, 然后使用 public String(char value[], int offset, int count)
截取部分并转换
public static int getProcessIdByProcessName(String processName) {
WinNT.HANDLE handle = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0));
if (null == handle || handle.equals(WinBase.INVALID_HANDLE_VALUE)) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
Tlhelp32.PROCESSENTRY32 pe = new Tlhelp32.PROCESSENTRY32();
try {
for (boolean success = Kernel32.INSTANCE.Process32First(handle, pe); success; success = Kernel32.INSTANCE.Process32Next(handle, pe)) {
int index = pe.szExeFile.length - 1;
for (int i = 0; i < pe.szExeFile.length; i++) {
char item = pe.szExeFile[i];
if (item == 0) {
index = i;
break;
}
}
if (processName.equals(new String(pe.szExeFile, 0, index))) {
Kernel32.INSTANCE.CloseHandle(handle);
return pe.th32ProcessID.intValue();
}
}
} catch (Throwable cause) {
throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
}
Kernel32.INSTANCE.CloseHandle(handle);
throw new RuntimeException("Process Not Exist");
}
通过测试, 完全正确, 但是如果程序多开的话, 只能取到遍历时第一个进程的进程ID
JNA Demo, PrivateExtractIconsW function (winuser.h)
通过 Java 获取 exe 等文件内包含的高清 Icon 图片, 因为网上的两种方法, 拿到的 Icon 只是 1616 和 3232 的, 而现在的 Icon 一般都是很高清的, 有 6464, 96/96, 128128, 256*256 等尺寸, 这些高清图片不太好拿到
但是发现网上有一些工具是可以拿到高清大图的, 比如 IconViewer (安装后, 右键文件, 属性, Icons 面板里有全部尺寸的图片)
所以一定有办法拿到全尺寸图片的, 一番研究发现可能关键在于 PrivateExtractIconsW 这个函数, 不确定的参数可用 Object 代替
interface User32X extends Library {
User32X INSTANCE = Native.load("user32", User32X.class, W32APIOptions.DEFAULT_OPTIONS);
int PrivateExtractIconsW(String szFileName, int nIconIndex, int cxIcon, int cyIcon, Integer phicon, Object piconid, int nIcons, int flags);
}
public static void getAllSizeImageFromIconInAnyFile() {
int size = User32X.INSTANCE.PrivateExtractIconsW("D:\\game\\虎牙直播\\HuyaClient\\Huya.exe", 0, 0, 0, null, null, 0, 0);
System.out.println(size);
}
效果是可以的, 能用, 且结果正确. 但是后续 c++ 解析 HICON 获取各尺寸图片的代码, 实在是模仿不来, 就先这样吧. 反正 JNA 调用的方法是明白了