写在前面:
本文本是想写个学习笔记供自己不时之需,但想了想既然都写了,不如就发出来好了,这也是笔者的第一篇博文,希望自己能坚持记录自己的学习笔记~
另,本文是基于笔者一天时间的查阅资料+测试而完成的,甚是稚嫩,定有许多不严谨以及不科学的描述,希望各位佬们不吝指教!
本文编写时,笔者尙读大二,冲刺做课程大作业时(还剩两天),因队友已有c++的源文件,想要在c++做个图形界面只知道能用QT,想不到别的简单的图形界面编写方法,于是萌生了使用Java的Swing(当时正在学,只会这个),来做程序的图形界面的想法。接着便查阅多方资料,但每篇文章都总有对于我这种跨语言实战经验为负数的小白十分不友好的地方(就是我看不懂的),因此在查阅资料上占据了大概60%的时间,总在似懂非懂面前盘旋,加之临时编写的C++和Java代码之间始终无法连通,更是怀疑继续坚持下去是否有用。但好才在经过了快一天的坚持之下,终于打通的测试代码,那一瞬间有如雷光一闪,欣喜若狂。
感谢你看了这么多废话,下面是正文了。
目录
写在前面:
笔者配置:
Java调用C++文件的逻辑梳理及思路:
详细步骤(可能过于详细)
笔者配置:
(如果读者使用VS相关产品,可以直接百度搜索教程,有很多)
Java IDE:Jetbrain Intellij IDEA 2021.2.2(Community Edition)
C++ IDE: Jetbrain CLion 2021.3
Java调用C++文件的逻辑梳理及思路:
以下内容分为形象版、抽象版和详细步骤(如有需要请点击超链接跳转,点击相应位置标题可以跳回此处)
(形象版)
①Java本身提供了一个工具包(Java动态接口,Java Native Interface,JNI)[1],专门用来为其他语言提供自己的规范,而Java与C++之间交流的通道,正是需要用到JNI才能实现。但这里需要注意的是JNI本身是Java对外的一份说明书,对外说明如何跟Java交流,并不能用它来约束Java本身来“妥协”其他平台(语言)的规范。
②有了JNI这份规范之后,我们要理清楚一下Java是如何从C++那里获得方法(函数)的。首先,C++要拿到这份规范(JNI),才能跟着Java的规范来实现自己的方法(函数)。也就是说,将C++“嵌入(不知道这么说恰不恰当)”Java程序,是需要“委屈”C++跟着Java的意思走的,毕竟要进人家门做客,还得遵从主人家的意思对吧。
③然后,Java对于C++来说还是个外乡人,Java的规范(JNI)世界公认,但是他听不懂Java的方法(函数)想要他做什么。Java想要C++帮他个忙,因此将自己的方法(.java文件)交给了翻译官Java虚拟机(Java Virtual Machine,JVM),然后翻译官翻译出了一份“二进制文件(.class)[2]”,这份文件是Java的世界语言(机器语言),由他写出,然后几乎所有的代码平台(语言)都可以获取他的内容,但是没有办法直接读它,但是,他们都可以翻译他从而知道他的意思。同时,Java还贴心地为C++翻译了一份C++能看懂的文件(.h),于是C++便拿了这份文件,装到自己的工具包里,当做规范来使用,也就能实现Java的意图了。
自此,Java对C++的传递已完成。
④接着C++在自己的工作室(.cpp)实现了Java的意图。
⑤但是,C++实现了方法,Java又看不懂了,这个世界语言(.class)C++又写不出来,咋办呢?诶,这时候有个万金油出现了,他叫动态链接库(Dynamic Link Library,DLL)他的功能很强大,虽然在这里出现有些杀鸡用牛刀,但是也不失为一种高效便捷的方法。DLL用某种办法,将C++的方法存到了他的库文件(.dll)中。这个库文件比较优秀,Java本地有工具(API)可以直接在几个熟悉的地方(环境变量%JAVA_HOME%)找到他,而只要C++导出的这个库文件放到Java熟悉的几个地方,就能够供Java使用。
⑥自此,Java和C++之间,关于这些方法的通道就彻底打通了。
我们将逻辑整理再简述一遍:
Java将自己的规范(.java)翻译成他的世界语言(.class)发布出来-,再翻译出一份文件(.h)交给C++使用。C++拿来理解这份文件(.h),然后在自己的工作室(.cpp)实现它。为了给Java读懂,C++将cpp文件交给动态链接库(DLL)处理,然后产生了一份.dll文件,供Java使用。接着,Java使用自己的小工具(API)拿到了.dll文件里的内容。
自此,Java将意图传给了C++,C++实现了意图又交还给Java,一次Java调用C++的过程就完成了。
什么?故事太泛了不明白具体需要什么?往下看
(抽象版)
①Java本身提供了一个Java动态接口(Java Native Interface,JNI),专门用来供多平台开发使用,而Java与C++之间交流的通道,正是需要用到JNI才能实现。
②要让C++实现Java声明的方法,需要提供有关于Java此方法的头文件,供C++进行内部实现,而头文件(.h)由Java交予JVM编译出的二进制文件(.class)生成。因此,需要将Java中声明了接口的文件(.java),即需要调用C++实现方法的文件,交给JVM进行编译。编译出的二进制文件(.class)再进行编译生成C++可以使用的头文件(.h)
③接着,生成的头文件在相应的.cpp文件中引入,并在.cpp文件中实现方法。
自此,Java对C++的传递已完成。
④然而C++无法原路返回生成.class的二进制文件。不过,Java内部提供了解析.dll文件的API,十分方便,只要将API需要调用的.dll文件放于 %JAVA_HOME% 路径(环境变量中可查)之下即可。因此,此时需要动态链接库(DLL)帮忙。C++通过DLL项目生成本方法的.dll文件。然后,将此.dll文件放于 %JAVA_HOME% 路径之下,该API调用时可以在这些路径里找到相应的.dll文件。自此C++对Java的传递也已完成。
接下来我们按照顺序,将Java调用C++的详细步骤梳理出来。
详细步骤(可能过于详细)
本流程是根据多方参考而得,每篇资料的顺序都是如此,笔者此处以百度百科所给的流程为准进行细节的增加和说明。笔者有对参考代码进行修改,但命名少有修改,将在文末引用参考作者的文案及网址,感谢各位作者的无私分享!如有侵权,笔者立即删除相关内容!
在此,笔者以最简单的HelloWorld输出作为例子进行讲解分析。
步骤一:编写Java类
首先我们需要建立一个Java文件,附带一个native类型的接口,接口名为displayHelloWorld。(图0)其中,native是JNI标准的关键字,表示本方法是由外部其他语言实现的,此处能且只能进行声明。displayHelloWorld是自定义的方法,用来供C++实现。读者可以参考本代码进行编写。(本代码详细说明在下方)
这里笔者给出代码:
package JavaCpp;
class HelloWorld {
public native void displayHelloWorld();
static {
System.loadLibrary("libJHelloWorld");
}
public static void main(String[] args) {
// System.out.println(System.getProperty("java.library.path"));
new HelloWorld().displayHelloWorld();
}
}
图0 Java文件内部声明
(①native是对外部实现接口的声明; ②处注意,此为声明,不是实现)
HelloWorld.java文件说明:
静态代码块中对dll文件引入的API(图1 ③处)可以先不写,对于他的作用下文会讲述到。
图1 DLL API所处的静态代码块
(③静态代码块中的DLL引入API)
本行,为笔者用来查找dll文件所应该放置的目录的代码,即查找环境变量的%JAVA_HOME%(图2 ④处)
图2 查找环境变量配置路径
(④可注释掉)
本行为调用native接口方法的代码,此时流程已结束(图3)
图3 调用由c++实现的代码接口
(⑤ 调用)
步骤二:使用javac命令编译所编写的Java类
Win+R打开cmd窗口,进入到上面编写的Java类(.java)所在的目录下(图4)
图4 目录进入示意
(①③为目录地址; ②为所编写的类)
输入javac【文件名】对上述类文件进行编译,即输入javac HelloWorld.java(图5)
图5 javac指令
(④为javac指令; ⑤生成成功后的样例)
再看到目录文件夹,生成了对应名字的二进制文件(.class)(图6)
图6 javac指令生成二进制文件成功
步骤三:然后使用javac -h生成扩展名为.h的头文件
输入javac -h【生成路径】【源文件名】生成头文件,即输入javac -h ./ HelloWorld.java(图7)
图7 javac -h指令
(⑥为javac -h指令; ⑦为生成文件储存路径; ⑧为目标文件)
生成成功之后,列表出现.h文件。值得注意的是,此头文件包含了我们Java包的名字,如果没有包将不会生成带包名的命名。(图8)
图8 javac -h指令生成头文件
步骤三点五:头文件内容解释:(图9)
①框内容是C++文件兼容C语言的宏定义。②处JNIEXPORT[4] 和 JNICALL[4] 是JNI标准指定的内容。以上内容有兴趣可看参考资料的链接[3]
图9 头文件内容
(①此处兼容C语言; ②头文件转换的Java接口函数)
此处为了引入方便,①处,笔者将JAVA目录下的include文件夹复制到了当前文件所在的文件夹下(include文件夹可以在java的安装目录中寻找)
②处是需要实现的头文件
#ifndef JHELLOWORLD_LIBRARY_H
#define JHELLOWORLD_LIBRARY_H
#include "include/jni.h"
#include "JavaCpp_HelloWorld.h"
JNIEXPORT void JNICALL Java_JavaCpp_HelloWorld_displayHelloWorld
(JNIEnv *, jobject);
#endif //JHELLOWORLD_LIBRARY_H
图10 需要实现的头文件内容
- jni.h路径 ②需要实现的头文件内容)
步骤四:使用C/C++实现本地方法
笔者使用的是CLion,如果读者使用的是visual studio系列的产品,可以直接在网络上搜索,有很多相关教程。
拷贝图9中②的函数,新建cpp文件进行内部实现。
笔者此处的内部实现为,输出“im from cpp”
#include "iostream"
#include "library.h"
#include "include/jni.h"
#include "JavaCpp_HelloWorld.h"
JNIEXPORT void JNICALL Java_JavaCpp_HelloWorld_displayHelloWorld
(JNIEnv *, jobject){
std::cout<<"im from cpp"<<std::endl;
}
图11 需要cpp实现文件内容
步骤五:生成相对应的动态链接库文件.dll
配置好CMakeLists.txt里面的内容[5],然后对library.cpp进行编译,即可生成相应的动态链接库文件(跟随目录找到lib【类名】.dll文件)
图12 生成的相对应的动态链接库文件
步骤六:将生成的dll文件放置到JAVA的环境变量中
图2处的方法即打印环境变量所在位置,当然你也可以用其他方法去了解环境变量配置的路径,图中这种方法比较便捷。(笔者已帮你复制过来了图2)
图2 查找环境变量配置路径
(④可注释掉)
笔者将此dll文件放入了如下路径处。(E:/JAVA/bin,如图13)
图13查找环境变量配置路径
步骤七:运行调用C++接口的JAVA程序
运行成功!显示出C++实现的打印内容“im from cpp”!(图14)
(另,图11对比图笔者已复制:)<-笑脸来的:)
图14 java文件运行成功
图11 需要cpp实现文件内容
至此,JAVA嵌入式调用C++的全过程已经结束咯!结果是JAVA成功地调用了C++实现的代码。
本操作可能略显繁琐,如有更好的方法,欢迎留言分享!
感谢您看到这里,您的支持是笔者最大的动力,咱们下篇再见!
参考资料:
[1]百度百科【JNI】:
JNI_百度百科
[2]百度百科【.class文件】:
class文件_百度百科
[3]博客园【关于extern "C"(详细剖析)】
关于extern "C"(详细剖析) - tenos - 博客园
[4]CSDN【【Android NDK 开发】JNI 方法解析 ( JNIEXPORT 与 JNICALL 宏定义作用 )
】
【Android NDK 开发】JNI 方法解析 ( JNIEXPORT 与 JNICALL 宏定义作用 )_让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 )-CSDN博客_jnicall jniexport
[5]CSDN【Clion生成动态链接库.dll】
Clion生成动态链接库.dll_superSmart_Dong的博客-CSDN博客_clion dll