Java调用c++示例

一、背景

最近的一个任务是搜索索引数据生产重构,目前亿级以上的搜索引擎基本还是c++的(阿里除外),我们的也不例外,无论是分词还是写索引文件格式都是c++工具实现,所以这个系统之前也是hadoopStreaming调c++跑在调度系统上的。

任务目标也挺明确,就是压缩现在小时级的做库时间。这样对于业务修改上线、研发人员自测数据这样的生产效率,以及线上系统的可用性,都会有较大的提升。修改的做法简单粗暴,就是用flink或者spark来替换hadoop,减少shuffle以及多次mapReduce之间的持久化操作。

而可以使用的方式,包括JNI/JNA/JNR/SWIG,上层panama project的FFI。

最初做实验,就是将旧代码打包封装,看看直接用JNI调用,跑在充分利用内存的flink/spark上面,看会不会有比较好的收益,因为JNI虽然慢,但比起shuffle以及落盘的收益还是小到可以试一把的。本文就是介绍一下在这个过程中的java调用c++代码时的一些步骤。以下的例子不是业务代码,只是为了对比不同的实现的demo。

关于JNI为什么慢,也先列一下引用的stackoverflow上面的回答: Calling a JNI method from Java is rather expensive comparing to a simple C function call. HotSpot typically performs most of the following steps to invoke a JNI method:

  1. Create a stack frame.
  2. Move arguments to proper register or stack locations according to ABI.
  3. Wrap object references to JNI handles.
  4. Obtain JNIEnv* and jclass for static methods and pass them as additional arguments.
  5. Check if should call method_entry trace function.
  6. Lock an object monitor if the method is synchronized.
  7. Check if the native function is linked already. Function 1. lookup and linking is performed lazily.
  8. Switch thread from injava to innative state.
  9. Call the native function
  10. Check if safepoint is needed.
  11. Return thread to in_java state.
  12. Unlock monitor if locked.
  13. Notify method_exit.
  14. Unwrap object result and reset JNI handles block.
  15. Handle JNI exceptions.
  16. Remove the stack frame.
  17. The source code for this procedure can be found at SharedRuntime::generatenativewrapper.

criticalNative以及openJDK13才可能会有的panama项目的FFI,也尝试了一下。
下面具体用例子介绍一下。

二、JNI&JNA

  • JNI:Java Native Interface,提供了API实现了Java和其他语言的通信,让java可以调用别的语言。

  • JNA:Java Native Access,提供一组Java工具类用于在运行期间动态访问系统本地库而不需要编写任何N

  • 区别:jni使得我们可以在java与native语言间相互调用(主要还是c/c++)。jvm本身就是native语言实现的。但是java被jvm封装成了平台无关,所以要调native的话,需要有一个native的中间库,用特定的函数名和数据类型,作为中间调用方,再来调native的待使用方法。ative/JNI代码。但这样就搞复杂了,实践证明,凡是复杂的东西,就一定会有人把它简化,于是JNA就出现了。通过JNA的封装,java就可以直接访问动态链接库中的函数而不需要新写一个中间的动态链接库,简化了java调native的工作量。但简单也意味着损失,JNA一般只适用于较为简单的C/C++库,并且只提供了C/C++对Java的接口转化而无法使用java的特性。

  • 具体的类型映射可以参考:jni types以及 JNI Data Type Mapping to C/C++

2.1 JNA

写上c++代码: jnacost.cc,打动态库: g++ -fPIC jnacost.cc -shared -o libjnacost.so, 注意加-fPIC以及linux下用lib*.so这个前后缀

extern "C" {  
    long jnaTransform(const char* s) {
        long num = 0;
        int i = 0;
        while (s[i] != '\0') {
            num += s[i++] - '0';
        }
        return num;
    }
}

在linux上打出动态库,记得lib+类名+后缀规则(win是dll,linux是so,mac是lib) 再编译java的 加入jar引用

  <dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.3.1</version>
  </dependency>

直接在本地写:JNADemo.java, 注意一下有一个写死在代码中的路径,用的时候换成自己的,编译: javac -cp /export/home/zhaowei/.m2/repository/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar:. JNADemo.java
-cp指定了jna的jar包,注意cp后面记得带一个:.(javac可能不需要不过java命令需要),

import com.sun.jna.Library;  
import com.sun.jna.Native;

import java.util.ArrayList;  
import java.util.List;

public class JNADemo  {  
    /** str num */
    private static final int STRING_NUM = 1000000;
    /** str len */
    private static final int STRING_LEN = 1000;

    /** mock strs with STRING_LEN chars */
    private static List<String> mockList() {
        List<String> strs = new ArrayList<>(STRING_NUM);
        for (int i = 0; i < STRING_NUM;i++) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < STRING_LEN; j++) {
                sb.append((char) ('1' + (j % 9)));
            }
            strs.add(sb.toString());
        }
        return strs;
    }

    /**
     * simple s transform
     * @param s
     * @return
     */
    private static long transform(String s) {
        long num = 0;
        for (int i = 0; i < s.length(); i++) {
            num += s.charAt(i) - '0';
        }
        return num;
    }

    /**
     * compute java javaCost
     * @return
     */
    private static long javaCost() {
        List<String> strs = mockList();
        long start = System.currentTimeMillis();
        for (String s: strs) {
            transform(s);
        }
        long end = System.currentTimeMillis();
        return (end - start);
    }


    /**
     * jna library
     */
    public interface MyCLibrary extends Library {
        MyCLibrary instance = (MyCLibrary) Native.load("/export/home/zhaowei/workspace/rtap_searchdata/jna/libjnacost.so", MyCLibrary.class);
        long jnaTransform(String s);
    }

    /**
     * jna cost
     * @return
     */
    private static long jnaCost() {
        List<String> strs = mockList();
        MyCLibrary instance = MyCLibrary.instance;
        long start = System.currentTimeMillis();
        for (String s: strs) {
            instance.jnaTransform(s);
        }
        long end = System.currentTimeMillis();
        return (end - start);
    }

    public static void main(String[] args) {
        System.out.println(javaCost());
        System.out.println(jnaCost());
    }
}

执行: java -cp /export/home/zhaowei/.m2/repository/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar:. JNADemo

多次执行耗时: (java:native) 840:9223,938:9491,864:9182

2.2 JNI

比JNA复杂一些,如下: jnicost.h

#include <jni.h>

#ifndef _Included_JNIDEMO
#define _Included_JNIDEMO
#ifdef __cplusplus
extern "C" {  
#endif
    JNIEXPORT long Java_JNIDemo_jnicost(JNIEnv *, const char* s);
    //JNIEXPORT void JavaCritical_JNIDemo_jnicost(int i);

#ifdef __cplusplus
}
#endif
#endif

jnicost.cc

#include <jni.h>
#include "jnicost.h"

JNIEXPORT long Java_JNIDemo_jnicost(JNIEnv *env, const char* s) {  
    long num = 0;
    int i = 0;
    while (s[i] != '\0') {
        num += s[i++] - '0';
    }
    return num;
}

编译: g++ -fPIC jnicost.cc -shared -o libjnicost.so -I /export/home/zhaowei/software/jdk1.8.051/include -I /export/home/zhaowei/software/jdk1.8.051/include/linux

需要注意加上-I否则jni的头文件会找不到.

mac下会有不一样:
g++ -fPIC -dynamiclib jnicost.cc -o libjnicost.jnilib -I /Library/Java/JavaVirtualMachines/jdk1.8.0131.jdk/Contents/Home/include -I /Library/Java/JavaVirtualMachines/jdk1.8.0131.jdk/Contents/Home/include/darwin/,注意名字-shared/-dynamiclib, so/jnilib的区别

JNIDemo.java:

import java.util.ArrayList;  
import java.util.List;

public class JNIDemo {  
    static {
        System.out.println( System.getProperty("java.library.path"));
        System.loadLibrary("jnicost"); // Load native library at runtime
    }
    /** str num */
    private static final int STRING_NUM = 1000000;
    /** str len */
    private static final int STRING_LEN = 1000;

    /** mock strs with STRING_LEN chars */
    private static List<String> mockList() {
        List<String> strs = new ArrayList<>(STRING_NUM);
        for (int i = 0; i < STRING_NUM;i++) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < STRING_LEN; j++) {
                sb.append((char) ('1' + (j % 9)));
            }
            strs.add(sb.toString());
        }
        return strs;
    }

    private static native long jnicost(String s);

    public static void main(String[] args) {
        List<String> strs = mockList();
        long start = System.currentTimeMillis();
        for (String s: strs) {
           jnicost(s);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

注意点: c++函数名是Javapackagep1p2Classname_function,这里的点号都用下划线代替,我这边为了省事没有写包名,所以只有类名+方法名。

编译:javac -cp . JNIDemo.java

执行:java -Djava.library.path=/export/home/zhaowei/workspace/rtap_searchdata/jna/ JNIDemo

多次执行耗时: 178,160,148,212,193

三、CriticalNative

几个注意点:

  • 某些虚拟机才会有,属于一个未公开的功能。一般hotstop1.8是有,官方说是JRE7之后的才会有。
  • java中的是一个静态的方法,之前jni可以是非静态也可以是静态的。
  • .h文件的函数名的头不一样,参数也少了env参数;另外原来的也需要保留别删除
  • 并不会一上来就执行criticalNative的方法,而是先执行一下旧的,再执行新的,由次数来决定,可以打印log验证
  • 整个函数成为临界区,会阻碍垃圾回收的进行
  • 参数中不能有对象、对象数组或多维数组

jnicost.h

#include <jni.h>

#ifndef _Included_JNIDEMO
#define _Included_JNIDEMO
#ifdef __cplusplus
extern "C" {  
#endif
    JNIEXPORT long Java_JNIDemo_jnicost(JNIEnv *, const char* s);
    JNIEXPORT long JavaCritical_JNIDemo_jnicost(const char* s);

#ifdef __cplusplus
}
#endif
#endif

jnicost.cc

#include <jni.h>
#include "jnicost.h"

 JNIEXPORT long JavaCritical_JNIDemo_jnicost(const char* s) {
    long num = 0;
    int i = 0;
    while (s[i] != '\0') {
        num += s[i++] - '0';
    }
    return num;
 }

JNIEXPORT long Java_JNIDemo_jnicost(JNIEnv *env, const char* s) {  
    long num = 0;
    int i = 0;
    while (s[i] != '\0') {
        num += s[i++] - '0';
    }
    return num;
}

编译和执行java程序不需要变动,复用上面的例子 多次执行耗时: 198,168,155,153,180

四、openJDK13 panama

4.1 文档

4.2 简单示例

目前Point这个参数传递我还没有弄清楚,先给一下和上面不太一样的例子,后续弄明白了可以传参了我再进行修改

panama.h

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {  
#endif
    long test(int i);
#ifdef __cplusplus
}
#endif
#endif


panama.cc

#include"panama.h"
long test(int i) {  
    long num = 0;
    i = 0;
    char s[] = "123";
    while (s[i] != '\0') {
        num += s[i++] - '0';
    }
    return num;
}

编译打动态库包: g++ -fPIC panama.cc -shared -o libpanama.so

和jextract来生成native对应的jar包,注意一下这个-l 与-L , 如果有这样的库,需要再-L /usr/lib

jextract  -C c++ -t test  -o panama.jar -l panama -L . panama.h  

jextract --helps可以查看参数信息。这个-l表示是:specify a library, lib+(-l).so这样拼接,表示libpanama.so这个库,-L .表示在当前目录下找libpanama.so这个动态库, -t是表示打的jar包的类会在哪个package下面。

执行完了会生成panama.jar,包内容如下:

 test/panama_h.class
 test/panama.class
 META-INF/jextract.properties

然后就可以在java类中调用这个jar了:

Jnitest.java:

import java.lang.invoke.*;  
import java.foreign.*;  
import java.foreign.memory.*;  
import java.util.ArrayList;  
import java.util.List;  
import static test.panama_h.*;

public class Jnitest{  
    private static final int STRING_NUM = 100000;
    private static final int STRING_LEN = 10000;
    private static List<String> mockListStr() {
        List<String> strs = new ArrayList<>(STRING_NUM);
        for (int i = 0; i < STRING_NUM;i++) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < STRING_LEN; j++) {
                sb.append((char)('a' + j % 26));
            }
            strs.add(sb.toString());
        }
        return strs;
    }

    public static void main(String[] args) {
        List<String> strs = mockListStr();
        long start = System.currentTimeMillis();
        for (String s: strs) {
            test(0);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

在import static test.panama_h.*;后,可以直接调用test(0)来调用native的代码了 编译执行:

javac -cp .:./jnicost2.jar Jnitest.java

java -cp "panama.jar:." -Djava.library.path=/export/home/zhaowei/workspace/rtapsearchdata/foreign/foreigntest Jnitest

就得到java通过ffi调用native的结果了


参考资料:

comments powered by Disqus