Chicory运行时
Chicory是一个JVM原生的WebAssembly运行时。它允许你在没有任何本地依赖或JNI的情况下运行WebAssembly程序。Chicory可以在JVM能够运行的任何地方运行Wasm。它在设计时考虑了简单性和安全性。查看开发部分以更好地了解我们想要实现的目标及其原因。
联系我们:Chicory还处于早期开发阶段,可能会有一些粗糙之处。在我们正式向世界宣布测试版之前,我们希望能与一些早期采用者和贡献者交流。如果你有兴趣提供反馈或贡献,或者只是想跟进开发进度,请使用此邀请链接加入我们的Zulip团队聊天。
入门指南(作为用户)
安装依赖
要使用运行时,你需要将com.dylibso.chicory:runtime
依赖添加到你的依赖管理系统中。
Maven
<dependency>
<groupId>com.dylibso.chicory</groupId>
<artifactId>runtime</artifactId>
<version>0.0.12</version>
</dependency>
Gradle
implementation 'com.dylibso.chicory:runtime:0.0.12'
安装CLI(实验性)
Chicory CLI可在Maven上通过以下链接下载:
https://repo1.maven.org/maven2/com/dylibso/chicory/cli/<version>/cli-<version>.sh
你可以用几行命令下载最新版本并在本地使用:
export VERSION=$(wget -q -O - https://api.github.com/repos/dylibso/chicory/tags --header "Accept: application/json" | jq -r '.[0].name')
wget -O chicory https://repo1.maven.org/maven2/com/dylibso/chicory/cli/${VERSION}/cli-${VERSION}.sh
chmod a+x chicory
./chicory
加载和实例化代码
首先,你的Wasm模块必须从磁盘加载,然后进行"实例化"。让我们下载一个测试模块。 这个模块包含了计算阶乘的代码:
从链接下载或使用curl:
curl https://raw.githubusercontent.com/dylibso/chicory/main/wasm-corpus/src/main/resources/compiled/iterfact.wat.wasm > factorial.wasm
现在让我们加载这个模块并实例化它:
import com.dylibso.chicory.runtime.ExportFunction;
import com.dylibso.chicory.wasm.types.Value;
import com.dylibso.chicory.wasm.Module;
import com.dylibso.chicory.wasm.Parser;
import com.dylibso.chicory.runtime.Instance;
import java.io.File;
// 将此处指向你磁盘上的路径
Module module = Parser.parse(new File("./factorial.wasm"));
Instance instance = Instance.builder(module).build();
你可以把module
看作是静态的代码,而instance
则是加载了代码并准备执行的虚拟机。
调用导出函数
Wasm模块,像所有代码模块一样,可以向外部世界导出函数。这个模块导出了一个名为"iterFact"
的函数。我们可以使用Instance#export(String)
来获取这个函数的句柄:
ExportFunction iterFact = instance.export("iterFact");
可以使用apply()
方法调用iterFact。我们必须将任何Java类型映射到wasm类型,反之亦然。这个导出函数接受一个i32
参数。我们可以在返回值上使用Value#asInt()
这样的方法来获取Java整数:
Value result = iterFact.apply(Value.i32(5))[0];
System.out.println("Result: " + result.asInt()); // 应该打印120(5的阶乘)
注意:Wasm中的函数可以有多个返回值,但这里我们只取第一个返回值。
内存和复杂类型
Wasm只理解基本的整数和浮点数原始类型。因此,跨边界传递更复杂的类型涉及传递指针。为了读取、写入或分配模块中的内存,Chicory为你提供了Memory
类。让我们看一个例子,其中有一个用Rust编写的count_vowels.wasm
模块,它接受一个字符串输入并计算字符串中的元音数量:
curl https://raw.githubusercontent.com/dylibso/chicory/main/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm > count_vowels.wasm
构建并实例化这个模块:
Instance instance = Instance.builder(Parser.parse(new File("./count_vowels.wasm"))).build();
ExportFunction countVowels = instance.export("count_vowels");
要传递一个字符串,我们首先需要将字符串放入模块的内存中。为了使这更容易和安全,该模块给我们提供了一些额外的导出函数,允许我们分配和释放内存:
ExportFunction alloc = instance.export("alloc");
ExportFunction dealloc = instance.export("dealloc");
让我们为字符串分配Wasm内存并将其放入实例的内存中。我们可以使用Memory#put
来做到这一点:
import com.dylibso.chicory.runtime.Memory;
Memory memory = instance.memory();
String message = "Hello, World!";
int len = message.getBytes().length;
// 分配{len}字节的内存,这会返回一个指向该内存的指针
int ptr = alloc.apply(Value.i32(len))[0].asInt();
// 现在我们可以将消息写入模块的内存:
memory.writeString(ptr, message);
现在我们可以用这个指向字符串的指针调用countVowels
。它会完成它的工作并返回计数。我们将调用dealloc
来释放模块中的那块内存。当然,如果你愿意,模块也可以自己做这件事:
Value result = countVowels.apply(Value.i32(ptr), Value.i32(len))[0];
dealloc.apply(Value.i32(ptr), Value.i32(len));
assert(3 == result.asInt()); // Hello, World! 中有3个元音
宿主函数
单独的Wasm只能进行计算,不能影响外部世界。这可能看起来像是一个弱点,但实际上这是Wasm最大的优势。默认情况下,程序是沙盒化的,没有任何能力。如果你想让程序有能力,你必须提供它们。这使你处于操作系统的位置。模块可以通过在其字节码格式中列出"导入"函数来请求能力。你可以用用Java编写的宿主函数来满足这个导入。无论模块的语言是什么,当它需要时都可以调用这个Java函数。如果有帮助的话,你可以把宿主函数想象成系统调用或语言的标准库,但你决定它们是什么以及它们如何行为,而且它是用Java编写的。
让我们下载另一个示例模块来演示这一点:
curl https://raw.githubusercontent.com/dylibso/chicory/main/wasm-corpus/src/main/resources/compiled/host-function.wat.wasm > logger.wasm
这个模块期望我们提供一个名为console.log
的导入,这将允许模块输出到stdout。让我们编写该宿主函数:
import com.dylibso.chicory.runtime.HostFunction;
import com.dylibso.chicory.wasm.types.ValueType;
var func = new HostFunction(
(Instance instance, Value... args) -> { // 反编译为:console_log(13, 0);
var len = args[0].asInt();
var offset = args[1].asInt();
var message = instance.memory().readString(offset, len);
println(message);
return null;
},
"console",
"log",
List.of(ValueType.I32, ValueType.I32),
List.of());
这里我们再次处理指针。模块调用console.log
时传入字符串的长度和它在内存中的指针(偏移量)。我们再次使用Memory
类,但这次我们是从内存中提取字符串。然后我们可以代表我们的Wasm程序将其打印到stdout。
注意,HostFunction需要3个东西:
- 当Wasm模块调用导入时要调用的lambda
- 导入的命名空间和函数名(在我们的例子中分别是console和log)
- Wasm类型签名(这个函数接受2个i32作为参数,不返回任何内容) 现在我们只需在实例化阶段传入这个主机函数:
import com.dylibso.chicory.runtime.HostImports;
var imports = new HostImports(new HostFunction[] {func});
var instance = Instance.builder(Parser.parse(new File("./logger.wasm"))).withHostImports(imports).build();
var logIt = instance.export("logIt");
logIt.apply();
// 应该打印 "Hello, World!" 10 次
开发
为什么需要这个?
如果你更倾向于观看视频而不是阅读,请查看我们在2024 Wasm I/O上的相关演讲:
有很多成熟的 Wasm 运行时可供选择来执行 Wasm 模块。 例如 v8、wasmtime、wasmer、wasmedge 等。
尽管这些可能是运行 Wasm 应用程序的很好选择,但将它们嵌入到现有的 Java 应用程序中有一些缺点。因为这些运行时是用 C/C++/Rust 等语言编写的,它们必须作为 本机代码分发和运行。这会导致两个主要的摩擦点:
1. 分发
如果你正在分发 Java 库(jar、war 等),你现在必须随之分发针对正确 架构和操作系统的本机对象。这个矩阵可能会变得相当大。这消除了发布 Java 代码的很多简单性和原始好处。
2. 运行时
在运行时,你必须使用 FFI 来执行模块。虽然对某些模块来说这样做可能会带来性能上的好处, 但当你这样做时,你实际上是在逃离 JVM 的安全性和可观察性。拥有纯 JVM 运行时意味着所有的 安全性和内存保证,以及你的工具,都可以保持不变。
目标
- 尽可能安全
- 我们愿意为安全性和简单性牺牲性能等方面
- 使在任何 JVM 环境中运行 Wasm 变得容易,无需本机代码,包括非常严格的环境。
- 完全支持核心 Wasm 规范
- 使与 Java(和其他主机语言)的集成变得简单和符合习惯。
非目标:
- 成为独立的运行时
- 成为最快的运行时
- 成为每个 JVM 项目的正确选择
路线图
Chicory 的开发始于 2023 年 9 月。以下是我们的目标里程碑。这些 可能会发生变化,但代表了我们根据当前信息做出的最佳猜测。这些并不一定是按顺序进行的, 有些可能会同时进行。除非特别说明,任何未勾选的框都尚未计划或开始。 如果你对其中任何一项感兴趣,请在 Zulip 上联系我们!
引导字节码解释器和测试套件 (2023 年底)
使解释器可用于生产环境 (2024 年夏)
- 使所有测试在解释器上通过(对正确性很重要)
- 实现验证逻辑(对安全性很重要)
- 接近完成
- v1.0 API 草案(对稳定性和开发体验很重要)
提高性能 (2024 年底)
这里的主要目标是创建一个生成 JVM 字节码的 AOT 编译器, 因为解释字节码的速度是有限的。
- 解耦解释器,创建独立的编译器和解释器"引擎"
- AOT 编译器概念验证(运行一些模块子集)
- AOT 引擎通过与解释器相同的所有规范(延伸目标)
- 进行中
- 堆外线性内存(延伸目标)
提高兼容性 (2024 年底)
- WASIp1 支持(包括测试生成)
- 我们有 部分 wasip1 支持 和 测试生成
- SIMD 支持
- 已开始
- 多内存支持
- GC 支持
- 线程支持
- 组件模型支持
先前的工作
构建运行时
贡献者和其他高级用户可能希望从源代码构建运行时。要做到这一点,你需要安装 Maven。
需要 Java 11+版本
才能正确构建。你可以下载并安装 Java 11 Temurin
基本步骤:
mvn clean install
运行项目的所有测试并在本地仓库中安装库mvn -Dquickly
安装库,跳过所有测试mvn spotless:apply
自动格式化代码./scripts/compile-resources.sh
将重新编译并重新生成resources/compiled
文件夹
注意: install
目标依赖 wabt
库来编译测试套件。目前 ARM 平台(例如使用 Apple Silicon 的新 Mac)尚未发布该库。然而,可以通过 Homebrew 获得 wabt
,所以在运行 mvn clean install
之前执行 brew install wabt
应该可以解决问题。
日志记录
为了最大程度的兼容性并避免外部依赖,我们默认使用JDK平台日志记录(JEP 264)。
您可以通过提供一个使用java.util.logging.config.file
属性的logging.properties
文件来配置它,您可以在这里找到可能的配置选项。
对于更高级的配置场景,我们建议您提供一个替代的、兼容的适配器:
如果JDK平台日志记录不可用或不适合,还可以提供自定义的com.dylibso.chicory.log.Logger
实现。