由于工作需要,要写一段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之旅,希望对大家有帮助。
分享到:
相关推荐
在本文中,我们将深入探讨如何在Visual Studio 2019环境下使用C++通过Java Native Interface (JNI)来调用Java代码。JNI是Java平台的一部分,它为Java应用程序提供了与本地代码交互的能力,使得开发者可以将Java应用...
Java程序可以通过JNI调用本地方法,这些本地方法是用其他语言编写的,并通过JNI接口暴露给Java。这个过程涉及到以下几个步骤: 1. **创建本地方法声明**:在Java类中,你需要声明本地方法。这些方法没有具体实现,...
总结来说,这个"JNI DEMO"是一个完整的示例,展示了如何使用Java的JNI技术来调用C/C++编写的DLL。它包括了Java代码、JNI接口的实现、DLL的编译以及Java应用的运行。对于初学者,这是一个很好的实践教程,即使没有...
Android-JNI,全称为Java Native Interface,是一种技术,允许Java代码和其他语言编写的代码进行交互。这个完整工程展示了如何在Android应用中使用JNI,实现C++与Java之间的双向调用。这种技术在需要高效计算、利用...
通过以上步骤,我们可以实现C++通过JNI调用Java类的功能。这个过程涉及到了Java和C++的混合编程,需要对两种语言有深入的理解,并且熟悉JNI提供的接口。在实际应用中,开发者还需要考虑多线程、内存管理、错误处理等...
本教程将详细讲解如何在Visual Studio 2019环境下,利用C++通过Java Native Interface (JNI) 调用Java代码。首先,我们需要了解JNI的概念和作用。 JNI是Java平台提供的一种接口,允许Java代码和其他语言写的代码...
通过本文,我们可以了解 Java 和 C++ 之间的互相调用实例的实现过程,包括 C++ DLL 文件的创建和使用,以及 Java 代码中使用 JNI 技术调用 C++ DLL 文件的实现细节。这种技术可以应用于需要 Java 和 C++ 之间互相...
在提供的资源"使用C++创建java虚拟机JVM,使用JNI调用java函数"中,可能详细介绍了以上步骤的实际应用,包括代码示例和可能遇到的问题及解决方案。通过学习和实践这个教程,开发者可以更好地理解和掌握C++与Java之间...
7. **Java调用C++**:同样,Java代码也可以通过JNI调用C++函数。只需声明对应的`native`方法,然后在C++中实现。 总的来说,C++通过JNI与Java的相互调用在Cocos2dx 3.3中是一个关键的集成技术,它使得开发者能够...
JNI,全称Java Native Interface,是Java平台标准的一部分,它允许Java代码和其他语言写的代码进行交互。JNI在Android开发中扮演着重要的角色,特别是在需要高效计算或者利用硬件特性时,如游戏开发、图像处理和底层...
Java通过JNI调用C# DLL是一个跨平台、跨语言的技术实践,主要应用于需要利用Java的稳定性和C#的高性能场景。JNI(Java Native Interface)是Java平台标准的一部分,它允许Java代码和其他语言写的代码进行交互。C# ...
总结来说,Delphi 10.3通过JNI调用Java函数的过程涉及以下步骤: 1. 创建C++ DLL项目,引入JNI头文件。 2. 定义JNI函数,实现Delphi和Java之间的交互逻辑。 3. 在Delphi程序中加载DLL,获取JNI函数的指针。 4. 使用...
通过上述介绍,我们可以了解到如何在C++环境中通过JNI调用Java代码来实现用户身份验证和权限管理的功能。这种跨语言调用的方法不仅可以提高系统的灵活性,还能充分利用两种语言的优势。对于需要在C++环境中集成Java...
Java 通过使用 JNI(Java Native Interface)接口可以调用 C++ 动态库,该技术可以大大扩展 Java 的能力,让 Java 应用程序可以与其他语言编写的程序进行交互。下面是 Java 使用 JNI 接口调用 C++ 动态库的详细步骤...
总结起来,JNI调用C++代码的过程包括:在Java中声明本地方法、生成JNI头文件、编写C++实现、编译C++代码为动态链接库,以及在Java程序中加载和调用本地方法。这种方式在处理底层操作、高性能计算或者与硬件设备交互...
5. **Java端的JNI调用**:在Java代码中加载动态库,然后通过`System.loadLibrary()`方法调用相应的JNI函数。 在实际应用中,开发人员需要考虑兼容性问题,如不同操作系统下的动态库加载方式、32位与64位系统的差异...
利用JNI技术实现Java中调用C++编写的函数库示例程序源码,并附上参考JNI文档。...详情见本人博客:Java学习之通过JNI调用C/C++编写的dll链接库(图文教程)(http://write.blog.csdn.net/postlist)
JNI 将 Java 应用程序与 C++ DLL 文件集成需要以下步骤:创建一个 Java 工程,生成 `.class` 文件,使用 `javah` 命令生成 `.h` 头文件,创建一个 C++ 工程,并将生成的 `.h` 头文件复制到工程中,编写 C++ 代码,并...
在本主题中,我们将深入探讨如何使用C++通过JNI来调用Java中的方法,以及如何实现C++与JavaScript的互调。这在跨平台开发、性能优化或利用现有C/C++库时尤其有用。 首先,我们需要理解JNI的基本结构。JNI接口定义了...
JNI(Java Native Interface)是Java平台的标准接口,它允许Java代码和其他语言写的代码进行交互。在本主题中,我们将深入探讨如何在Windows和Linux环境中利用JNI来生成动态链接库(DLL for Windows,SO for Linux)...