java调用dll或者.so库--JNI

0. JNI

    .dll和.so就不用介绍了吧, 不知道的也应该不需要看本文, 就是库文件. JNI是啥, JNI全称Java Native Interface, 如果你有跟过Java的源码, 反正我觉得最恶心的就是一路跟到一个签名带native的方法, 你什么都看不到. 当然其实也不是什么都看不到要是想知道更加内部的原理还是有办法的, 比如下载hotspot的源码.JNI就是其它语言的代码和Java之间的一个桥, 这个其它语言基本也就是C++了.
    JNI的原理就没必要在这里探讨, 就写个hello world怎么使用. 先闲聊几句, 一开始学习Java的时候就有人说Java跨平台的, 一次编译到处运行, 其实了解的越多就有更多疑惑, 首先Java肯定是跨平台的没错, 然后其实也不那么完全的跨平台, 某些Java代码在不同的平台其实结果会有一点差别, 就我所知socket的某些属性设置, 在linux和windows上结果是不一样的.再其次其实Java所谓的跨平台, 嗯, “你以为岁月静好,其实有人在为你负重前行”, 那个负重前行的就是JVM.
    主要分两种动态注册和静态注册

1. 静态注册

  1. 第一步编写一个包含native方法的java类
    package aa.bb;
    public class TestJNI{
    	public native String hello();
    }
    
  2. 编译这个类生成.class文件
        嗯, 这个java程序应该不需要教吧, 还是说一下:
    • 在TestJNI的同级包目录执行javac TestJNI.java会生成TestJNI.class文件

    • 退到src根目录执行javah aa.bb.TestJNI会在根目录同级生成一个aa_bb_TestJNI.h, 这里解释一下, 如果你的路径是src/aa/bb/TestJNI, 而TestJNI.java的包路径是aa.bb那么就要到src目录下执行javah. 看一下生成的.h文件

      #include <jni.h>
      #ifndef _Included_aa_bb_TestJNI
      #define _Included_aa_bb_TestJNI
      #ifdef __cplusplus
      extern "C" {
      #endif
      	JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello
      		 (JNIEnv *, jobject);
      #ifdef __cplusplus
      }
      #endif
      #endif
      

          需要解释一下内容吗? 首先jni.h头文件在你的jdk文件目录下的include文件夹下, 这里面定义了一些c/c++数据类型到java数据类型的映射之类的宏变量. 下面两行是防止重复包含头文件, 然后第一个__cplusplus到与之配对的#endif那一行, 这还有一点不好解释, 简单说就是识别是否是通过g++等c++的编译器, 如果是就在中间的JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello (JNIEnv *, jobject);这句代码前后添加点内容, 变成extern "C" {JNIEXPORT void JNICALL Java_com_waxxd_TestJni_hello (JNIEnv *, jobject);}至于为什么要这样那就说来话长了, 不过懂的自然懂, 不懂的好像也没必要懂. 大致就是在静态注册的时候上面写的TestJNI类的hello方法会通过Java_com_waxxd_TestJni_hello这个函数名称去动态库找相应的函数调用, 但是c++是实现了函数重载的, Java_com_waxxd_TestJni_hello这个c++函数在编译后并不叫这个名, 也可以说不完全叫这名, 前后加了些东西. extern "C"就是告诉编译器这个函数按照c的方式(c没有实现重载, 所以不改名)编译, 如果不加按照c++的编译是找不到这个函数的.
          然后说说Java_com_waxxd_TestJni_hello 这个函数的两个参数:

      • JNIEnv 这个是当前的java环境, 可以通过这个指针调用java中很多有用的方法
      • jobject 这是个对象, 如果native方法是静态的, 就是当前对象的Class对象实例(为啥是Class对象呢? 这得从类加载说起), 如果不是静态的这个对象就是当前对象的实例. 上面的示例TestJNI的hello方法显然是当前对象实例.

          至于JNIEXPORT 和JNICALL 这两个宏在windows和linux环境是不一样的, JNICALL在linux环境没啥实意, 在windows环境下表示__stdcall, JNIEXPORT点到定义处, 如果知晓linux或者windows动态库编写就没啥可以讲的了.

  3. 编写动态库
    // TestJNI.cpp
    #include "aa_bb_TestJni.h"
    #include <iostream>
    JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello
      (JNIEnv *, jobject)
      {
          std::cout << "helloqqqq world" << std::endl;
          return;
      }
    
        动态库编写其实没什么特别的可以说的, 这属于另外一门课程了, 简单分别说两个基于linux和windows hello world级别的动态库编写.再次提一句jni.h头文件在你jdk目录的include的下
    1. linux, linux相对简单. 将include目录复制到你项目路径下. 编写上述的TestJNI.cpp. 在TestJNI.cpp同级目录下编译g++ -I 头文件路径 -shared -o libTestJNI.so. 笔者编译命令如下g++ -I /home/waxxd/jdk/include -shared -o libTestJNI.so, 如果没有g++ 可以百度安装一下,因为笔者的jdk在/home/waxxd/jdk下. 然后就可以使用了.
    2. windows, 笔者基于vs演示. 首先新建一个空项目, 进入后在解决方案视图右键项目-配置属性-常规-配置类型改为动态库确定, 同样配置属性-c/c++常规附加包含目录, 里面将你在windows平台的jdk环境下的include路径和include/win32路径设置进去. 在源文件新建TestJNI.cpp, 将aa_bb_TestJNI.h复制到头文件去.点击编译就完毕了, 如果不报错就可以在项目路径相应的下级目录下找到项目名称.dll文件.
  4. 使用
    public class Main {
        public static void main(String[] args) {
            System.load("动态库文件所在的全路径");
            String test = new TestJni().hello();
            System.out.println(test);
        }
    }
    
    不出意外你就成功了.

2. 动态注册

    其实可以发现刚才的代码挺繁琐的, 在java native方法hello要调用动态库, 动态库的方法必须使用Java_aa_bb_TestJni_hello这样的方法名称, 很长并且当动态库和java代码开发人员不同的时候, 协作很不方便, 所以就有了动态注册. 动态注册省略了javah生成头文件这一步, 其它和静态库类似. 在java 加载动态库的时候会自动执行JNI_OnLoad所以, 由动态库开发人员在动态库中完成映射.
    新建一个TestJNID文件代码如下:

#include "jni.h"
#include <iostream>
jstring stringTest(JNIEnv *env, jobject thiz)
{

    std::string hello = "hello wwww+++";
    return env->NewStringUTF(hello.c_str());
}
/*
typedef struct {
    char *name; 在java中native方法的名称
    char *signature; 方法签名括号内表示参数, 右侧表示返回值.
    void *fnPtr; 在c/c++中的函数
} JNINativeMethod;
*/
static const JNINativeMethod nativeMethods[] = {
    {"hello", "()Ljava/lang/String;", (jstring *)stringTest}};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    std::cout << "load TestJNID ... " << std::endl;
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK)
    {
        std::cout << "222" << std::endl;
        return -1;
    }
    jclass clazz = env->FindClass("aa/bb/TestJni");
    if (!clazz)
    {
        std::cout << "can not get class" << std::endl;
        return -1;
    }
    if(env->RegisterNatives(clazz, nativeMethods, 
    				sizeof(nativeMethods)/sizeof(nativeMethods[0]))) {
        std::cout << "register fail" << std::endl;
        return -1;
    }
    return JNI_VERSION_1_4;
}

java到c++有具体的类型映射, 这就是深入的内容了, 这里简单的入门helloworld就不介绍了.

3. 总结

入门JNI的hello world实际就介绍完了. 但其实在日常工作JNI调用还是比较麻烦的, 有一个很大的缺陷就是, Java和动态库开发人员必须配合工作, 在实际的工作情况下我们更多的是调用已有的动态库, 这个时候我们还需要编写一个中间的支持JNI的库, 进行函数或者数据类型相关的映射转化操作, 略显复杂, 并且不少Java开发人员是不具有c/c++动态库的编写能力, 读代码也比较费劲, 实际的工作中, 反正笔者一般用的是另外一个基于JNI封装的库JNA, JNA可以自动进行Java类型数据结构和C/C++类型数据结构的映射, 但运行效率比JNI低不少, 并且JNI可以调用Java代码, JNA貌似不可以. 改天有时间写一个JNA的介绍.

上一篇:5linux挂载


下一篇:2021-06-17