使用 GraalVM 实现多语言 inception。为什么?因为它很有趣🏄
让我们玩得开心点
结论
最初发表于deepu.tech。
你听说过GraalVM吗?如果没有,你应该去了解一下。这是一项令人兴奋的技术,你知道,它能让一个多语言开发者兴奋不已 😉
来自网站:
GraalVM 是一个通用虚拟机,用于运行用 JavaScript、Python、Ruby、R、基于 JVM 的语言(如 Java、Scala、Groovy、Kotlin、Clojure)以及基于 LLVM 的语言(如 C 和 C++)编写的应用程序。
GraalVM 正是其中的佼佼者。它是 Oracle 开发的一款多语言虚拟机,除了多语言功能外,它还被证明具有相当高的性能和较小的内存占用。它支持构建原生镜像,一些现代 Java 微服务框架(例如Micronaut和Quarkus)也支持 GraalVM,因为它启动速度更快、占用空间更小,非常适合微服务架构。
那么 GraalVM 有哪些功能呢?让我们快速了解一下
GraalVM 功能
- JDK 替代品的下降——一些基准测试显示,GraalVM 比其他 JDK 供应商更快、内存占用更少,我个人还没有运行过任何基准测试
- 引入 NodeJS 替代品- 使用 GraalVM 代替 V8 作为 NodeJS 引擎
- Ruby和R的运行速度比默认实现更快
- 提前 (AOT) 编译的原生镜像
- 多语言能力——Java(任何 JVM 语言)、JavaScript、Ruby、Python、R、C/C++/Rust(LLVM)以及语言互操作性
- Truffle 语言实现框架,用于实现第三方语言支持
安装 GraalVM
开始之前,我们先来设置一下 GraalVM。我使用了 SDKMAN,如果你想手动安装,也可以参考这个。
- 如果你还没有安装SDKMAN,请先安装
sdk list java
# you can use a newer version if available
sdk install java 19.3.1.r11-grl
sdk use java 19.3.1.r11-grl
# Check everything
java -version
node -v
lli --version
以上命令将安装 GraalVM 并将其设置为java
、node
和lli
上下文。请注意:如果您启动新的终端会话,则必须sdk use java 19.3.1.r11-grl
再次运行。
- 安装 LLVM 工具链、Python 和 Ruby 支持
gu install llvm-toolchain
export LLVM_TOOLCHAIN=\$(lli --print-toolchain-path)
gu install python
gu install ruby
- 安装 Rust
curl https://sh.rustup.rs -sSf | sh
就这样,我们准备出发了!
让我们玩得开心点
作为一名多语言开发者,GraalVM 对我来说非常有吸引力,因为我可以结合使用多种我喜爱的语言,并充分利用每种语言的精髓。让我们来探索 GraalVM 提供的多语言功能。请注意,它对 Python、Ruby、R 和 Rust 的支持仍处于试验阶段,因此您的体验可能会有所不同。今天我们将使用 Java、JavaScript、Ruby、Rust、Python 和 C++ 构建一个程序。
我也想用 Rust 和 Go。Rust 主要通过 GraalVM
lli
命令行运行,但在多语言模式下嵌入时有很多限制。经过一番折腾,我终于让它运行起来了。对于 Golang,或许可以用Go LLVM 编译器(如这里所示)来实现,但我尝试的时候也遇到了一些问题。所以我暂时放弃了 Golang。如果你们有人成功了,请告诉我。
我们将用 Java 编写一个简单(有点傻)的应用程序,该应用程序在 Java 内部为每个步骤组合来自不同语言的方法
- Python:从给定的输入数组中过滤出斐波那契数
- JavaScript:找出上一步输出数组中每个数字的立方
- C++:获取上一步输出数组中数字的总和
- Rust:求上一步得到的数字的立方根
- Ruby:查找上一步中数字的阶乘
- Java:最后打印结果(这也是包装程序)
如果您喜欢更复杂的例子,请查看这个。
步骤 1:Java
让我们从 Java 包装程序开始Polyglot.java
import java.io.*;
import org.graalvm.polyglot.*;
class Polyglot {
// We create a polyglot context to evaluate source files
static Context polyglotCtx = Context.newBuilder().allowAllAccess(true).build();
// Utility method to load and evaluate a source file
static Value loadSource(String language, String fileName) throws IOException {
File file = new File(fileName);
Source source = Source.newBuilder(language, file).build();
return polyglotCtx.eval(source);
}
// Utility method to convert arrays between languages
static int[] getIntArrayFromValue(Value val) {
int[] out = new int[(int) val.getArraySize()];
if (val.hasArrayElements()) {
for (int i = 0; i < val.getArraySize(); i++) {
out[i] = val.getArrayElement(i).asInt();
}
}
return out;
}
public static void main(String[] args) throws IOException {
int[] input = new int[] { 4, 2, 8, 5, 20, 1, 40, 13, 23 };
/* PYTHON: Get the Fibonacci numbers from the array */
loadSource("python", "pythonpart.py");
Value getFibonacciNumbersFn = polyglotCtx.getBindings("python").getMember("getFibonacciNumbers");
int[] fibNumbers = getIntArrayFromValue(getFibonacciNumbersFn.execute(input));
/* JAVASCRIPT: Find cube of numbers in the output array */
loadSource("js", "jspart.js");
Value findCubeOfNumbersFn = polyglotCtx.getBindings("js").getMember("findCubeOfNumbers");
int[] sqrNumbers = getIntArrayFromValue(findCubeOfNumbersFn.execute(fibNumbers));
/* C++: Get the sum of the numbers in the output array */
loadSource("llvm", "cpppart");
Value getSumOfArrayFn = polyglotCtx.getBindings("llvm").getMember("getSumOfArray");
int sum = getSumOfArrayFn.execute(sqrNumbers, sqrNumbers.length).asInt();
/* Rust: Find the cube root of sum */
Value cubeRootFn = loadSource("llvm", "rustpart.bc").getMember("cube_root");
// println! macro does not work from Rust when embedded, some issue with mangling
System.out.println("Rust => Find cube root of the number");
Double cubeRoot = cubeRootFn.execute(sum).asDouble();
/* RUBY: Find factorial of the number */
Value factorialFn = loadSource("ruby", "rubypart.rb");
long out = factorialFn.execute(cubeRoot).asLong();
System.out.println("Sum: " + sum);
System.out.println("Cube Root: " + cubeRoot);
System.out.println("Factorial: " + out);
}
}
实用函数是为了简化代码,现在让我们看看它组成函数的每个步骤。
第 2 步:Python
我们正在执行getFibonacciNumbers
位于文件中的函数pythonpart.py
并将我们的输入数组传递给它。
/* PYTHON: Get the Fibonacci numbers from the array */
loadSource("python", "pythonpart.py");
Value getFibonacciNumbersFn = polyglotCtx.getBindings("python").getMember("getFibonacciNumbers");
int[] fibNumbers = getIntArrayFromValue(getFibonacciNumbersFn.execute(input));
让我们看一下pythonpart.py
,它是一个标准的 Python 程序,它接受一个数组并从中过滤出斐波那契数并返回结果数组。
import math
def isPerfectSquare(num):
n = int(math.sqrt(num))
return (n * n == num)
# Function to check if the number is in Fibonacci or not
def getFibonacciNumbers(array):
print("Python => Filtering Fibonacci number from the array");
out = []
n = len(array)
count = 0
for i in range(n):
if (isPerfectSquare(5 * array[i] * array[i] + 4) or
isPerfectSquare(5 * array[i] * array[i] - 4)):
out.append(array[i]);
count = count + 1
if (count == 0):
print("None present");
return out
步骤3:JavaScript
我们正在执行findCubeOfNumbers
位于文件中的函数jspart.js
并传递 Python 函数的结果。
/* JAVASCRIPT: Find cube of numbers in the output array */
loadSource("js", "jspart.js");
Value findCubeOfNumbersFn = polyglotCtx.getBindings("js").getMember("findCubeOfNumbers");
int[] sqrNumbers = getIntArrayFromValue(findCubeOfNumbersFn.execute(fibNumbers));
让我们看一下jspart.js
,它是一个标准的 JavaScript 函数,它接受一个数组,并映射元素并返回该数组。但是我们必须调用Array.prototype.map.call
而不是 ,array.map
因为 Java 传递的数组不是标准的 JS 数组。
function findCubeOfNumbers(array) {
console.log("JS => Getting cube of numbers in the array");
return Array.prototype.map.call(array, (it) => Math.pow(it, 3));
}
步骤4:C++
我们正在执行getSumOfArray
位于cpppart
二进制文件中的函数。我们在这里传递 JS 函数的结果和数组的长度。与 Python、Ruby 和 JavaScript 等解释型语言不同,我们必须使用编译后的二进制文件。
/* C++: Get the sum of the numbers in the output array */
loadSource("llvm", "cpppart");
Value getSumOfArrayFn = polyglotCtx.getBindings("llvm").getMember("getSumOfArray");
int sum = getSumOfArrayFn.execute(sqrNumbers, sqrNumbers.length).asInt();
二进制文件的源代码位于cpppart.cpp
文件中。该文件使用以下命令进行编译:
export LLVM_TOOLCHAIN=$(lli --print-toolchain-path)
$LLVM_TOOLCHAIN/clang++ -shared cpppart.cpp -lpolyglot-mock -o cpppart
让我们看一下cpppart.cpp
,它是一个导出函数的标准 C++ 程序。它接受一个数组及其长度作为参数,并返回一个数字
#include<iostream>
using namespace std;
// Function to find the sum of integer array
// extern "C" is required to suppress mangling
extern "C" int getSumOfArray(int array[], int size) {
printf("C++ => Find sum of numbers in an array\n");
int i, sum = 0;
for(i = 0; i < size; i++) {
sum += array[i];
}
return sum;
}
第五步:生锈
我们正在执行cube_root
位于文件 中的函数rustpart.bc
,该文件是从 编译而来的rustpart.rs
。我们将 C++ 函数的结果传递到这里。
/* Rust: Find the cube root of sum */
Value cubeRootFn = loadSource("llvm", "rustpart.bc").getMember("cube_root");
// println! macro does not work from Rust when embedded, some issue with mangling
System.out.println("Rust => Find cube root of the number");
Double cubeRoot = cubeRootFn.execute(sum).asDouble();
让我们看一下rustpart.rs
,它是一个标准的 Rust 函数,它接受一个数字,求出它的立方根并返回结果。但是我们必须指定#[no_mangle]
注解,而且显然也不能使用任何 crate。带有原始参数/输出的简单函数似乎可以工作,但嵌入更复杂的函数时无法工作。
#[no_mangle]
fn cube_root(arg: f64) -> f64 {
arg.cbrt()
}
fn main(){}
rustc
我们使用带有--emit=llvm-bc
标志的编译器将 Rust 源代码编译为二进制代码
rustc --emit=llvm-bc rustpart.rs
步骤6:Ruby
我们正在执行factorial
位于 file 中的函数rubypart.rb
。我们在这里传递 Rust 函数的结果。
/* RUBY: Find factorial of the number */
Value factorialFn = loadSource("ruby", "rubypart.rb");
long out = factorialFn.execute(cubeRoot).asLong();
让我们看一下rubypart.rb
,它是一个标准的 Ruby lambda 函数,它接受一个数字并返回其阶乘。
factorial = -> (num) {
puts "Ruby => Find factorial of the number"
(1..num).inject {|product, num| product * num }
}
最后,我们用 Java 打印输出。
运行程序
要运行此程序,我们需要先编译 C++、Rust 和 Java 文件,然后使用 GraalVM 提供的 JVM 运行它。步骤如下,您可以将其保存run.sh
并执行。
export LLVM_TOOLCHAIN=$(lli --print-toolchain-path)
$LLVM_TOOLCHAIN/clang++ -shared cpppart.cpp -lpolyglot-mock -o cpppart || exit
rustc --emit=llvm-bc rustpart.rs || exit
javac Polyglot.java && java Polyglot
它将产生以下输出:
结论
是不是很有意思?那么,这种多语言能力有用吗?这得看具体情况。GraalVM 的多语言能力目前还未达到生产环境,但它仍然很有用,因为它为真正的语言互操作性打开了大门。想象一下,你的程序可以使用任何语言的库。对于许多 C、Ruby、R、JS 和 Java 库来说,使用 GraalVM 已经可以做到这一点,但随着支持的不断完善,我们将能够摆脱语言限制。例如,GraalVM 对于 Ruby 等语言的运行速度似乎比标准的 CRuby 或 JRuby 快得多,这很有前景,因为这意味着你在程序中嵌入多种语言时不必担心开销。
GraalVM 是我最近遇到的最具革命性的技术之一,我希望多语言支持能够很快投入生产,结合其原生图像功能,它将成为真正多语言应用程序的强大平台。
如果您喜欢这篇文章,请点赞或留言。
封面图片来源:基于各个项目的官方标识。
文章来源:https://dev.to/deepu105/polyglot-inception-with-graalvm-why-because-it-s-fun-aa