Hefery 的个人网站

Hefery's Personal Website

Contact:hefery@126.com
  menu
73 文章
0 浏览
1 当前访客
ღゝ◡╹)ノ❤️

Java 使用 JNA 调用 DLL 动态库的函数操作文件

需求

设备会产生一个 cex 后缀的文件,cex 文件经过特殊的加密处理,不能像之前的 txt、dat、xml、json 文件直接解析的方式去获取数据,必须通过设备供应商提供的函数去获取 cex 文件的箱号、通道号、下位机、充放电容量等数据。

而且,设备产生的 cex 文件是增量文件(设备更新数据只是在原有 cex 文件基础上追加数据,跟 MySQL 的写日志那种类似)

设备供应商提供了《数据接口Dll使用说明》、LANDCex_x64.dll、LANHE.sys、CexConst.h 等核心文件,还提供了 VS2015 和 ExcelVBA 使用接口的案例

过程

之前本来不是这么麻烦的,只是做一个接口,去接收设备供应商传过来的 json 数据。但是,接口开发完了,才知道要收费,为了省钱,只能让我去“卖艺”😭 😭 😭

先理一下整体的思考方向

  1. 获取 cex 文件中之前需要传递 json 数据,包括箱号、通道号、下位机、充放电容量等
  2. 改造下之前已经写好但是废弃的接口(毕竟逻辑还在那里可以用)
  3. ……

仔细阅读设备供应商提供的《数据接口Dll使用说明》

首先是数据:了解这种数据的逻辑结构,对于准确理解和使用输出函数非常重要

测试数据在逻辑上可以理解为由“t_cycle”、“t_step”和“t_rec”三张数据表相互交错而行成的一个复合数据表。一个“cycle”包含多个“step步”,而一个“工step”又包含多个“rec”

所以要定位数据,需要确定的是:数据在哪张表、行号、列号。设备供应商为了简化接口,将三张表所有数据的列进行了统一的编号 columnID 来唯一标识,这样定位数据就不需要确定是哪张表了,这个 columnID 记录在 CexConst.h 头文件

然后就是一堆的函数接口及使用说明,并且给出了基本的调用流程:

  1. 检查 dll 版本:CheckDllVersion()
  2. 加载 cex 数据到内存中:LoadData()
  3. 操作数据:GetRows()GetDataEx()GetDataAsFloat()
  4. 是否数据:FreeData()

那么,Java 怎么调用 dll 的函数呢?简单百度了解到,主要有 JNative、JNI、JNA 等

最后选用好用的 JNA:

  1. 导入 JNA 依赖

    <!-- JNA -->
    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna</artifactId>
        <version>5.0.9</version>
    </dependency>
    
  2. 创建 dll 接口

    public interface DllUtil extends Library {
    
        DllUtil instance = (DllUtil) Native.loadLibrary("Cex_x64", DllUtil.class);
    
        /**
         * 获取 DLL 的接口版本号
         * @return
         */
        Long GetDllVersion();
    
    }
    
  3. 调用 dll 接口

    DllUtil.instance.GetDllVersion()
    

探知

  • dll:Dynamic Link Library,动态链接库,windows 中许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,放置于系统。当我们执行某一个程序时,相应的 dll 文件就会被调用
  • JNA:(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(Native Library:Dll)而不需要编写任何 Native/JNI 代码。我们只要在一个 Java 接口中描述目标 Native Library 的函数与结构,JNA 将自动实现 Java 接口到 Native Function 的映射。JNA 建立在 JNI 的基础之上的一个框架,比 JNI 更好用

参考博文:

实践

前期准备

  1. 引入依赖

    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna</artifactId>
        <version>5.9.0<ersion>
    </dependency>
    
  2. 存放 Dll

    参考很多资料,有的说是放项目路径下,有的说放 JDK 的 bin 目录中,其实都可以,参考 Native.loadLibrary 源码其实这些路径都是会去找的,这里把 Dll 存放在项目 resources 路径,使用 Native.loadLibrary 方式加载(要确认 Dll 在 64 位还是 32 位机使用,并选择对应版本)

添加 dll 后要重启系统,我之前没有重启调用 load 方法一直 load 不了,后来看了哪篇博文说是要重启下才能加载 dll

参考 Native.loadLibrary 源码,会去获取 dll 接口类的类加载器,然后通过类加载器和获取的 handler 生成一个 Library 的代理对象,最后返回这个代理对象

public final class Native {
    public static Object loadLibrary(String name, Class interfaceClass, Map options) {
        Handler handler = new Handler(name, interfaceClass, options);
        ClassLoader loader = interfaceClass.getClassLoader();
        Library proxy = (Library)Proxy.newProxyInstance(loader, new Class[]{interfaceClass}, handler);
        cacheOptions(interfaceClass, options, proxy);
        return proxy;
    }
}

如使用的那样 Native.loadLibrary("Cex_x64", DllUtil.class) ,第一个参数传的是 dll 不带后缀的名称,即参数 name ,这个 name 在 loadLibrary 中用作 new Handler,看到 new Handler 的部分,主要追踪参数 libname,先是对 libname 做了合法校验,经过处理填充 options,通过 NativeLibrary.getInstance 得到本地 Library 的实例

public final class Native {
    public static class Handler implements InvocationHandler {
        public Handler(String libname, Class interfaceClass, Map options) {
            if (libname != null && "".equals(libname.trim())) {
                throw new IllegalArgumentException("Invalid library name \"" + libname + "\"");
            } else {
                this.interfaceClass = interfaceClass;
                Map options = new HashMap(options);
                int callingConvention = AltCallingConvention.class.isAssignableFrom(interfaceClass)?1:0;
                if (options.get("calling-convention") == null) {
                    options.put("calling-convention", new Integer(callingConvention));
                }

                this.options = options;
                this.nativeLibrary = NativeLibrary.getInstance(libname, options);
                this.functionMapper = (FunctionMapper)options.get("function-mapper");
                if (this.functionMapper == null) {
                    this.functionMapper = new Library.Handler.FunctionNameMap(options);
                }

                this.invocationMapper = (InvocationMapper)options.get("invocation-mapper");
            }
        }
    }
}

A.isAssignableFrom(B) 确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。主要,这里比较的维度不是实例对象,而是类本身

其实再追踪 NativeLibrary.getInstance会发现,先会根据 libraries 得到一个弱引用 WeakReference 的 ref 尝试去获取 library,如果是首次加载的话,会走到 library == null 的 if 里面,这里就是为 libraries 填充信息,看到libraries.put(file.getAbsolutePath() + options, ref);往 libraries 填充library.getFile()的绝对路径。但是看到这个也不太对啊,我本来是要找 dll 的路径,系统如果有多个同名的 dll ……

public class NativeLibrary {
    private static final Map libraries = new HashMap();

    public static final NativeLibrary getInstance(String libraryName, Map options) {
        Map options = new HashMap(options);
        if (options.get("calling-convention") == null) {
            options.put("calling-convention", new Integer(0));
        }

        if (Platform.isLinux() && "c".equals(libraryName)) {
            libraryName = null;
        }

        synchronized(libraries) {
            WeakReference ref = (WeakReference)libraries.get(libraryName + options);
            NativeLibrary library = ref != null ? (NativeLibrary)ref.get() : null;
            if (library == null) {
                if (libraryName == null) {
                    library = new NativeLibrary("<process>", (String)null, Native.open((String)null), options);
                } else {
                    library = loadLibrary(libraryName, options);
                }

                ref = new WeakReference(library);
                libraries.put(library.getName() + options, ref);
                File file = library.getFile();
                if (file != null) {
                    libraries.put(file.getAbsolutePath() + options, ref);
                    libraries.put(file.getName() + options, ref);
                }
            }

            return library;
        }
    }
}

所以,我肯定是看漏了那里,慢慢往上找,回到 library == null 的 if 里面,还要判断 libraryName == null,libraryName,libraryName 是我们传进来的 dll 名称,肯定不是 null,所以会调用 loadLibrary(libraryName, options) 补充 library

跳转到 NativeLibrary.loadLibrary 方法,看到很多路径,我就感觉有戏

  1. 先设置了 searchPath 作为搜索到的路径

  2. 搜索可能的路径

    1. 是不是 web 环境:Native.getWebStartLibraryPath(libraryName)
    2. customPaths 这里没看懂啥意思
    3. 将 "jna.library.path" 所有路径加载到 searchPath,去找有无 libraryName 的路径
  3. 查找 searchPath 中 libraryName 的路径

    String libraryPath = findLibraryPath(libraryName, searchPath);
    
  4. 尝试各个系统下这个 libraryPath 是否存在

注意:这里的 handle 在 C++ 函数转 Java 接口有用

long handle = 0L;
try {
	handle = Native.open(libraryPath);
} catch (UnsatisfiedLinkError var13) {
        searchPath.addAll(librarySearchPath);
}
public class NativeLibrary {
    private static NativeLibrary loadLibrary(String libraryName, Map options) {
        List searchPath = new LinkedList();
        String webstartPath = Native.getWebStartLibraryPath(libraryName);
        if (webstartPath != null) {
            searchPath.add(webstartPath);
        }

        List customPaths = (List)searchPaths.get(libraryName);
        if (customPaths != null) {
            synchronized(customPaths) {
                searchPath.addAll(0, customPaths);
            }
        }

        searchPath.addAll(initPaths("jna.library.path"));
        String libraryPath = findLibraryPath(libraryName, searchPath);
        long handle = 0L;

        try {
            handle = Native.open(libraryPath);
        } catch (UnsatisfiedLinkError var13) {
            searchPath.addAll(librarySearchPath);
        }

        try {
            if (handle == 0L) {
                libraryPath = findLibraryPath(libraryName, searchPath);
                handle = Native.open(libraryPath);
                if (handle == 0L) {
                    throw new UnsatisfiedLinkError("Failed to load library '" + libraryName + "'");
                }
            }
        } catch (UnsatisfiedLinkError var15) {
            UnsatisfiedLinkError e = var15;
            if (Platform.isLinux()) {
                libraryPath = matchLibrary(libraryName, searchPath);
                if (libraryPath != null) {
                    try {
                        handle = Native.open(libraryPath);
                    } catch (UnsatisfiedLinkError var12) {
                        e = var12;
                    }
                }
            } else if (Platform.isMac() && !libraryName.endsWith(".dylib")) {
                libraryPath = "/System/Library/Frameworks/" + libraryName + ".framework/" + libraryName;
                if ((new File(libraryPath)).exists()) {
                    try {
                        handle = Native.open(libraryPath);
                    } catch (UnsatisfiedLinkError var11) {
                        e = var11;
                    }
                }
            } else if (Platform.isWindows()) {
                libraryPath = findLibraryPath("lib" + libraryName, searchPath);

                try {
                    handle = Native.open(libraryPath);
                } catch (UnsatisfiedLinkError var10) {
                    e = var10;
                }
            }

            if (handle == 0L) {
                throw new UnsatisfiedLinkError("Unable to load library '" + libraryName + "': " + e.getMessage());
            }
        }

        return new NativeLibrary(libraryName, libraryPath, handle, options);
    }
}

创建 Java 接口

其实这些个接口就是 native 修饰的接口,想想也是,我现在做的事不就是 Java 调用 C++ 的函数嘛,相比于 JNA 来说 JNI 更适合说明这个问题。

native:

  • 一个 native 方法就是一个 Java 调用非 Java 代码的接口。即该方法实现非 Java,比如用 C 或 C++
  • 定义一个 native 方法时,并不提供具体实现,因为本来就由 C 或 C++ 实现,Java 只是“声明”

通过 JNI,我们就可以通过 Java 调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能,实现不同编程语言的相互调用

在 windows 系统上,可执行的应用程序是基于 native 的 PE 结构,JVM 也是基于 native 结构实现。Java 应用体系构建于 JVM 之上

这里也引发了相关的思考:Java 跨平台主要依靠 JVM,那么使用 JNI 不就使得程序破坏了跨平台的特性了吗?

接口名称

接口的方法名称与 Dll 提供函数名称一致,包括大小写:C/C++ 提供什么就是什么,如,Dll 提供 CheckDllVersion() 函数,那么在接口中也必须声明的是 CheckDllVersion()

接口参数

参数的名称无所谓的,主要是返回参数很折磨人。

Java 是值传递的,没有指针的概念,但是 C/C++ 有指针,如,bool GetDescriptionOfDownId(VARIANT& vDesc, DWORD dwDownId, bool bDotSet=true),函数作用是通过 dwDownId 返回描述信息 vDesc。这里就是用到的指针,好在 JNA 中引入了 Pointer 类,用来表示 native 方法中的指针。

当然 C/C++ 中对结构体使用得很是频繁,JNA 中也引入了 Structure 类与之对应。C++ 中的结构体类似于 Java 中的实体类,但在 JNA 中 Java 实体类的每个属性都要与 C++ 结构体的每一个字段对应

// C++ 结构体
typedef struct _testStruct {
      char   id[18];                       
      char   name[48];            
      int    intValue;                  
      float  floatValue;
      long   longValue;
} testStruct_t;

// Java 类
public class TestStruct extends Structure {
	public byte[] id = new byte[18];   // 长度也是需要对应的,否则数据会发生错乱
	public byte[] name = new byte[48];
	public int intValue;
	public float floatValue;
	public NativeLong longValue;

	@Override
	protected List<String> getFieldOrder() {
		return Arrays.asList(new String[] {
			"id", "name", "intValue", "floatValue", "longValue"
		});
	}

    public static class ByReference extends TestStruct implements Structure.ByReference {}
    public static class ByValue extends TestStruct implements Structure.ByValue {}
}

数据类型

  • 基本数据类型

  • 指针类型

    用于返回数据的参数不能使用基本数据类型,而是要用到 com.sun.jna.ptr 包下的 ByReference 的具体实现

    • ByteByReference
    • IntByReference
    • LongByReference
    • DoubleByReference
    • FloatByReference、
    • ativeLongByReference
    • ShortByReference
    • PointerByReference

    JNA 中用 Point 映射 C++ 指针,一般情况下一级指针用 Point(msg.getInt(0),0表示偏移量从0开始),二级指针用 PointerByRefrence 比较方便, Pointer 也可以用二级指针(msg.getPointer(0).getString(0),表示指针的指针再取值;多级指针以此类推)

    C++ :void GetListTest(char**name, int* number, long* ier)
    Java:void GetListTest(PointerByReference name, IntByReference number, LongByReference ier);
    

    char** 是二级指针,JNA 中用 PointerByReference 映射;int* 是一级指针,JNA 中用 IntByReference,如果这个一级指针数据类型是 long,则用 PointerByReference

接口示例

public interface DllUtil extends Library {
    /** 加载 Dll */
	DllUtil instance = (DllUtil) Native.loadLibrary("DLL_NAME", DllUtil.class);

    /** 获取 DLL 的接口版本号 */
    // DWORD __stdcall GetDllVersion (void)
    Long GetDllVersion();
  
    /** 验证 DLL 的接口版本的兼容性 */
    // bool __stdcall CheckDllVersion (DWORD dwVerRequested)
    Boolean CheckDllVersion(Long dwVerRequested);
  
    /** 将测试数据文件的详细数据调入内存 */
    // HANDLE __stdcall LoadData (const char*pszPath, int nHopeUnitScheme=USCH_BaseOn_Auto)
    Long LoadData(String pszPath, Long nHopeUnitScheme);
  
    /** 释放由 LoadData 调入内存中的详细数据 */
    // void __stdcall FreeData (HANDLE hDataObj)
    void FreeData(Long hBriefInfo);
  
    /** 获取测试开始时间 */
    // __time32_t __stdcall GetStartTime (HANDLE hBriefInfoOrDataObj)
    Long GetStartTime(Long hBriefInfo);
  
    /** 获取概要信息中的测试通道信息 */
    // bool __stdcall GetChannel (HANDLE hBriefInfoOrDataObj, BYTE& cBoxNo, BYTE &cChl, DWORD& dwDownId, WORD& wDownVer)
    Boolean GetChannel(Long hBriefInfo, ByteByReference cBoxNo, ByteByReference cChl, LongByReference dwDownId, LongByReference wDownVer);
  
    /** 根据 下位机ID 获取描述信息 */
    // bool GetDescriptionOfDownId(VARIANT& vDesc, DWORD dwDownId, bool bDotSet=true)
    Boolean GetDescriptionOfDownId(Pointer vDesc, Long dwDownId, Boolean bDotSept);
  
    /** 获取“列”ID 的文字描述 */
    // bool __stdcall GetDescriptionOfColumn (UINT columnID, VARIANT& vDesc)
	Boolean GetDescriptionOfColumn(Long columnID, Pointer vDesc);
  
    /** 获取指定数据列的行数,即数据的个数 */
    // int __stdcall GetRows (HANDLE hDataObj, UINT columnID)
    int GetRows(Long hBriefInfo, Long columnID);
  
    /** 只能用于访问 float 类型(即 VT_R4 型)的数据,如电压、电流等 */
    // float __stdcall GetDataAsFloat(HANDLE hDataObj, UINT columnID, int nRow)
	Float GetDataAsFloat(Long hBriefInfo, Long columnID, Long nRow);
}

注意:Dll 函数接口文档声明了很多种类型,如 HANDLE、UINT、VARIANT 几个用的比较频繁的类型说说

  • HANDLE:句柄,对应 Java 的 Long 类型,句柄的含义参考:什么是句柄(handle)
  • UNIT:对应于 32 位无符号整数
  • VARIANT:特殊的数据类型,没有类型声明字符,能够在运行期间动态的改变类型,支持所有简单数据类型,如整型、浮点、字符串、布尔型、日期时间、货币等

调用 Java 接口

理清楚 JNA 使用规则,就可以编写业务代码了,就是按照文档提供的函数去操作 cex 文件,获取需要的数据

还有一个问题就是,设备产生的 cex 文件是增量文件需要处理,还是先理个思路:

写个定时任务监听存放 cex 路径下的 cex 文件,如果文件有“变化”就操作变化的部分,这里的变化就是增量的记录,使用 ThreadLocal 存储上一次读取的行数 oldRows,如果这次的行数 newRows 相较于 ThreadLocal 存储的行数 oldRows 大,那么就操作 oldRows 到 newRows 记录

public class DllDemo {

    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println("=================================================");
        System.out.println(DllUtils.instance.toString());
        System.out.println("=================================================");

        // 1. 校验 Dll 版本
        Long dllVersion = DllUtils.instance.GetDllVersion();
        System.out.println("当前 Dll 版本: 0x000" + Long.toHexString(dllVersion));
        if (!DllUtils.instance.CheckDllVersion(DllConstant.LANDCEX_DLL_VER_0_3_1_0)) {
            System.out.println("Dll版本不匹配);
        }

        // 2. 获取 cex 文件全路径
        String cexPath = "C:\\Users\\Kangyh\\Desktop\\-D221018_017_6.cex";

        // 3. 载入数据
        Long loadData = DllUtils.instance.LoadData(cexPath, DllConstant.USCH_BASEON_MA);

        // 使用 GetStartTime 测试一下 Dll 和 cex 文件
        Long startTimeSecond = DllUtils.instance.GetStartTime(loadData);
        Date date = new Date();
        date.setTime(startTimeSecond * 1000);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("startTime: " + simpleDateFormat.format(date));  // 2022-10-18 08:50:13

        // 4. 处理数据
        ByteByReference cBoxNo = new ByteByReference();
        ByteByReference cChl = new ByteByReference();
        LongByReference dwDownId = new LongByReference();
        LongByReference wDownVer = new LongByReference();
        if (DllUtils.instance.GetChannel(loadData, cBoxNo, cChl, dwDownId, wDownVer)) {
            System.out.println("cBoxNo: " + cBoxNo.getValue());      // 16
            System.out.println("cChl: " + cChl.getValue());          // 5
            System.out.println("dwDownId: " + dwDownId.getValue());  // 705888533
            System.out.println("wDownVer: " + wDownVer.getValue());  // 111
        } 

        // 2021.07.11.266
        // 2021.01.19.042
        Pointer downIdDesc = new Memory(20);
        if (DllUtils.instance.GetDescriptionOfDownId(downIdDesc, dwDownId.getValue(), false)) {
            int pointerOffset = downIdDesc.getInt(0);
            char[] data = downIdDesc.getPointer(pointerOffset).getCharArray(0, 12);
            System.out.println("downIdDesc:" + new String(data));  // 20210119042 
        }
        Native.free(Pointer.nativeValue(downIdDesc));
        Pointer.nativeValue(downIdDesc, 0);
                           
        Pointer desc = new Memory(20);
        if (DllUtils.instance.GetDescriptionOfColumn(DllConstant.CYCLE_CapC, desc)) {
            char[] data = desc.getPointer(8).getCharArray(0, 20);
            String descBydownId = new String(data);
            System.out.println("GetDescriptionOfColumn:" + descBydownId.substring(0,4));  // 充电容量
        }
        Native.free(Pointer.nativeValue(desc));
        Pointer.nativeValue(desc, 0);

                           
        Map<Integer, Float> cycleCapCMap = new HashMap<>();
        int cycleCapCLength = DllUtils.instance.GetRows(loadData, DllConstant.CYCLE_CapC);
        System.out.println("cycleCapCLength:" + cycleCapCLength);  // 201
        for (int i = 0; i < cycleCapCLength; i++) {
            Float cycleCapC = DllUtils.instance.GetDataAsFloat(loadData, DllConstant.CYCLE_CapC, (long) i);
            cycleCapCMap.put(i, cycleCapC);
        }

        Map<Integer, Float> cycleCapDMap = new HashMap<>();
        int cycleCapDLength = DllUtils.instance.GetRows(loadData, DllConstant.CYCLE_CapD);
        System.out.println("cycleCapDLength:" + cycleCapDLength);  // 201
        for (int i = 0; i < cycleCapDLength; i++) {
            Float cycleCapD = DllUtils.instance.GetDataAsFloat(loadData, DllConstant.CYCLE_CapD, (long) i);
            cycleCapDMap.put(i, cycleCapD);
        }

        cycleCapCMap.forEach((keyC, valueC) -> {
            cycleCapDMap.forEach((keyD, valueD) -> {
                if (keyC.equals(keyD)) {
                    System.out.println("充电容量" + valueC.toString() + "-- 放电容量" + valueD.toString());
                }
            });
        });
                           
                           
        // 5. 释放数据
        DllUtils.instance.FreeData(loadData);
    }

}

常见问题

Unable to load library

检查 Dll 文件路径访问是否存在问题,没有找到动态库文件,有些路径好像是要重启电脑才能加载

Invalid memory access

访问无效的内存,应该是在调 getPointer 方法时出现的问题

访问数据乱码问题

要么是真的字符集不对应,要么就是使用 Pointer 取的数据不对,后者可能性居多。还有一点就是如果每次运行的数据都不对,可能是这个数据是 Pointer,所以一直在变化


标题:Java 使用 JNA 调用 DLL 动态库的函数操作文件
作者:Hefery
地址:http://hefery.icu/articles/2022/10/13/1665654134166.html