[译]Rust 编写的 WebAssembly 运行时,Wasmtime 的架构

原文:Architecture of Wasmtime

序言

出于对成为编译器工程师的向往,我开始深入挖掘各项编译技术的细节。作为一名前端工程师,我决定首先从 WebAssembly 技术开始学习。在阅读完 WebAssembly 规范后,我准备着手深入了解如何实现一个 WebAssembly 运行时。

考虑到我只熟悉 Rust,我选择从 Wasmtime 开始,这是一个由 Rust 编写的 WebAssembly 运行时。它严格遵循 WebAssembly 规范并采用 JIT 技术以提高运行速度,是个相当优秀的实现。

我计划首先研究 Wasmtime 的文档和相关资料,以深入了解其总体设计。随后,我会阅读其代码,对其实现细节进行深入理解。

使用

rust 复制代码
extern crate wasmtime;
use std::error::Error;
use wasmtime::*;

fn main() -> Result<(), Box<dyn Error>> {
    // Engine 存储和配置全局的编译配置,例如优化等级、允许的 wasm 功能等。
    let engine = Engine::default();

    if false {
        // 首先创建一个 `Module`,它代表我们输入的 wasm 模块的编译形式。
        // 在这种情况下,它将在我们解析文本格式后进行 JIT 编译。
        let module = Module::from_file(&engine, "hello.wat")?;
    }
    let module = Module::new(&engine, r#"(module (func (export "answer") (result i32) i32.const 42))"#)?;

    // `Store` 拥有实例、函数、全局变量等。
    // 所有的 wasm 项目都存储在 `Store` 中,我们将始终使用它来与 wasm 世界进行交互。
    // 自定义数据可以存储在 Store 中,但现在我们只使用 `()`。
    let mut store = Store::new(&engine, ());

    // 通过编译的 `Module`,我们可以实例化它,创建一个 `Instance`,
    // 我们可以在其上实际操作函数。
    let instance = Instance::new(&mut store, &module, &[])?;

    // `Instance` 为我们提供了访问各种导出函数和项的途径,
    // 在这里,我们访问它以提取我们的 `answer` 导出函数并运行它。
    let answer = instance.get_func(&mut store, "answer")
        .expect("`answer` was not an exported function");

    // 有几种方法可以调用 `answer` `Func` 值。
    // 最简单的方法是使用 `typed` 静态断言其签名(在这种情况下断言它不带参数并返回一个i32),
    // 然后调用它。
    let answer = answer.typed::<(), i32>(&store)?;

    // 最后我们可以调用我们的函数!
    // 请注意,使用 `?` 进行的错误传播是为了处理 wasm 函数陷阱的情况。
    let result = answer.call(&mut store, ())?;
    println!("Answer: {:?}", result);
    Ok(())
}

wasmtime crate

Wasmtime 的主入口点是 wasmtime crate。Wasmtime 的设计使得 wasmtime crate 几乎是一个100%安全的 API(在 Rust 层面上是安全的),除了一些小的、有文档说明的函数是不安全的。wasmtime crate 提供了对 WebAssembly 原语、功能特性和访问,例如编译模块、实例化、调用函数等等。

wasmtime crate 是第一个旨在被用户使用的 crate。这里的 "第一个" 指的是 wasmtime 所依赖的一切都被视为内部依赖。我们将 crate 发布到 crates.io,但几乎不会花费精力来为内部 crate 提供"友好"的 API,也不会担心内部库之间的版本断裂。这主要意味着,这里讨论的所有其他 crate 都被视为 Wasmtime 的内部依赖,根本不会出现在 wasmtime 的公共 API 中。使用一些 Cargo 术语,wasmtime 所依赖的所有 wasmtime-* crate 都是"私有"依赖项。

此外,目前在 Wasmtime 的内部包之间,安全/不安全边界并不是明确的。有些方法应该标记为不安全,但实际上没有标记,而不安全的方法也没有详尽的文档说明为什么不安全。这是一个需要持续改进的问题,目标是让安全方法在 Rust 层面上真正安全,并为不安全的方法提供清晰列出其不安全原因的文档。

重要的概念

在讨论更多深入细节的内部工作原理之前,重要的是要了解一些基本概念。以下是 Wasmtime 中的一些重要类型及其含义:

wasmtime::Engine

这是一个全局编译上下文,类似于"根上下文"。Engine 通常在程序中只创建一次,并预期在多个线程间共享(在内部,它是原子化的引用计数 Arc)。每个 Engine 存储配置值和其他跨线程数据,如用于 Module 实例的内部类型。关于 Engine 需要记住的主要事项是,对其内部的任何修改通常涉及到获取锁,而对于下面的 Store 则不需要锁。

wasmtime::Store

Store 对应 WebAssembly 中的 "store" 概念。虽然也有一个正式的定义,但它可以被看作是一组相关的 WebAssembly 对象。包括实例、全局变量、内存、表等。Store 并未实现对内部项的任何形式的垃圾收集(有一个 gc 函数,但那只是针对 externref 值的)。这意味着,一旦你创建一个 InstanceTable,内存实际上并不会被释放,直到 Store 本身被释放。Store 在某种程度上是用于几乎所有 wasm 操作的 "上下文"。Store 还包含递归引用回 Store 的实例句柄,导致 Store 内部的指针有相当多的别名。然而,现在要知道的重要一点是,Store 是隔离的单位。WebAssembly 对象总是完全包含在一个 Store 中,并且目前没有任何东西可以在 Store 之间交叉(除非你手动搭配标量)。换句话说,来自不同 Store 的 wasm 对象不能互相交互。Store 不能同时从多个线程使用(几乎所有操作都需要 &mut self)。

wasmtime_runtime::InstanceHandle

InstanceHandle 是 WebAssembly 实例的低级表示。同时,它也被用作所有宿主定义对象的表示。例如,如果你调用 wasmtime::Memory::new,它会在后台创建一个 InstanceHandle。这是一种非常不安全的类型,可能应该将其所有的函数标记为不安全,或者对它有更严格的保证文档,但这是我们目前没有为公共消费者考虑太多的内部类型。InstanceHandle 不知道如何释放自己,并依赖于调用者来管理其内存。目前,这要么是按需分配(使用 malloc),要么是池化方式(使用池化分配器)。在这两条路径中,释放方法是不同的(分配方法也是如此)。

InstanceHandle 在内存中的布局先有一些 Rust 拥有的值,捕获了内存/表/等的动态状态。大多数这些字段对于只服务于一个目的的主机定义对象(例如 wasmtime::Table::new)是未使用的,但对于一个实例化的 WebAssembly 模块,这些字段将有更多的信息。在内存中的 InstanceHandle 的背后是一个 VMContext,我们将在下面讨论。InstanceHandle 值是主要的内部运行时表示,是 wasmtime_runtime 库所处理的。wasmtime::Store 持有所有这些 InstanceHandle 值,并在适当的时候释放它们。从运行时的角度看,这简化了事情,使得 wasm 模块之间的交互图简化为仅仅是 InstanceHandle 值自我交谈。

wasmtime_runtime::VMContext

VMContext 是一个原始指针,在 InstanceHandle 的分配中传递,在 JIT 代码中使用。VMContext 在 Rust 中没有定义结构(它是一个零大小的结构),因为其内容是基于 VMOffsets 动态确定的,或者是源自 wasm 模块。每个 InstanceHandle 都有一个与之对应的 VMContext 的 "形状"。例如,VMContext 存储所有 WebAssembly 全局值,但如果 wasm 模块没有全局值,那么这个数组的大小将为 0,不会被分配。VMContext 的目的是成为 JIT 代码可能访问的所有 wasm 模块状态的高效内存表示。VMContext 的布局由模块动态确定,JIT 代码针对这一结构进行专门化。这意味着这个结构可以被 JIT 代码有效地访问,但被原生主机代码访问的效率较低。VMContext 的非详尽用途列表包括:

  • 存储 WebAssembly 实例状态,如全局值、指向表的指针、指向内存的指针和指向其他 JIT 函数的指针。
  • 分离 wasm 导入和本地状态。导入的值存储指向实际值的指针,本地状态定义了内联状态。
  • 保持一个指向栈限制的指针,在此点 JIT 代码将触发栈溢出。
  • 保持一个指向 VMExternRefActivationsTable 的指针,以便在表中快速插入 externref 值。
  • 保持一个指向 *mut dyn wasmtime_runtime::Store 的指针,以便在 libcalls 中执行存储级操作。
  • vmoffsets.rs 文件中可以找到关于 VMContext 布局的注释。

wasmtime::Module

Module 是已编译 WebAssembly 模块的表示。目前,Wasmtime 总是假定 wasm 模块总是被编译为本地 JIT 代码。Module 保存了此编译的结果,目前可以使用 Cranelift 进行编译。Wasmtime 的目标是支持其他形式的模块表示,但现在还未实现,只实现和支持了 Cranelift。

wasmtime_environ::Module

ModuleTranslation 是 wasm 模块的类型和结构的描述符,但不包含任何实际的 JIT 代码。在编译过程的早期阶段就会创建这个类型的实例,并且在函数本身实际编译时不会修改它。这里包含了类型内部化表示和关于函数、全局变量等的状态。从某种意义上说,这可以被看作是验证或类型检查 wasm 模块的结果,尽管它没有像每个操作码的类型或那样微小的函数级详细信息。

编译一个模块

通过对类型的总览和背景信息,接下来将介绍编译 WebAssembly 模块所采取的步骤。这个过程的主要入口点是 wasmtime::Module::from_binary API。还有一些其他的入口点处理像文本到二进制的转换、从文件系统加载等上层问题。

编译大致分为几个阶段:

  1. 首次编译遍历 WebAssembly 模块,验证除函数体以外的所有内容。这个同步的 wasm 模块遍历创建了一个 wasmtime_environ::Module 实例,并准备进行函数编译。请注意,根据模块链接提案,一个输入模块可能最终会产生多个要处理的输出模块。每个模块都是独立处理的,所有后续步骤都是基于每个模块并行化的。请注意, WebAssembly 模块的解析和验证是使用 wasmparser crate 完成的。验证与解析交替进行,在使用解析值之前验证它们。
  2. 接下来,模块中的所有函数都会并行进行验证和编译。此时不进行跨过程分析,每个函数都被编译为自己独立的代码块。这是 Cranelift 在每个函数基础上调用其核心部分的时候。
  3. 此时编译结果被封装成了一个 wasmtime_jit::CompilationArtifacts 结构。其中包含模块信息(wasmtime_environ::Module)、编译的 JIT 代码(存储为 ELF 镜像)以及有关函数的其他信息,例如平台无关的展开信息、每个函数的陷阱表(指示哪些 JIT 指令可能会产生陷阱以及陷阱的含义)、每个函数的地址映射(从 JIT 地址映射回 wasm 偏移量)以及调试信息(从 wasm 模块中的 DWARF 信息解析而来)。这些结果是惰性的,无法实际执行,但此时它们可以序列化到磁盘,或者进入下一阶段...
  4. 最后一步是将所有代码放入一个准备好执行的表单中。这始于上一步的 CompilationArtifacts。在这里,分配了一个新的内存映射,并将 JIT 代码复制到该内存映射中。然后将该内存映射从读/写切换到读/执行,此时是可实际执行的 JIT 代码。在这里,各种钩子,如加载 debuginfo,通知 JIT 分析器有新代码等,都会发生。此时会生成一个 wasmtime_jit::CompiledModule,并将其包装在 wasmtime::Module 中。此时,模块已准备好实例化。

wasmtime::Module 是一个原子引用计数对象,在实例化到 Store 时,Store 将对模块的内部持有强引用。这意味着所有 wasmtime::Module 的实例共享相同的编译代码。另外,wasmtime::Module 是少数几个在 wasmtime::Store 外部生活的对象。这意味着 wasmtime::Module 的引用计数是其自身形式的内存管理。

注意,所有实例共享模块编译代码的属性对编译代码可以假设的内容有着有趣的影响。例如,Wasmtime 实现了一种类型内部化形式,但是内部化的类型在几个不同的级别发生。在模块内部,我们对函数类型进行了去重,但是在 Store 中的模块间,类型需要用相同的值来表示。这意味着,如果同一个模块被实例化到多个存储中,其相同的函数类型可能会有多个值,所以编译代码不能假设函数类型的特定值。(稍后会有关于类型信息的更多内容)。总的来说,编译代码相当依赖于 VMContext 提供上下文输入,因为 JIT 代码的重用性非常高。

跳板

编译的另一个重要方面是创建跳板。在这里,跳板指的是 Wasmtime 执行以进入 WebAssembly 代码的代码。宿主可能并不总是事先知道它想要调用的 WebAssembly 函数的签名。Wasmtime JIT 代码按照本地 ABI 编译(例如,根据 Unix 上的 System V,参数/结果在寄存器中),这意味着 Wasmtime 嵌入没有简单的方法进入 JIT 代码。

这个问题是模块编译进的跳板解决的问题,那就是提供一个已知 ABI 的函数,它将调用具有特定其他类型签名/ABI 的函数。Wasmtime 收集模块的所有导出函数,并创建它们的类型签名集。注意,在这个上下文中,导出实际上意味着 "可能导出",包括像插入到全局/函数表、转换为 funcref 等。为每个这种类型签名生成一个跳板,并与模块的其余 JIT 代码一起存储。

然后,这些跳板与 wasmtime::Func::call API 一起使用。在这种特定情况下,因为我们不知道目标函数的 ABI,所以使用跳板(具有已知的 ABI),并通过堆栈传递所有的参数/结果。

另一个需要注意的点是,目前跳板没有去重。每个编译的模块都包含自己的跳板集,如果两个编译的模块具有相同的类型,那么它们将有相同跳板的不同副本。

类型内部化和 VMSharedSignatureIndex

讨论编译时,VMSharedSignatureIndex 类型及其使用方式是一个重要的问题。在 wasm 中,call_indirect 操作码会比较实际函数的签名和指令的函数签名,如果签名不匹配,会产生陷阱。在 Wasmtime 中,这是通过整数比较来实现的,比较发生在 VMSharedSignatureIndex 值上。这个索引是函数类型的内部表示。

VMSharedSignatureIndex 的内部化范围在 wasmtime::Engine 级别发生。模块被编译进 Engine 中。将模块插入到 Engine 中将为模块中找到的所有类型分配 VMSharedSignatureIndex

模块的 VMSharedSignatureIndex 值对于该模块的一个实例是局部的(并且它们可能在每次将模块插入到不同的 Engine 中时都会改变)。这些在实例化过程中由运行时使用,以有效地为所有函数分配类型 ID,例如导入等。

实例化一个模块

一旦模块被编译,通常接下来会进行实例化,以便实际获取到导出功能并调用 wasm 代码。实例化始终在 wasmtime::Store 内进行,创建的实例(以及所有导出功能)都与 Store 绑定。

实例化本身(位于 crates/wasmtime/src/instance.rs)可能看起来很复杂,但这主要是由于实现了模块链接提案。实例化的大致流程如下:

  1. 首先,会检查所有导入的类型都。提供的导入列表会与 wasmtime_environ::Module 中记录的导入列表进行相互引用,验证所有类型是否对齐和匹配(根据 wasm 核心规范中对类型匹配的定义)。

  2. 每个 wasmtime_environ::Module 都有一些初始化器列表,这些初始化器需要在实例化完成前完成。对于 MVP wasm,这只涉及将导入加载到正确的索引数组中,但对于模块链接,这可能涉及实例化其他模块、处理别名字段等。无论如何,这一步的结果是一个 wasmtime_runtime::Imports 数组,它包含了所有导入到 wasm 模块中的项的值。注意,在这种情况下,导入通常是指向实际状态的原始指针,以及从中导入的实例的 VMContext。这一步的最终结果是一个 InstanceAllocationRequest,它然后被提交给配置的实例分配器,可以是按需的,也可以是池化的。

  3. 分配与此实例对应的 InstanceHandle。如何分配取决于采用的策略(按需分配为 malloc,池化分配为 slab 分配)。除了初始化 InstanceHandle 的字段外,这还初始化了此句柄的 VMContext 的所有字段(如上所述,它在内存中位于 InstanceHandle 分配后的相邻位置)。这个阶段并不处理任何数据段、元素段或开始函数。

  4. 此时,InstanceHandle 被存储在 Store 中。这是"不返回点",句柄必须与 Store 本身保持相同的生命周期。如果初始化步骤失败,那么实例可能仍然已经将其函数,例如,通过元素段插入到导入的表中。这意味着即使我们未能初始化此实例,其状态仍可能对其他实例/对象可见,因此我们必须无论如何保持其活跃。

  5. 最后一步是执行 wasm 定义的实例化。这涉及处理元素段、数据段、开始函数等。大部分工作只是从 Wasmtime 的内部表示转换为规范所需的行为。

实例化模块的另一个值得注意的部分是,Store 内维护了一个 ModuleRegistry,其中包含了所有实例化到 store 的模块。这个注册表的目的是保留执行实例所需的模块中的项的强引用。这主要包括 JIT 代码,但也包括如 VMSharedSignatureIndex 注册、关于函数地址等的元数据等信息。大部分这些数据存储在 GLOBAL_MODULES 映射中,以便在后续产生陷阱时进行访问。

陷阱

一旦实例已经创建且 wasm 开始运行,大部分事情都相当标准。跳板用于进入 wasm(或者如果使用 wasmtime::TypedFunc,我们可以通过已知的 ABI 进入),JIT 代码通常做它需要做的事来执行 wasm。然而,值得讨论的实现的重要方面是陷阱。

Wasmtime 现在使用 longjmp 和 setjmp 实现陷阱。setjmp 函数不能在 Rust 中定义(即使是不安全的 -- github.com/rust-lang/r...),所以 crates/runtime/src/helpers.c 文件实际调用 setjmp/longjmp。注意,一般来说,longjmp 的操作在 Rust 中是不安全的,因为它跳过了基于堆栈的析构函数,所以在 setjmp 后我们调用回 Rust 来执行 wasm,我们需要在 Wasmtime 中小心,一旦调用了 wasm,堆栈上就不会有任何重要的析构函数。

陷阱可以由几个不同的源产生:

  • 显式陷阱 - 例如,当主机调用返回一个陷阱时,这些可能会发生。这些最终在 raise_user_trap 或 raise_lib_trap 中,两者都立即调用 longjmp 返回到 wasm 的起始点。注意,这些,就像调用 wasm 一样,必须要非常小心不要在堆栈上有任何析构函数。
  • 信号 - 这是陷阱的主要途径。基本上我们使用 segfault 和非法指令来在 wasm 代码本身中实现陷阱。当线性内存访问越界时会发生 Segfaults,非法指令是 wasm unreachable 指令实现的方式。在这两种情况下,Wasmtime 安装一个特定于平台的信号处理器来捕获信号,检查信号的状态,然后处理它。注意,Wasmtime 试图只捕获来自 JIT 代码本身的信号,以免意外地掩盖其他错误。退出信号处理器通过 longjmp 返回到原始的 wasm 调用点。

总的来说,Wasmtime 对 wasm 的堆栈帧有非常严格的控制(自然是通过 Cranelift),并且对我们进入 wasm 之前执行的代码(也就是在 setjmp 之前)和我们重新进入 wasm 之后执行的代码(也就是可能的 longjmp 之前的帧)也有非常严格的控制。

Wasmtime 的信号处理器使用在实例化期间填充的 GLOBAL_MODULES 映射来确定触发信号的程序计数器是否确实是一个有效的 wasm 陷阱。除了宿主程序有其他错误触发信号的情况外,这应该是正确的。

值得一提的是,Wasmtime 使用 Rust 的 backtrace crate 在出现 wasm 异常时捕获堆栈跟踪。这迫使 Wasmtime 生成本地特定的解开信息,以正确地解开堆栈并为 wasm 代码生成堆栈跟踪。这也有其他好处,比如当与 Wasmtime 一起使用时,可以提高通用的采样分析器。

线性内存

Wasmtime 中的线性内存实际上是通过 mmap(或其平台等效物)实现的,但这里也有一些微妙的细微差别值得指出。线性内存的实现相对可配置,这导致运行时和生成的代码需要处理许多情况。

首先,线性内存有一些可以配置的属性:

  • wasmtime::Config::static_memory_maximum_size
  • wasmtime::Config::static_memory_guard_size
  • wasmtime::Config::dynamic_memory_guard_size
  • wasmtime::Config::guard_before_linear_memory

Config 上的方法有很多文档来讨论一些细节,但一般来说,Wasmtime 有两种内存模式:静态和动态。静态内存表示永不移动的预留地址空间,提交页以表示内存增长。动态内存表示分配的地址空间,其中提交的部分正好匹配 wasm 内存的大小,通过分配更大的内存块来实现增长。

防护区大小的配置指示线性内存后的防护区的大小。这个防护区大小影响生成的 JIT 代码是否发出边界检查。如果越界地址可以证明会遇到防护页,则省略边界检查。

guard_before_linear_memory 配置还在线性内存前面和后面放置了防护页(两端都是相同的大小)。这只用于防止可能的 Cranelift 错误,否则没有任何作用。

Wasmtime 在 64 位平台上的默认设置是:

  • 4GB 静态最大大小,意味着所有 32 位内存都是静态的,64 位内存是动态的。
  • 2GB 静态防护区大小,意味着所有加载/存储的偏移量小于 2GB 的都不需要对 32 位内存进行边界检查。
  • 启用了线性内存前的防护页。

总的来说,这意味着在 Wasmtime 中,32 位线性内存默认会预留 8GB 的虚拟地址空间。使用池分配器,我们知道线性内存是连续的,这导致每个内存默认预留 6GB,因为一个内存后的防护区是下一个内存前的防护区。

注意,64 位内存(WebAssembly 的 memory64 提案)可以配置为静态,但目前永远无法省略边界检查。这可以通过 static_memory_forced 配置选项进行配置。还要注意,Wasmtime 对 64 位内存的支持目前是可用的,但尚未进行调优,所以可能还有一些性能工作和更好的默认管理需要处理。

表和 externref

WebAssembly 表包含引用类型,目前是 funcref 或 externref。在 Wasmtime 中,funcref 表示为 *mut VMCallerCheckedFuncRef,externref 表示为 VMExternRef(内部为 *mut VMExternData)。因此,表被表示为指针的向量。默认情况下,表存储内存管理通过 Rust 的 Vec 进行,它使用 malloc 和相关函数进行内存管理。使用池分配器,这会使用预分配的内存进行存储。

如前所述,Store 对 wasm 对象本身没有内部垃圾回收,所以 wasm 中的 funcref 表非常简单,存储在其中的任何指针都没有生命周期管理,只假设在表被使用的时间内它们都有效。

对于 externref 的表,情况更为复杂。VMExternRefArc<dyn Any> 的一个版本,但在 Wasmtime 中进行了特化,所以 JIT 代码知道引用计数字段的偏移量在哪里,以直接操作它。此外,externref 值的表需要自己管理引用计数字段,因为存储在表中的指针需要分配一个强引用计数。

GC 和 externref

Wasmtime 使用原子引用计数指针实现了 WebAssembly 的 externref 类型。请注意,原子部分不是 wasm 本身所需要的,而是 Rust 嵌入环境需要的,因为它必须能够安全地将 ExternRef 值发送到其他线程。Wasmtime 也没有循环收集器,所以宿主分配的 ExternRef 对象的循环会泄漏。

尽管有引用计数,但 Store::gc 方法仍然存在。这是 wasm 代码执行时如何管理引用计数的实现细节。Wasmtime 实现了 "延迟引用计数",而不是在堆栈上移动 externref 值时单独管理其引用计数,这里有一个可能被过度使用的 ExternRef 值列表,周期性地执行 GC 以使这个过度保守的列表变为精确的列表。这利用了 Cranelift 的堆栈映射支持以及 backtrace 的回溯支持来确定堆栈上的活动根。Store::gc 方法强制可能过度保守的列表变为堆栈上正在使用的 externref 值的精确列表。

Crate 列表

Wasmtime 主要的内部 crate 包括:

  • wasmtime - 暴露安全的公共 API。
  • wasmtime-jit - JIT 特定的 Wasmtime 支持。这是在运行时生成的可执行内存的具体实现。目前所有模块都是以这种方式编译的,但有一天 Wasmtime 可能能够在没有 JIT 支持的情况下编译,只能加载预编译的模块。
  • wasmtime-runtime - Wasmtime 的低级运行时实现。这是 VMContext 和 InstanceHandle 存在的地方。这个 crate 理论上不关心 JIT 代码是如何编译的以及它运行在哪个运行时中。
  • wasmtime-environ - 低级编译支持。这是模块及其环境的翻译发生的地方,尽管在这个 crate 中实际上并没有编译发生(尽管它为编译器定义了一个接口)。这个 crate 的结果被移交给其他 crate 进行实际编译。
  • wasmtime-cranelift - 使用 Cranelift 实现函数级别的编译。

请注意,目前 Cranelift 是 Wasmtime 的必需依赖项。wasmtime-environ 导出的大多数类型在其 API 中使用 Cranelift 类型。尽管有一天,我们的目标是删除必需的 Cranelift 依赖项,并使 wasmtime-environ 成为一个相对独立的 crate。

除了上述的 crate 之外,wasmtime 还依赖一些其他 crate:

  • wasmtime-cache - 可选依赖项,用于管理文件系统上的默认缓存。默认情况下在 CLI 中启用,但在 wasmtime 包中默认未启用。
  • wasmtime-fiber - Wasmtime 中 async支持使用的堆栈切换实现。
  • wasmtime-debug - 将 WASM Dwarf 调试信息映射到本地 Dwarf 调试信息的实现。
  • wasmtime-profiling - 将生成的 JIT 代码与标准分析运行时连接的实现。
  • wasmtime-obj - 从编译的函数创建 ELF 镜像的实现。
相关推荐
速盾cdn22 分钟前
速盾:网页游戏部署高防服务器有什么优势?
服务器·前端·web安全
小白求学123 分钟前
CSS浮动
前端·css·css3
什么鬼昵称24 分钟前
Pikachu-csrf-CSRF(POST)
前端·csrf
golitter.41 分钟前
Vue组件库Element-ui
前端·vue.js·ui
golitter.1 小时前
Ajax和axios简单用法
前端·ajax·okhttp
雷特IT1 小时前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
长路 ㅤ   2 小时前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
亚里士多没有德7752 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010142 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴2 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript