JNA模拟C类型——Java映射char*、int*、float
最近项目在用Java调用C写的一些三方库,没办法直接调,用Java封装一下C的接口,这就少不了要用到JNA的知识。
关于JNA相关概念介绍参考一位博主的文章[JNA结构体的使用]。这里主要分享一些比较复杂的类型之间的映射关系。
JNA介绍
JNA(Java Native Access)框架是一个开源的Java框架,是SUN公司主导开发的,建立在经典的JNI的基础之上的一个框架。它提供一组Java工具类用于在运行期动态访问系统本地共享类库而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。
JNA调用过程
JNA调用C/C++的过程大致如下:
image.png
也就是说,不需要写任何C/C++的代码,我们就能调用C/C++的程序里面的程序
JNA版本与依赖
目前最高版本是5.12.0,JNA的项目是放在Github【点击访问】上面
maven项目可以使用直接依赖
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.12.0</version>
</dependency>
然后引入项目中即可使用,引入方法可以参照 [IDEA 导入外部JAR文件【点击访问】]
JNA使用演示
demo项目说明:
1、普通java项目,程序入口是main函数
2、c语言文件hello.c,生成动态链接库文件libhello.so
3、项目结构类HelloJNA加载libhello.so,调用其指定方法完成a+b的计算,返回计算结果
4、项目结构如下:
image.png
1、hello.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
/**
* 返回a+b的值
*/
int add(int a, int b){
return a + b;
}
2、HelloJNA.java
import com.sun.jna.Library;
import com.sun.jna.Native;
/**
* 一个java类
* 运行环境是linux,需要打包生成jar文件放到linux环境运行
*/
public class HelloJNA {
/**
* 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
* 这个接口对应一个动态链接(SO)文件
*/
public interface LibraryAdd extends Library {
// 这里使用绝对路径加载
LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);
/**
* 接口中只需要定义你要用到的函数或者公共变量,不需要的可以不定义
* 映射libadd.so里面的函数,注意类型要匹配
*/
int add(int a, int b);
}
public static void main(String[] args) {
// 调用so映射的接口函数
int add = LibraryAdd.LIBRARY_ADD.add(10, 15);
System.out.println("相加结果:" + add);
}
}
3、项目打包
打包java程序成可执行jar,具体可参考 该文章后半部分打包步骤【点击访问】
打包完之后,将 JNATestC.jar 文件上传到linux的 /program/cpp/ 目录
4、so文件
生成so动态链接库
把 hello.c 放到linux环境的 /program/cpp/ 目录下,并在该目录下运行
gcc -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -shared -o libhello.so hello.c
5、运行程序
进入 /program/cpp/ 目录
image.png
执行java程序,执行结果符合预期
image.png
类型映射关系
Java/Native Type Conversions
官方给出的映射关系如下:
image.png
理论上,对于枚举类型的指针,Java中可以使用Pointer传参。
Java和C基本类型指针对应关系
C类型 | Java类型 |
---|---|
char* | ByteByReference或Pointer |
int* | IntByReference或Pointer |
float* | FloatByReference或Pointer |
double* | DoubleByReference或Pointer |
建议使用对应的ByReference对象替代Pointer,使用Pointer有时可能会得到一个垃圾值(正常情况下两种方式结果一样)
ByReference类子类
ByteByReference、DoubleByReference、FloatByReference、 IntByReference、LongByReference、 NativeLongByReference、PointerByReference、 ShortByReference、 W32API.HANDLEByReference、X11.AtomByReference、X11.WindowByReference
ByteByReference等类故名思议,就是指向原生代码中的字节数据的指针。
PointerByReference类表示指向指针的指针。
在JNA中模拟指针,最常用到的就是Pointer类和PointerByReference类。Pointer类代表指向任何东西的指针,PointerByReference类表示指向指针的指针。Pointer类更加通用,事实上PointerByReference类内部也持有Pointer类的实例。
Native Type | Java Type |
---|---|
void ** | PointerByReference |
void* | Pointer |
char** | PointerByReference |
char& | PointerByReference |
char* | ByteByReference/Pointer |
int& | IntByReference |
int* | IntByReference |
指针参数Pointer
在java中都是值传递,但是因为使用JNA框架,目标函数是C/C++是有地址变量的,很多时候都需要将变量的结果带回,因此,地址传递在JNA项目中几乎是必须的。
/**
* 返回a+b的值
* 同时c和msg通过参数返回
*/
int add(int a, int b, int* c, char* msg) {
*c = (a + b) * 2;
//msg = "hello world!"; //与string& msg配置使用
snprintf(msg, 100, "hello world!");
//*msg = string; //std::string& msg
return a + b;
}
java这样如下
public class HelloJNA {
/**
* 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
* 这个接口对应一个动态链接(SO)文件
*/
public interface LibraryAdd extends Library {
// 这里使用绝对路径加载
LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);
int add(int a, int b, int c, String msg);
}
public static void main(String[] args) {
int c = 0;
String msg = "start";
// 调用so映射的接口函数
int add = LibraryAdd.LIBRARY_ADD.add(10, 15, c, msg);
System.out.println("相加结果:" + add);
}
}
那么不管add函数对c和msg做了何种改变,返回java中,值都不会被变更。
那么如何实现类似C语言那样的地址传递,或者说指针传递呢?在JNA框架中,我们可以借助一个类完成,他就是Pointer。
com.sun.jna.Pointer,指针数据类型,用于匹配转换映射函数的指针变量。
1、定义
Pointer c = new Memory(50);
Pointer msg = new Memory(50);
说明:
这样的指针变量定义很像C的写法,就是在定义的时候申请空间。比如这里就申请50个空间
根据测试结果对于字符串,一个空间对于两个字符左右。如果返回的结果长度比分配的空间大,则会报错
Exception in thread “main” java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=6, offset=8
最后可以这样释放申请的空间
Native.free(Pointer.nativeValue(c)); //手动释放内存
Pointer.nativeValue(c, 0); //避免Memory对象被GC时重复执行Nativ.free()方法
Native.free(Pointer.nativeValue(msg));
Pointer.nativeValue(msg, 0);
2、使用
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
/**
* 一个java类
* 演示指针传输指针变量
*/
public class HelloJNA_Pointer {
/**
* 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
* 这个接口对应一个动态链接(SO)文件
*/
public interface LibraryAdd extends Library {
LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);
/**
* 指针变量,用Pointer类型定义
* c是int*
* msg是char**
*/
int add_c(int a, int b, Pointer c, Pointer msg);
}
public static void main(String[] args) {
Pointer c = new Memory(50);
Pointer msg = new Memory(8);
// 调用so映射的接口函数
int add = LibraryAdd.LIBRARY_ADD.add_c(10, 15, c, msg);
System.out.println("相加结果:" + add);
// 指针变量
System.out.println("c的值:" + c.getInt(0));
// 这样才能拿到
System.out.println("msg的值:" + msg.getPointer(0).getString(0));
Native.free(Pointer.nativeValue(c)); //手动释放内存
Pointer.nativeValue(c, 0); //避免Memory对象被GC时重复执行Nativ.free()方法
Native.free(Pointer.nativeValue(msg)); //手动释放内存
Pointer.nativeValue(msg, 0); //避免Memory对象被GC时重复执行Nativ.free()方法
}
}
说明:
①、传递参数直接传Pointer 定义出来的对象即可
②、取值时,需要根据变量的类型采用不同的API读取,例如,如果函数是int*变量,那么就c.getInt(0)
如果是char **msg,就msg.getPointer(0).getString(0)
③、指针说明:一层指针就直接取值c.getInt(0),其中0表示偏移量从0开始;如果是二级指针,msg.getPointer(0).getString(0),表示指针的指针再取值;多级指针以此类推。
3、运行结果
[root@192 cpp]# java -jar JNATestC.jar
相加结果:25
c的值:50
msg的值:hello world!
引用传递ByReference
引入另一个类ByReference来实现参数的地址传递(指针传递)
ByReference类
com.sun.jna.ptr.ByReference
提供通用的“指向类型的指针”功能,通常在C代码中用于向调用方返回值以及函数结果。
ByReference提供了很多继承类,类似Pointer的指针属性,每种数据类型都对应子类供使用
比如int的用IntByReference,字符串的使用PointerByReference。
应用
注意上面的add方法中, char*使用ByteByReference对应
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
/**
* 一个java类
* 演示指针传输指针变量
*/
public class HelloJNA_ByReference {
/**
* 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
* 这个接口对应一个动态链接(SO)文件
*/
public interface LibraryAdd extends Library {
LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);
/**
* 指针变量,用IntByReference【整型】,PointerByReference【字符串指针】类型定义
* c是int*
* msg是char*
*/
int add_c(int a, int b, IntByReference c, ByteByReference msg);
}
public static void main(String[] args) {
IntByReference c = new IntByReference();
ByteByReference msg = new ByteByReference();
// 调用so映射的接口函数
int add = LibraryAdd.LIBRARY_ADD.add_c(10, 15, c, msg);
System.out.println("相加结果:" + add);
// 指针变量IntByReference的值需要getValue()
System.out.println("c的值:" + c.getValue());
System.out.println("msg的值:" + msg.getValue().getString(0));
}
}
运行结果:
image.png
小结
Pointer和ByReference都可以在JNA项目中用来地址传递参数,Pointer的使用方式更像C/C++的语言结构,自己分配内存空间,自己释放。ByReference则是完完全全的java语法,只要用就行了,内存通过垃圾回收完成。
所以总的来讲ByReference是更好的,但是对于更多层的指针引用,可能Pointer更合适。
参考:
https://blog.csdn.net/zhan107876/article/details/121051129
https://blog.csdn.net/zhan107876/article/details/121056384
https://blog.csdn.net/zhan107876/article/details/121058925
https://blog.csdn.net/zhan107876/article/details/121088636