目标
假设有Java组的同学写了一段代码:
public class Greeting {
public void SayHello() {
System.out.print("--你好 C++,我是Java。--\n");
}
}
保存在 Greeting.java 的文件里,然后使用 javac 编译:javac Greeting.java
, 得到 Greeting.class 。现在,“球”到了C++组的同学的脚下了,我们需要在一个C++程序里,执行这个 .class 文件,并在 C++ 程序的控制台(Windows下)或终端(Linux)下看到这一行:
--你好 C++,我是Java。--。
方案
下面我使用 Code::Blocks 为IDe,以 linux 环境为例,但也会说到Windows下的不同处,讲讲如何做。
步骤1 :准备 JVM 和 JDK
- JVM 是 “Java 虚拟机”,它负责执行 java 程序;
- JDK 是 Java 的开发包,它含有C或C++语言“二次开发 java”所需要的文件
JDK 提供的东西多数和Java语言自身无关,而是 C 语言的头文件和不同的操作系统的库,本质也是C语言的库,因为操作系统的接口通常使用C语言描述。
Java的“卖点”之一,就是跨平台,但前提是在不同的平台上,先安装(当然也是不同)的JVM及JDK。大家可以自行找资料(网上多的是)。
步骤2 :配置C++项目
无非就是要解决以下问题:
- 让C++编译器,能找到 JDK 提供的头文件(.h)在哪?库文件 (.so.lib)在哪?
- 让C++编译器(严格讲是链接器),把 JDK 里的 库文件链接到 C++ 程序里。
下面的内容,均使用 $(jdk) 代表你的电脑上安装的 JDK 的文件夹。
通常:
Linux: 此文件夹大概是:/user/lib/jvm/java-17-openjdk-amd64
Windows: 则大概是:C:\Program Files\Java\jdk1.8.0_241
需要加入 C++ 编译头文件搜索路径的有两项,先看 linux 下:
- $(jdk)/include
- $(jdk)/include/linux
在Windows 系统下,保留第一项,然后第二项需改为 :
- $(jdk)/include/win32
不同的IDE有不同的配置头文件搜索路径的方法,下面是 Code::Blocks 在Linux下的例子。
- 首先,配置项目路径变量:$(jdk)
假设我们的Code::Blocks项目名为 CPPCallJava,进入项目构建配置对话框后:
项目路径变量仅在当前项目里生效。如果你想把它配置为 Code::Blocks的全局路径(可在所有项目中生效)也可以。(《白话C++》中有很多配置全局路径变量的例子,线上视频课程:配置Code::Blocks全局路径变量)
- 然后,配置头文件搜索路径:
如前所述,如果是 Windows 环境,你需要把第二项路径中 “linux” 改为 “win32”。
- 再配置库搜索路径:
将上图中“Compiler”页,切换为其右边的“Linker”页,然后也添加两项:
- 最后配置需要链接的Java库:
注意到了吗?这库的名字就叫 “jvm” ,所以把它嵌入(链接)到一个C++的程序,是不是这个C++程序就拥有了自己的一个 java 虚拟机,于是就是可以执行指定的 java 代码(当然,得是编译好的结果,即 .class 文件)。
步骤3 :C++代码
Java代码我们在一开头就给了,并且编译好了,学过 Java 编程的同学可能会有点嘀咕:Java 程序的运行入口,不应该是一个静态类里的名为main的静态函数吗?上面给的java 代码,没有入口呀?
此情此景,C++忍不住要猥琐地笑了:“我都把她揽入怀里了,还要你们教我哪里是入口函数?”
下面就是完整C++代码。该代码要求所要执行的 java 代码,需放在本C++程序运行目录下的一个名为 java 的子文件夹中。
#include <cassert>
#include <iostream>
#include <jni.h>
struct JVMInfo
{
JavaVM* jvm;
JNIEnv* env;
JavaVMInitArgs vm_args;
#define VM_OPT_COUNT 3
JavaVMOption options[VM_OPT_COUNT];
JVMInfo()
: jvm(nullptr), env(nullptr)
{
// 第一个条件:不需要java编译器(因为我们已经编译好了测试用的java代码)
options[0].optionString = const_cast<char *>("-Djava.compiler=NONE");
// 第二个条件://classpath有多个时,用";"分隔,UNIX下以":"分割。
//这里,至少要包含前面java代码编译出来的Greeting.class文件所在路径
//根据我设置的相对路径,可以推出我的callJava 的C++工程和demo的Java工程所在位置的相对关系
options[1].optionString = const_cast<char *>("-Djava.class.path=./java");
// 第三个条件:用于跟踪运行时的信息
// "-verbose:jni" 换成这个,则jvm启动时,不会在屏幕上输出一堆信息
options[2].optionString = const_cast<char *>("-verbose:none");
// JNI版本号
vm_args.version = JNI_VERSION_10;
vm_args.nOptions = VM_OPT_COUNT;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
}
// 创建JVM
bool Create()
{
assert(!jvm && !env);
return 0 == JNI_CreateJavaVM(&jvm, (void **)(&env), &vm_args);
}
// 销毁JVM
void Destory()
{
if (jvm)
{
jvm->DestroyJavaVM();
jvm = nullptr;
env = nullptr;
}
}
void Demo()
{
assert(jvm && env);
auto test = [](bool condition, char const* error)
{
if (!condition)
{
std::cerr << error << std::endl;
}
return condition;
};
// 第1步: 找指定 class
jclass greetingClass = env->FindClass("Greeting");
if (!test(greetingClass, "Can't found java class 'Greeting'."))
{
return;
}
// 第2步:找 Greeting 类的构造函数
jmethodID greetingCtor = env->GetMethodID(greetingClass, "<init>", "()V");
if (!test(greetingCtor, "Can't found constructor for 'Greeting'."))
{
return;
}
// 第3步:通过构造函数,创建出一个 Greeting对象:
jobject greetingObject = env->NewObject(greetingClass, greetingCtor);
if (!test(greetingObject, "Can't create a object of 'Greeting'."))
{
return;
}
// 第4步:找到 Greeting 的 SayHello 方法:
jmethodID sayHello = env->GetMethodID(greetingClass, "SayHello", "()V");
if (!test(sayHello, "Can't found method 'SayHello()'."))
{
return;
}
// 最后:调用 对象 greetingObject 的 sayHello 方法:
env->CallObjectMethod(greetingObject, sayHello);
}
};
int main()
{
JVMInfo ji;
if (!ji.Create())
{
std::cerr << "Create JVM fail." << std::endl;
return -1;
}
ji.Demo();
ji.Destory();
}
运行结果:
更复杂的java代码?
把java程序自身依赖的外部库配置好,并且也走传统习惯走 java 的 main() 入口,大概都是能执行的。
下面给一个相对复杂的——其实就是带有线程的 java 代码:
public class Greeting {
private static class Task implements Runnable {
@Override
public void run() {
for (int i=0; i<10; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + "在打印 : " + i);
try {
Thread.sleep(1000);
}
catch(InterruptedException e) {
System.err.printf("线程 %s 睡眠时异常 %s。\n", Thread.currentThread().getName(), e.getMessage());
}
}
}
}
}
public void SayHello() {
System.out.print("--你好 C++,我是Java。--\n");
Thread trd = new Thread(new Task());
trd.setName("Java线程");
trd.start();
try {
trd.join();
}
catch (InterruptedException e) {
System.err.printf("等待 %s 结束发生异常 %s。\n", trd.getName(), e.getMessage());
}
}
}
把它替换掉上面的 Greeking.java,并记得用 javac 重新编译后,就可以运行了,结果如下:
意义?
C++程序内嵌(相对)简单、灵活且强大的另一门语言,能够赋给C++程序非常棒的功能,比如:
- 不修改C++程序(这对上线的C++程序来说是很烦的事),直接改变某些功能(这些功能由内嵌的语言实现);
- 公司里,C++程序员没有 Java 或其它程序员多时……(巧妙地转移工作量);
- 第三方语言有更多丰富、成熟的库时……
事实上,C++程序内嵌其它语言这种做法很常见。我最早试的是 lua,但尴尬地发现,自己不太会 lua……后台又兴冲冲地内嵌了 Python ,结果,最大失望是:一个C++程序只内嵌一个 Python 虚拟机,次大需求是 Python GIL 带来的程序的那个大卡小卡…… 最后终于发现,原来 Java 才是最美的小妹,哦,最美的计算机编程语言。