加载中...
看C++大叔如何拥 java 妹子入怀……
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
课文封面

在C++程序里,嵌入一个JVM(java 虚拟机),就可以把 java 代码编译后的 .class 文件当成 “脚本”语言来执行了。当然,前提是你的电脑上已经有安装 JVM 。

目标

假设有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++项目

无非就是要解决以下问题:

  1. 让C++编译器,能找到 JDK 提供的头文件(.h)在哪?库文件 (.so.lib)在哪?
  2. 让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库

注意到了吗?这库的名字就叫 “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 重新编译后,就可以运行了,结果如下:

在线程的java程序运行结果

意义?

C++程序内嵌(相对)简单、灵活且强大的另一门语言,能够赋给C++程序非常棒的功能,比如:

  • 不修改C++程序(这对上线的C++程序来说是很烦的事),直接改变某些功能(这些功能由内嵌的语言实现);
  • 公司里,C++程序员没有 Java 或其它程序员多时……(巧妙地转移工作量);
  • 第三方语言有更多丰富、成熟的库时……

事实上,C++程序内嵌其它语言这种做法很常见。我最早试的是 lua,但尴尬地发现,自己不太会 lua……后台又兴冲冲地内嵌了 Python ,结果,最大失望是:一个C++程序只内嵌一个 Python 虚拟机,次大需求是 Python GIL 带来的程序的那个大卡小卡…… 最后终于发现,原来 Java 才是最美的小妹,哦,最美的计算机编程语言。