`
jiangwenfeng762
  • 浏览: 285836 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

c++ 通过JNI技术调用java代码使用总结

阅读更多

 

由于工作需要,要写一段c++代码来调用java的api。下面把实现和调研的过程总结出来。

 

1. 如何解决?

首选JNI,首先对JNI的原理和使用方法简单调研一下,JNI的权威资料是:

http://java.sun.com/docs/books/jni/html/jniTOC.html

 

简单点说,JNI可以帮助我们解决两个问题:

1)实现java代码调用其他代码(c,c++,...)

大致的做法:

a)写java 类

 

 class HelloWorld {
     private native void print();
     public static void main(String[] args) {
         new HelloWorld().print();
     }
     static {
         System.loadLibrary("HelloWorld");
     }
 }

 

b)把需要由其他语言实现的逻辑在java类中用native关键字标示(注意native 方法只能有声明,不能有实现)

 

private native void print();

 

c)javac编译java类,获得对应的class文件

javac HelloWorld.java

 

d)javah -jni 生成本地方法对应的头文件

javah -jni HelloWorld

 

查看生成的头文件,核心部分是:

 

 JNIEXPORT void JNICALL 
 Java_HelloWorld_print (JNIEnv *, jobject);

 

e)编写本地方法的代码(c或者c++代码)

 

 #include <jni.h>
 #include <stdio.h>
 #include "HelloWorld.h"
 
 JNIEXPORT void JNICALL 
 Java_HelloWorld_print(JNIEnv *env, jobject obj)
 {
     printf("Hello World!\n");
     return;
 }

 

 

f)编译本地方法,生成可执行文件

 

cc -G -I/java/include -I/java/include/solaris 
     HelloWorld.c -o libHelloWorld.so

 

cl -Ic:\java\include -Ic:\java\include\win32 
     -MD -LD HelloWorld.c -FeHelloWorld.dll

 

 

g)运行java代码,你会发现c写的本地方式实现被成功调用。

 

2)实现其他代码(比如c++)调用java代码(这正是我要解决的问题)

 

jni是双向的,java可以调用其他语言实现的native方法,同样其他语言同样可以调用java代码。如何做呢?由于java代码必须(一定)要运行在JVM中,因此其他语言要调用java代码,首先必须启动一个JVM实例,然后再通过JNI规范中给出的一些方法(具体参考上面给出的url),来实现对java代码的调用。

 

具体做法,以我实际解决的问题为例吧:

a)我要调用的java API是:

 

 

public class API
{

  public static byte[] read(String ip, String path, int maxlen)
    throws XiheWorkerCommException, FileNotFoundException, IOException{
..........
    }
}

 

 b)设计我的c++类(某些涉及公司保密的组件没有贴出来)

 

#ifndef LOG_VIEW_BRIDGE_H_
#define LOG_VIEW_BRIDGE_H_
#include "jni.h"
class LogViewAdaptor
    {                                                                
        private:
            LogViewAdaptor();                                        
            
            LogViewAdaptor(const LogViewAdaptor&);                   
            
            LogViewAdaptor& operator= (const LogViewAdaptor&);       
            
		//启动虚拟机
            static void BeginJVM();                                                                 
              //创建虚拟机实例                                                       
            static int CreateJVM(JavaVM **jvm, JNIEnv **env, std::vector<std::string>& opts);
               //输出java异常信息                                                      
            static void PrintJNIErrorStack(JNIEnv *env);             
             //最重要。通过它可以调用JNI的各个方法                                                        
            static JNIEnv* env;                                      
                                                                     
            static JavaVM* jvm;
 
        public:
		//核心方法
            static void ReadLog(const std::string& ip,const std::string& path,unsigned int logLength,std::vector<char>&);
    }; 

#endif

 

 

c)实现static int CreateJVM(JavaVM **jvm, JNIEnv **env, std::vector<std::string>& opts)

 

int LogViewAdaptor::CreateJVM(JavaVM **jvm, JNIEnv **env, vector<string>& opts)
{   
    JavaVMInitArgs vm_args;
    JavaVMOption* options = new JavaVMOption[opts.size()];
    for (size_t i = 0; i < opts.size(); ++i)
    {   
        options[i].optionString = (char*) opts[i].c_str();
        LOG_INFO(sLogger,("JVM Opt",options[i].optionString));
    }   
    vm_args.version = JNI_VERSION_1_6;
    vm_args.options = options;
    vm_args.nOptions = opts.size();
    vm_args.ignoreUnrecognized = JNI_TRUE;


    return JNI_CreateJavaVM(jvm, (void**) env, &vm_args);
} 

 

 

 创建虚拟机实例的核心方法是JNI_CreateJavaVM,第三个参数类型是JavaVMInitArgs ,提供虚拟机参数,有关java虚拟机参数的介绍网上随处可见。

只需注意的是你必须指定清楚,你创建的jvm实例到哪里去加载你要调用的java class,即必须设置-Djava.class.path这个参数。

还有一点需要注意的是vm_args.version = JNI_VERSION_1_6,在jni.h中一共有三个参数,分别是JNI_VERSION_1_2,JNI_VERSION_1_4和

JNI_VERSION_1_6,我用的jdk是1.6的,所以就设置为JNI_VERSION_1_6。

 

 

JNI_CreateJavaVM方法的返回值是整数,负数代表创建失败,非常杯具的是,除了这个整数返回值外,没有任何log信息(也许是我没有找到),导致定位问题非常困难。

-1:代表虚拟机初始化失败。

其他值还需要再调查。

我在调式这段代码时遇到的问题是:由于LD_LIBRARY_PATH中指向了错误的libjvm.so,导致创建虚拟机失败,总是返回-1. 正确的libjvm.so必须是${JAVA_HOME}/jre/lib/amd64/server/下的libjvm.so。 而且不要copy它到别的目录,否则还是报错。

 

d)CreateJVM方法被BeginJVM方法调用,BeginJVM方法如下:

void LogViewAdaptor::BeginJVM()
{
    if(jvm == NULL)
    {
        ScopedLock lock(mJvmLock);
        if(jvm == NULL)
        {
            vector<string> opts;
            opts.push_back("-Xmx128m");
            opts.push_back("-Xmn64m");
        ScopedLock lock(mJvmLock);
        if(jvm == NULL)
        {
            vector<string> opts;
            opts.push_back("-Xmx128m");
            opts.push_back("-Xmn64m");
            opts.push_back("-Xss128k");
            opts.push_back("-Djava.compiler=NONE");
            opts.push_back("-Djava.class.path=.:./xihe_api/xapi4odps.jar:/xihe_api/");
            opts.push_back("-verbose:jni");
            int ret = CreateJVM(&jvm,&env,opts);
            if(ret < 0)
            {
                LOG_INFO(sLogger,("CreateJVM","Failure")("Ret",ret));
                APSARA_THROW(OdpsException,"JNI: Could not create the Java virtual machine.");
            }
            else
            {
                LOG_INFO(sLogger,("CreateJVM", "Success"));
            }
        }
        else
        {
            LOG_INFO(sLogger,("CreateJVM","jvm Not NULL"));
            jvm->AttachCurrentThread((void **) &env, NULL);
        }
    }
    else
    {
        LOG_INFO(sLogger,("CreateJVM","jvm Not NULL"));
        jvm->AttachCurrentThread((void **) &env, NULL);
    }
}

 这里需要特别注意的是 jvm->AttachCurrentThread((void **) &env, NULL);方法的调用。

首先搞清楚AttachCurrentThread方法的作用,以下是jni规范的引用:

JNI规范写道
jint AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args);

Attaches the current thread to a Java VM. Returns a JNI interface pointer in the JNIEnv argument.

Trying to attach a thread that is already attached is a no-op.

A native thread cannot be attached simultaneously to two Java VMs.

When a thread is attached to the VM, the context class loader is the bootstrap loader.

 我们的应用都是多线程的,用户的两次请求和可能是不同的线程,因此必须调用此方法把当前线程attach到vm中,才可以让jvm work。这一点非常重要。

 

 

 

e)创建好了JVM实例,就可以调用java的方法了,先把代码贴出来:

 

    jclass clazz= env->FindClass(API_PATH);
    if(clazz == NULL)
    {
        PrintJNIErrorStack(env);
        APSARA_THROW(OdpsException,"not found api class:"+string(API_PATH));
    }

    jmethodID mid = env->GetStaticMethodID(clazz,"read","(Ljava/lang/String;Ljava/lang/String;I)[B");
    if(mid == NULL)
    {
        PrintJNIErrorStack(env);
        APSARA_THROW(OdpsException,"not found method:read");
    }

    LOG_INFO(sLogger,("CallJavaAPI","Begin"));
    int curTime = time(NULL);
    jstring jip = env->NewStringUTF(ip.c_str());
    jstring jpath = env->NewStringUTF(path.c_str());
    jint jlogLength = logLength;
    jbyteArray obj = (jbyteArray)env->CallStaticObjectMethod(clazz,mid,jip,jpath,jlogLength);
    int endTime = time(NULL);
    LOG_INFO(sLogger,("CallJavaAPI","End")("Cost",boost::lexical_cast<string>(endTime-curTime)+"s"));
    if (env->ExceptionCheck())
    {
        PrintJNIErrorStack(env);
        APSARA_THROW(OdpsException,"exception occured in java.");
    }
    char* data = (char*)env->GetByteArrayElements(obj, 0);
    size_t len = strlen(data);
    for(size_t i = 0; i<len; i++)
    {
        container.push_back(data[i]);
    }

  

 

以上代码还是很清晰的:

a)首先找到java的class

jclass clazz= env->FindClass(API_PATH);
在jni中,有一套类型对应java的类型,具体参见规范。这里java的class类型 对应jni的jclass

 

b)找到了类,就接着找对应的方法,由于java API的方法是static的,所以这里调用
jmethodID mid = env->GetStaticMethodID(clazz,"read","(Ljava/lang/String;Ljava/lang/String;I)[B");
来找静态方法。第一个参数是class,第二个是方法名,第三个是该方法的字节码信息。为了正确地搞清楚方法的字节码可以
使用javap -c 来查看。
(Ljava/lang/String;Ljava/lang/String;I)[B 表明该方法有三个参数,分别是String,String和int,返回类型是byte数组。

L代表类型是java的完整路径;I代表int,[:代表数组,B:代表byte。更详尽的信息请参看spec

 

c)找到了方法,接下来就call呗。

jstring jip = env->NewStringUTF(ip.c_str());

jstring jpath = env->NewStringUTF(path.c_str());

jint jlogLength = logLength;

jbyteArray obj = (jbyteArray)env->CallStaticObjectMethod(clazz,mid,jip,jpath,jlogLength);

 

这里要注意的是,首先要把c++的类型转换为jni的类型。

 

另外,我的java方法返回的是byte[],而jni并没有对应的CallStaticByteArrayMethod,找了半天发现CallStaticObjectMethod可用。

 

jni的方法命名很有规律,Call<Static><ReturnType>Method.

 

CallStaticObjectMethod方法返回的是jobject类型,而

jbyteArray 是其子类,所以这里强转。接下来就是如何把jni的jbyteArray转换为c++的vector<char>了。

char* data = (char*)env->GetByteArrayElements(obj, 0);

size_t len = strlen(data);

for(size_t i = 0; i<len; i++)

{

container.push_back(data[i]);

}

 

另外还需要注意:

JNI规范指出,一个进程内只能开启一个JVM实例,否则就会报错。因此你必须确保JavaVM* jvm 在进程内绝对唯一。做法有singleton,static等。我采取static做法,但是我在实际测试中却发现在一个进程内重复调用两次ReadLog方法(具体做法是在UT只中运行两个case,每个case都会调用ReadLog方法,而UT的两个case是在一个进程内执行的)时,后面的一次会失败,而且非常严重,直接这样了:“A fatal error has been detected by the Java Runtime Environment”。为啥呢?我个人的分析(可能不对):我们知道一个JVM实例是在他运行的main方法退出时,自动也就结束了。第一次调用ReadLog方法结束后,当前进程启动的JVM应该也结束了,因为它已经没啥事可干了。那紧接着第二次调用ReadLog时,BeginJVM方法判断jvm这个指针 != NULL,于是它不再创建JVM了,但是这个jvm实例实际已经不可用了。以上是我的分析,有点武断,欢迎大家讨论,解惑。

 

以上的分析是完全错误的!

jvm何时退出?jni启动的jvm实例是作为子进程存在的,只要主进程仍然存在,那么jvm子进程就存在,不会退出,因此我们才不需要多次创建jvm实例。

小结

回过头看,使用jni也没啥难的,就是有些繁琐,需要好好看看jni的spec。以上是我的第一次jni之旅,希望对大家有帮助。


 

分享到:
评论
2 楼 lee091201 2013-10-09  
如果Java函数中使用了第三方jar包,c++通过jni调用的时候需要注意哪些?
1 楼 舒马赫 2013-04-19  
不错不错,很实用

相关推荐

Global site tag (gtag.js) - Google Analytics