java高级开发

JNA模拟C类型——Java映射char*、int*、float

2022-10-14  本文已影响0人  老鼠AI大米_Java全栈

最近项目在用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代码中用于向调用方返回值以及函数结果。

image.png
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

上一篇下一篇

猜你喜欢

热点阅读