Skip to content

Variant codegen is incorrect with wasmtime Rust host and wit-bindgen Rust guest #526

@philpax

Description

@philpax

Hi there! I've been updating our bindings from pre-CM to the latest version of wasmtime and wit-bindgen, and have been largely successful, but I've encountered a showstopper issue.

Issue

We use a variant for typed values that we want to transmit between the host and the guest. These are sent as a list of (u32, value) pairs. The guest calls a spawn: func(data: list<tuple<u32, component-type>>) -> entity-id function, which the host then handles.

However, after updating to the CM, I've discovered that the host is receiving entirely incorrect values from the guest. I'm not sure if this is an issue with the host reading or the guest writing; I don't have enough background on what the codegen output should look like to debug it.

To demonstrate, here's excerpts from my minimised example:

// WIT:
variant component-type {
    type-empty(tuple<>),
    type-bool(bool),
    type-entity-id(entity-id),
    type-f32(float32),
    type-f64(float64),
    type-mat4(mat4),
    type-i32(s32),
    type-quat(quat),
    type-string(string),
    type-u32(u32),
    type-u64(u64),
    type-vec2(vec2),
    type-vec3(vec3),
    type-vec4(vec4),
    type-list(component-list-type),
    type-option(component-option-type),
}

// Guest:
let entity = vec![
    ( 0, wit::component::ComponentTypeParam::TypeQuat(wit::types::Quat { x: 0., y: 0., z: 0., w: 1., }), ),
    ( 1, wit::component::ComponentTypeParam::TypeVec3(wit::types::Vec3 { x: 1., y: 1., z: 1., }), ),
    ( 2, wit::component::ComponentTypeParam::TypeVec3(wit::types::Vec3 { x: 0., y: 0., z: 0., }), ),
    (3, wit::component::ComponentTypeParam::TypeF32(0.1)),
    (4, wit::component::ComponentTypeParam::TypeMat4(identity)),
    (5, wit::component::ComponentTypeParam::TypeMat4(identity)),
    (6, wit::component::ComponentTypeParam::TypeF32(1.)),
    (7, wit::component::ComponentTypeParam::TypeF32(1.)),
    (8, wit::component::ComponentTypeParam::TypeEmpty(())),
    (9, wit::component::ComponentTypeParam::TypeEmpty(())),
    (10, wit::component::ComponentTypeParam::TypeEmpty(())),
    ( 11, wit::component::ComponentTypeParam::TypeVec3(wit::types::Vec3 { x: 5., y: 5., z: 4., }), ),
    ( 12, wit::component::ComponentTypeParam::TypeVec3(wit::types::Vec3 { x: 0., y: 0., z: 0., }),
    ),
];
println!("Guest: {entity:?}");
wit::entity::spawn(&entity);

// Host:
fn spawn(
    &mut self,
    args: Vec<(u32, component::ComponentTypeResult)>,
) -> anyhow::Result<types::EntityId> {
    println!("---");
    println!("Host: {args:?}");
    println!("---");
    anyhow::bail!("Unimplemented");
}

This results in the following output:

Guest: [(0, ComponentTypeParam::TypeQuat(Quat { x: 0.0, y: 0.0, z: 0.0, w: 1.0 })), (1, ComponentTypeParam::TypeVec3(Vec3 { x: 1.0, y: 1.0, z: 1.0 })), (2, ComponentTypeParam::TypeVec3(Vec3 { x: 0.0, y: 0.0, z: 0.0 })), (3, ComponentTypeParam::TypeF32(0.1)), (4, ComponentTypeParam::TypeMat4(Mat4 { x: Vec4 { x: 1.0, y: 0.0, z: 0.0, w: 0.0 }, y: Vec4 { x: 0.0, y: 1.0, z: 0.0, w: 0.0 }, z: Vec4 { x: 0.0, y: 0.0, z: 1.0, w: 0.0 }, w: Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 1.0 } })), (5, ComponentTypeParam::TypeMat4(Mat4 { x: Vec4 { x: 1.0, y: 0.0, z: 0.0, w: 0.0 }, y: Vec4 { x: 0.0, y: 1.0, z: 0.0, w: 0.0 }, z: Vec4 { x: 0.0, y: 0.0, z: 1.0, w: 0.0 }, w: Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 1.0 } })), (6, ComponentTypeParam::TypeF32(1.0)), (7, ComponentTypeParam::TypeF32(1.0)), (8, ComponentTypeParam::TypeEmpty(())), (9, ComponentTypeParam::TypeEmpty(())), (10, ComponentTypeParam::TypeEmpty(())), (11, ComponentTypeParam::TypeVec3(Vec3 { x: 5.0, y: 5.0, z: 4.0 })), (12, ComponentTypeParam::TypeVec3(Vec3 { x: 0.0, y: 0.0, z: 0.0 }))]
---
Host:  [(0, ComponentTypeResult::TypeQuat(Quat { x: 0.0, y: 0.0, z: 0.0, w: 1.0 })), (12, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeEmpty(())), (0, ComponentTypeResult::TypeVec2(Vec2 { x: 1.7e-44, y: 0.0 })), (12, ComponentTypeResult::TypeVec3(Vec3 { x: 0.0, y: 0.0, z: 0.0 })), (0, ComponentTypeResult::TypeEmpty(()))]
---

The first value is correct, but everything afterwards is corrupt. Adding the following to the list of components

( 13, wit::component::ComponentTypeParam::TypeList( wit::component::ComponentListTypeParam::TypeString(&["Hello", "world!"]), ), ),

results in a more direct failure:

Error: error while executing at wasm backtrace:
    0: 0x306036 - wit-component:shim!indirect-entity-spawn
    1: 0x1aadb - guest::wit::entity::spawn::h487a4080df6b6eec
    /* ... */

Caused by:
    unexpected discriminant: 225

Reproduction

I've uploaded my minimal reproduction here: https://github.com/philpax/wit-bindgen-variant-codegen-issue

This uses wasmtime 3ff3994a12, wasi-common/wasi-cap-std-sync (preview2-prototyping) 75f8fc615 and wit-bindgen 39030f9ada. I was previously using wasmtime b5e9fb710b as it was the closest version I could find to 6.0.0 that supported the wit hierarchy I'd set up, but I updated from that version to see if the latest version would alleviate this issue.

Running cargo run in the root (I've uploaded a precompiled guest.wasm) should show the issue immediately.

The guest can be built by going into guest and running cargo build && wasm-tools component new ./target/wasm32-wasi/debug/guest.wasm -o guest.wasm --adapt wasi_snapshot_preview1.wasm.

Notes

  • I have a custom stubbed wasmtime_wasi based on the work in preview2-prototyping. I didn't want to convert my existing bindings to async, which P2-P uses (which would require the wasmtime host to use async), so I figured it'd be easier to replicate the interface and stub out everything we didn't currently need.
  • wit-bindgen code is generated by the host's build script, not using the guest macro. This is done so that the guest code/API can be uploaded to crates.io - otherwise, the guest would have a Git dependency on wit-bindgen, which would block publishing. As far as I can tell, the build-script codegen should work identically to the guest macro. We are already using this solution in production with the pre-CM version of wit-bindgen et al.
    • This also required me to subsume wit-bindgen's Rust guest code. Luckily, there's not much of it. I do a few minor changes to the generated code to support the use of the code I subsumed, but it's mostly just importing wit_bindgen::rt from super.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions