虽说跨平台是Java的一个巨大大优势,但是在CICD + 镜像 + 云原生的今天。这种优势在企业端越来越不重要了,镜像与容器屏蔽了环境差异,CICD可以在对应环境下生成对应的镜像。据我所知,搞cv的同学和搞Android的同学使用native技术比较多。

JNI(Java Native Interface)

我们可以看到很多JDK提供的很多方法是native方法,尤其是在Unsafe类里面,很多调用都依赖系统实现。有些对性能由极致要求的框架与库也在某些实现上也使用native实现,典型的是Netty,它提供了大量调用在本地库里面。你可以在其resource/native看到当前模块的本地库。

实践

虽然Java提供了一些根据方便我们实现本地方法,但是对于我个人来说,JNI的开发还是有些丑陋的。

标识你提供的native方法:

package com.bigbrotherlee.jni;

public class TestNativeClass {
    public native String doSome(ObjectClass person);
}

参数:

package com.bigbrotherlee.jni;

import lombok.Data;

@Data
public class ObjectClass {
    private String name;
    private Long age;
    private boolean gender;
}

生成头文件、实现具体逻辑

javah com.bigbrotherlee.jni.TestNativeClass

生成内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_bigbrotherlee_jni_TestNativeClass */

#ifndef _Included_com_bigbrotherlee_jni_TestNativeClass
#define _Included_com_bigbrotherlee_jni_TestNativeClass
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_bigbrotherlee_jni_TestNativeClass
 * Method:    doSome
 * Signature: (Lcom/bigbrotherlee/jni/ObjectClass;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_bigbrotherlee_jni_TestNativeClass_doSome
  (JNIEnv *, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif

将头文件复制到cpp项目里面,复制jni.h与jni_md.h(在jdk目录下面的include文件夹),修改头文件#include <jni.h>为#include "jni.h",创建cpp文件并实现业务:

#include <iostream>
#include "com_bigbrotherlee_jni_TestNativeClass.h"
using namespace std;

JNIEXPORT jstring JNICALL Java_com_bigbrotherlee_jni_TestNativeClass_doSome
  (JNIEnv * env, jobject thisObj, jobject param)
{
   // using namespace std;
  std::cout << "Hello World! cpp >>>>> " << std::endl;
  jclass paramClass = env -> GetObjectClass(param);
  
  jfieldID nameId = env -> GetFieldID(paramClass,"name","Ljava/lang/String;");
  jstring nameStr = static_cast<jstring>(env -> GetObjectField(param,nameId));
  jsize size = env->GetStringLength(nameStr);
  std::cout << "start name allChar size = " << size << std::endl;
  jboolean *isCopy = JNI_FALSE;
  const char* allChar = env-> GetStringUTFChars(nameStr,isCopy);
  std::cout << "start name allChar " << std::endl;
  std::cout << "name=" << allChar << std::endl;
  env->ReleaseStringUTFChars(nameStr,allChar);

  
  jfieldID ageId = env -> GetFieldID(paramClass,"age","Ljava/lang/Long;");
  jobject ageObj = env->GetObjectField(param,ageId);
  jmethodID toString = env->GetMethodID(paramClass,"toString","()Ljava/lang/String;");
  jstring ageStr = static_cast<jstring>(env-> CallObjectMethod(ageObj,toString));
  const char* allAgeChar = env->GetStringUTFChars(ageStr,isCopy);
  std::cout << "age=" << allAgeChar << std::endl;
  env->ReleaseStringUTFChars(ageStr,allAgeChar);


  jfieldID genderId = env -> GetFieldID(paramClass,"gender","Z");
  jboolean gender = env->GetBooleanField(param,genderId);
  bool genderBool = gender == JNI_TRUE;
  std::cout << "gender=" << genderBool << std::endl;

  

  std::cout << "<== cpp" << std::endl;
  return env->NewStringUTF("hello native");
}

总的来说,就是通过env当前上下文环境获取与执行方法、获取属性等等操作,过程十分繁琐。一个方法的前两个参数分别是当前上下文环境抽象与隐式参数this,后面的参数是参数列表

生成动态链接库:

C:\mingw64\bin\g++.exe "-IC:\Program Files\Java\jdk1.8.0_251\include" -fPIC -shared D:\workspace\demo\jni-and-jna\src\c\com_bigbrotherlee_jni_TestNativeClass.cpp -o D:\workspace\demo\jni-and-jna\src\c\com_bigbrotherlee_jni_TestNativeClass.dll

注意:

  • 安装cpp环境,MinGW-w64安装gcc编译环境 ,环境用来玩已经ok了,你也可以通过CryWin安装编译环境,安装完成后将bin加入path环境变量就ok了。
  • Ljava/lang/String; 方法和属性签名是带有" ; "的。

使用:

将本地库文件复制到java项目目录。
默认情况下的java.library.path是带有当前目录“ . ”的,如果报找不到文件的错误,建议你修改自己的加载库的路径逻辑,通过代码找到当前项目目录下面的库文件。虽然网上绝大多人通过修改-Djava.library.path达到目的,但是为了环境稳定,不引入新的问题我个人建议不使用修改配置的方式。

package com.bigbrotherlee.jni;

public class MainClass {

    static {
        System.loadLibrary("com_bigbrotherlee_jni_TestNativeClass");
    }

    public static void main(String[] args) {
        ObjectClass objectClass = new ObjectClass();
        objectClass.setAge(18L);
        objectClass.setGender(true);
        objectClass.setName("lige");
        TestNativeClass testNativeClass = new TestNativeClass();
        System.out.println(testNativeClass.doSome(objectClass)+" ==> java");
    }
}

JNA(Java Native Access)

JNA并不是jdk标准的一部分,它是基于JNI发展出来的一个工具库,可以让你很方便的调用本地库。

实践

尽管默认已经映射了基本数据类型,大大简化了本地代码开发,但是在复杂类型映射上还是没办法,c有着更加丰富的数据类型。

引入jna库,声明函数

pom:

<!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna -->
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.13.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform -->
<!--        <dependency>-->
<!--            <groupId>net.java.dev.jna</groupId>-->
<!--            <artifactId>jna-platform</artifactId>-->
<!--            <version>5.13.0</version>-->
<!--        </dependency>-->

接口:继承Library,这里在加载本地库的时候会生成代理。

package com.bigbrotherlee.jna;

import com.sun.jna.Library;

public interface NativeAccessor extends Library {
    String doSome(ObjectClass person);
}

参数:必须是public,继承Structure是因为我们本地动态链接库C参数是一个结构体

@Data
@Structure.FieldOrder({"name","age","gender"})
public class ObjectClass extends Structure {
    public String name;
    public Long age;
    public Boolean gender;
}

实现业务逻辑

声明头文件:

extern "C" {
    typedef struct {
        char *name;
        long long age;
        bool gender;
    } ObjectClass;

    const char* doSome(ObjectClass * objectClass);
}

实现函数:

#include <iostream>
#include "simple_native.h"

using namespace std;


const char* doSome(ObjectClass * objectClass){
  std::cout << objectClass->age << std::endl;
  std::cout << objectClass->name << std::endl;
  std::cout << (objectClass->gender ? "true" :"false") << std::endl;

  return "helle jna";
}

导出动态链接库:

C:\mingw64\bin\g++.exe "-IC:\Program Files\Java\jdk1.8.0_251\include" -fPIC -shared D:\workspace\demo\jni-and-jna\src\c\simple_native.cpp -o D:\workspace\demo\jni-and-jna\src\c\simple_native.dll

使用

使用上要比JNI简单很多,Native.load("动态库路径",接口类,参数选项); 就可以加载本地动态库并开始使用。

package com.bigbrotherlee.jna;

import com.sun.jna.Native;

import java.util.HashMap;
import java.util.Map;

public class MainClass {
    public static void main(String[] args) {
        Map<String,String> options = new HashMap<>();
        NativeAccessor nativeAccessor = Native.load("D:\\workspace\\demo\\jni-and-jna\\src\\c\\simple_native.dll", NativeAccessor.class, options);
        ObjectClass objectClass = new ObjectClass();
        objectClass.setAge(Long.MAX_VALUE);
        objectClass.setGender(false);
        objectClass.setName("lige");
        String s = nativeAccessor.doSome(objectClass);
        System.out.println(s);

    }
}

注意:

  • Java类型与C类型映射:类型映射。java的long对于c的long long,通常标准库会取别名为int64_t。

总结

总的来说调用本地库还是挺繁琐的。并且由于Java屏蔽了平台类型差异数据长度差异使得我们在使用本地调用的时候会再次捡起糟粕。
通常情况下,调用已存在的库使用JNA可以不需要再写c代码,在环境与数据类型允许的情况下。JNI为我们在写C的时候封装了很多东西,在C与Java需要交互的时候会更加强大。

标签: none

添加新评论