>创建一个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