JNA 从结构内的结构数组中读取无效的内存访问

Invalid memory access with JNA reading from an array of structures inside a structure

我的任务是将 C# 应用程序迁移到 java 应用程序。 C# 应用程序使用几个 DLL 来完成它的工作,与外围设备通信。

DLL 的 header 在 C#

中看起来像这样
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct InnerStructure
    {
        /// COM port used by the device
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = PORT_SIZE)] //7
        public string szPort;            

        /// Specifies whether the device is activated.
        public bool fActivated;         

        /// Name of the device
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_NAME_SIZE)]  //248
        public string szName;             //COM
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct ParamStructure
    {
        /// COM port used by the devices
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
        public InnerStructure[] USB;
    }

    [DllImport("PCLService.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern bool startPclService();

    [DllImport("PCLService.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern bool stopPclService();

    [DllImport("PclUtilities.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)]
    public static extern int getUSBDevices(ref ParamStructure pStructure, ref int pdwSize, ref int pdwEntries);

我最初尝试使用 JNI,但我无法加载 DLL(他们使用 .NET,并且很难找到 DependenciesWalker 的依赖项),所以我切换到 JNA。

这是我的Java代码

public class DllDemo {

    @Structure.FieldOrder({ "szPort", "fActivated", "szName" })
    public static class InnerStructure extends Structure {
        public PointerByReference szPort;
        public IntByReference fActivated;
        public PointerByReference szName;

        public InnerStructure() {};
        public InnerStructure(Pointer p) {
            super(p);
            read();
        };
    }

    @Structure.FieldOrder({ "USB" })
    public static class ParamStructure extends Structure implements Structure.ByReference {
        // In the C# code, the array is size 10
        public InnerStructure[] USB = (InnerStructure[])new InnerStructure().toArray(10);

        public ParamStructure() {};
        public ParamStructure(Pointer p) {
            super(p);
            read();
        };
    }

    public interface MyService extends Library {
        MyService INSTANCE = (MyService) Native.load("C:\IRD\Documentacion\VisaNet\App PCL Demo VisaNet - V2.11\x32\PCLService.dll", MyService.class);
        boolean startPclService();
        boolean stopPclService();
    }

    public interface MyUtilities extends Library {
        MyUtilities INSTANCE = (MyUtilities) Native.load("C:\IRD\Documentacion\VisaNet\App PCL Demo VisaNet - V2.11\x32\PclUtilities", MyUtilities.class);
        int getUSBDevices(ParamStructure paramStructure, IntByReference pdwSize, IntByReference pdwEntries);
    }

    public static void main(String args[]) {
        System.out.println("start");

        MyService.INSTANCE.startPclService();
        ParamStructure paramStructure = new ParamStructure();
        paramStructure.write();
        //This value is copied straightforward from the original code as well
        int size = (248 + 8 + 7) * 10;
        IntByReference pdwSize = new IntByReference(size);
        IntByReference pdwEntries = new IntByReference(0);

        int Ret2 = MyUtilities.INSTANCE.getUSBDevices(paramStructure, pdwSize, pdwEntries);
        System.out.println("Ret2 = " + Ret2 + ", pdwEntries = " + pdwEntries.getValue());
        if (pdwEntries.getValue() > 0) {
            for (int i = 0 ; i < pdwEntries.getValue() ; i++) {
                InnerStructure inner = paramStructure.USB[i];
                inner.read();
                System.out.println(i + " => " + inner.toString());
                System.out.println("toString 1 => " + inner.szPort.toString());
                System.out.println("toString 2 => " + inner.szPort.getPointer().toString());
                System.out.println(">" + inner.szPort.getPointer().getString(0, "utf8") + "<");
            }
        }
        paramStructure.clear();
        MyService.INSTANCE.stopPclService();
        System.out.println("stop");
    }
}

这是输出。

start
Ret2 = 0, pdwEntries = 1
0 => DllDemo$InnerStructure(allocated@0x59b550 (12 bytes) (shared from auto-allocated@0x59b550 (120 bytes))) {
  PointerByReference szPort@0x0=native@0x4f0043 (com.sun.jna.ptr.PointerByReference@4f0043)
  IntByReference fActivated@0x4=native@0x35004d (com.sun.jna.ptr.IntByReference@35004d)
  PointerByReference szName@0x8=null
}
toString 1 => native@0x4f0043 (com.sun.jna.ptr.PointerByReference@4f0043)
toString 2 => native@0x4f0043
Exception in thread "main" java.lang.Error: Invalid memory access
    at com.sun.jna.Native.getStringBytes(Native Method)
    at com.sun.jna.Native.getString(Native.java:2224)
    at com.sun.jna.Pointer.getString(Pointer.java:681)
    at com.ingenico.DllDemo.main(DllDemo.java:65)
Process finished with exit code 1

第 65 行是这样的

System.out.println(">" + inner.szPort.getPointer().getString(0, "utf8") + "<");

有时它不会给出错误,但字符串为空。我还没弄明白为什么会这样。

类 和行 paramStructure.write() 和 inner.read() 中是否存在构造函数没有区别。

无论如何,这就是它在 IntelliJ 调试器中的样子

debugger

我试过像这样改变内部结构

public static class InnerStructure extends Structure implements Structure.ByReference {
    public PointerByReference szPort;
    public IntByReference fActivated;
    public PointerByReference szName;

    public InnerStructure() {};
    public InnerStructure(Pointer p) { super(p); };
}

甚至这样。

public static class InnerStructure extends Structure implements Structure.ByReference {
    public String szPort;
    public int fActivated;
    public String szName;

    public InnerStructure() {};
    public InnerStructure(Pointer p) { super(p); };
}

在这两种情况下,我得到

Exception in thread "main" java.lang.Error: Invalid memory access
    at com.sun.jna.Native._getPointer(Native Method)
    at com.sun.jna.Native.getPointer(Native.java:2211)
    at com.sun.jna.Pointer.getPointer(Pointer.java:642)
    at com.sun.jna.Pointer.getValue(Pointer.java:390)
    at com.sun.jna.Structure.readField(Structure.java:732)
    at com.sun.jna.Structure.read(Structure.java:591)
    at com.sun.jna.Structure.autoRead(Structure.java:2141)
    at com.sun.jna.Structure.conditionalAutoRead(Structure.java:561)
    at com.sun.jna.Structure.updateStructureByReference(Structure.java:690)
    at com.sun.jna.Pointer.readArray(Pointer.java:492)
    at com.sun.jna.Pointer.getValue(Pointer.java:450)
    at com.sun.jna.Structure.readField(Structure.java:732)
    at com.sun.jna.Structure.read(Structure.java:591)
    at com.sun.jna.Structure.autoRead(Structure.java:2141)
    at com.sun.jna.Function.invoke(Function.java:381)
    at com.sun.jna.Library$Handler.invoke(Library.java:265)
    at com.sun.proxy.$Proxy3.getUSBDevices(Unknown Source)
    at com.ingenico.DllDemo.main(DllDemo.java:49) <- getUSBDevices

您没有正确映射字符串。

您已将 szPort 定义为 PointerByReference,它是指向包含指针的内存位置的指针。然后你试图用 inner.szPort.getPointer() 调用它。那还是一样"pointer to the pointer"。 JNA 的一个好的经验法则是,如果您正在使用 ByReference class 并且从不访问它的 getValue() 方法,那么您可能做错了什么。你的意思可能是 inner.szPort.getValue().getString(...)。但这会失败,因为您实际上没有真正的指针。填充 szPort 元素的前 4 个字节实际上是 unicode 字符。

您已经定义了包含 3 个 4 字节元素(3 x 4 字节指针)的结构,总计 12 个字节(根据您的调试图像)。您实际上需要一个 7 个字符的固定宽度字符串、4 个字节的布尔值和 248 个字符的固定宽度字符串。如果您的编码是 ASCII,则为 259 个字节,或者在您的情况下,Unicode 字符串为 514 个字节。

查看您的调试图像,我们可以了解发生了什么。前 4 个字节包含 0x0043004f,您将其视为指针(地址 0x4f0043)并尝试从中读取数据。但它实际上是 unicode 字符 0x0043 ("C") 和 0x004f ("O")。当您尝试从该地址读取时,您并不拥有内存。如果幸运的话,内存为零并且您从未读过任何东西,并且您的代码 returns 是一个零长度的空终止字符串。但是,如果该内存不是 null,则会出现错误。

接下来的 4 个字节是 0x004d0035,您将其称为 IntByReference "pointer" (0x35004d),但它实际上是字符 "M" 和“5”。由于 szName 字段有一个空指针,接下来的 4 个字节似乎是 0x00000000 并且您已经命中了空终止符。所以看起来你的 szPort 字符串是 "COM5",隐藏在众目睽睽之下!

现在我们知道您的代码为什么会出错了。你是怎么解决的?

您的 C# 映射定义 szPort 的类型是 UnmanagedType.ByValTStr,其中 according to MS Docs 是:

A fixed-length array of characters; the array's type is determined by the character set of the containing structure.

您可能需要该结构中的字节数组(您可以使用适当的编码将其放入 String 构造函数中),长度为 PORT_SIZE x 字符宽度(如果您的编码为 1是 ASCII,2 表示 Unicode)。您应该使用 charwidth x MAX_NAME_SIZE.

类似地映射 szName

最后,IntByReference 也是一个指针(在您的系统上是 4 个字节),但 C# 中的原始变量是 bool。如果不对其进行编组,它会映射到 Windows BOOL,这是一个普通的 4 字节 C int。 Java 的 boolean 也是 4 个字节,所以你可以在这里使用它。所以你可能想要这样的东西:

int CHAR_WIDTH = W32APITypeMapper.DEFAULT == W32APITypeMapper.UNICODE ? 2 : 1;
int PORT_SIZE = 7;
int MAX_NAME_SIZE = 248;

class InnerStructure ... {
    public byte[] szPort = new byte[CHAR_WIDTH * PORT_SIZE];
    public boolean fActivated;
    public byte[] szName = new byte[CHAR_WIDTH * MAX_NAME_SIZE];
...
}