从零开始的JNA之路(二):自定义DLL的调取

>创建一个DLL文件

    首先编写一个DLL,打开VS,创建一个DLL项目:

         

     

    可以直接勾上预编译头,调整工程结构,我的工程结构如下: 

    

    其中,源文件中代码如下:

#include "MyDllHead.h"
#include "stdio.h"

void pickCard(char * name, Card * card) {
	printf("%s pick the Card %ld.\n",name,card->id);
}
     MyDllHead.h文件中代码如下:

#define DLLIMPORT __declspec(dllimport)

/*Your code here*/

/*Example Struct*/
struct Card {
	long id;
};

/*Example Method*/
extern "C" DLLIMPORT void pickCard( char * name,Card * card);
    头文件中分别声明了一个结构体Card和一个函数pickCard(char*,Card*),pickCard是一个extern函数,在源.cpp中进行了实现。直接编译成DLL文件:   

     

     编译后可在.\Visual Studio 2017\Projects\工程名\x64\Debug中找到编译完成后的dll文件。

     注意,编译完成后可能会有warning:dlll链接不一致或下图情况,都是正常的,不用理会。

    


>JNA调取DLL

     JNA代码如下:

// 自定义的接口库
public interface MyLibrary extends Library {
	// 定义并初始化接口的静态变量
	MyLibrary Instance=(MyLibrary) Native.loadLibrary("My64DllPrj",MyLibrary.class);
	public class Card extends Structure {
		// 结构体值表
		public NativeLong id;
		// 引用传递
		public static class ByReference extends Card implements Structure.ByReference{ };
		// 值传递
		public static class ByValue extends Card implements Structure.ByValue{ };
		@Override
		protected List<String> getFieldOrder() {
			List<String> list = new ArrayList<>();
			list.add("id");// 注意!这个"id",对应的是JAVA中的变量名,而不是C语言中的
			return list;
		}
	}
	// DLL库中的函数声明
	void pickCard(String name,Card card);
}

     与 >上一篇<中调取msvcrt.dlll相比,我们为了实现模拟C语言中定义的结构体,我们在MyLibrary接口中定义了一个内部类Card,并继承了jna.Structure类。其中有三个要素:

  • 结构体的值表
  • 结构体的两种传递方式
  • 值表顺序

    我们一个个来说:


    一、结构体的值表就是用JAVA中的数据类型模拟C语言中的值表

    

     参考:https://github.com/java-native-access/jna/blob/master/www/Mappings.md


     那么,如果是一个数组,JAVA中该怎么模拟呢?如:

struct Desk {
	Card cards[50];
};
     需要这么模拟:

public class Desk extends Structure {
	//值表 注意:必须是ByValue
	public Card.ByValue[] cards = new Card.ByValue[50];

	// 引用传递与值传递
	public static class ByReference extends Desk implements Structure.ByReference{ };
	public static class ByValue extends Desk implements Structure.ByValue{ };
	@Override
	protected List<String> getFieldOrder() {
		List<String> list = new ArrayList<>();
		list.add("cards");
		return list;
	}
}
      需要注意的是,我们必须仿照C语言提前划定空间的特性,提前 new一个相应大小的对象数组,且必须是值传递的(ByValue)


     二、引用方式有两种:引用传递(ByReference)和值传递(ByValue)

     查看API,可以发现Structure.ByReference和Structure.ByValue是两个空接口,也就是标记接口(Marker);标记接口是接口的非典型用法,常用于指出某类的某种特性,如对于JDK中的Serializable接口,实现了该接口,我们就认为该类是可被序列化的,也就不再抛出SerializationException。这种用法在JAVA引入注解机制之前非常常见。

     注意到这两个标记接口都继承了Card类,用于表示new出来的Card类是值传递类型还是引用传递类型。

     为什么要标示是值传递类型还是引用传递类型的呢?

     我们都知道JAVA中的一切对象传参都是引用传参,但C语言却有实参虚参;在默认情况下,JAVA传参是以引用ByReference传参的。观察下面这个例子:

// JAVA代码中,DLL库中的函数声明
void pickCard(String name,Card card);// 注意,JAVA默认的是引用传递 等效于 pickCard(String name, Card.ByReference card)
// C++代码中,实际的实现方式。注意参数表用的是指针Card *
void pickCard(char * name, Card * card) {
	printf("%s pick the Card %ld.\n",name,card->id);
}
     那么,在JAVA中,我们调用DLL可以这么写:

MyLibrary.Card.ByReference card = new MyLibrary.Card.ByReference();// 注意这里,是ByReference()
card.id = new NativeLong(14);
MyLibrary.Instance.pickCard("身披白袍",card);
    如果改写成new MyLibrary.Card.ByValue(),那么传参将会失败,JAVA虚拟机运行到JNA模拟的pickCard()函数将会抛出错误。

    如果要使用值传递呢?我们需要把JAVA代码中的DLL库函数声明改为:

void pickCard(String name,Card.ByValue card);

    再总结一下:

  • C中传参为&或*(引用或指针)的,JAVA中必为Class.ByReference或Class;
  • C中传参为虚参的,JAVA中必为Class.ByValue;


    三、值域表顺序

    在>上一篇<中,我提到“Java调用dll中的C函数,实际上把一段地址(内存)传递给了dll,并通过相应的c函数进行处理”。别忘了由于C语言是一段连续和有顺序的内存,JNA在模拟时,也需要体现出连续、有顺序。

   JAVA虚拟机在编译.java文件为.class文件时,有权利重新安排Field的顺序。因此,为了复现“顺序性”,保证CLASS在反射时能够按顺序把对应的JAVA值类型映射为C语言中的相应值,而getFieldOrder函数就是用于完成这一个映射动作的。

   上文代码中的list.add("id"),其中的"id",对应的是JAVA模拟Card类中public NativeLong id的值名"id",而不是对应着DLL头文件中struct Card中的id。

    再举个小例子,对于C语言中的这个结构体:

struct Men{
     long Cage;
     char* Cname;
}
    那么JAVA中的模拟类就要这么写:
public class Jmen extends Structure {
	//值表:定义顺序无所谓
	public NativeLong Jage;
	public String Jname;		
	// 引用传递与值传递
	public static class ByReference extends Jmen implements Structure.ByReference{ };
	public static class ByValue extends Jmen implements Structure.ByValue{ };
	@Override
	protected List<String> getFieldOrder() {
		List<String> list = new ArrayList<>();
		list.add("Jage");// 这里的顺序必须与C中的定义一致
		list.add("Jname");
		return list;
	}
}


>测试

    把My64DllPrj.dll文件移动到JAVA工程目录下。注意DLL文件并不是移动到工程目录下的src文件夹中,而是移动到该模拟类实际.class文件所在之处。举个例子,比如我用IDEA,编译结果默认输出地址为:.工程目录\out\production\包名\,模拟类的类名为JNATest.class:


    输出如下:


    需要注意的是,在生成自己编写的dll文件时,要注意JDK的位数版本要和生成dll的编译的位数一致。如上文中利用VS2017X64编译生成的就是64位的DLL文件。要查看自己的JDK位数,可以打开CMD,输入java -version查看:



    本文所有代码可从此处查看:https://gitee.com/shenpibaipao/codes/ir15ljnhfgydp26mev4qk98




猜你喜欢

转载自blog.csdn.net/shenpibaipao/article/details/79214757