Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support dynamic function calls in component model #4442

Merged
merged 10 commits into from
Jul 25, 2022

Conversation

dicej
Copy link
Contributor

@dicej dicej commented Jul 13, 2022

This addresses #4310, introducing a new component::values::Val type for
representing component values dynamically. It also adds a call method to
component::func::Func, which takes a slice of Vals as parameters and returns
a Result<Val> representing the result.

As an implementation detail, I've also added a component::values::Type type,
which is an owned, despecialized version of
wasmtime_environ::component::InterfaceType. That serves two purposes:

  • It allows us to despecialize as the first step, which reduces the number of cases to consider when typechecking and lowering.
  • It avoids needing to borrow the store both mutably and immutably when lowering, as we would if we used InterfaceTypes.

Finally, I happened to notice that the ComponentType::SIZE32 calculation in
expand_record_for_component_type needed a correction, so I did that.

Signed-off-by: Joel Dice joel.dice@fermyon.com

@dicej
Copy link
Contributor Author

dicej commented Jul 13, 2022

@alexcrichton @jameysharp This addresses #4310. I plan to push one more commit to add "sad path" tests, but this should be complete otherwise.

Please pay particular attention to the unsafe block in Func::call -- I copied it from TypedFunc::call_raw and modified it, but I want to make sure I did that correctly.

@github-actions github-actions bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Jul 13, 2022
@github-actions
Copy link

Subscribe to Label Action

cc @peterhuene

This issue or pull request has been labeled: "wasmtime:api"

Thus the following users have been cc'd because of the following labels:

  • peterhuene: wasmtime:api

To subscribe or unsubscribe from this label, edit the .github/subscribe-to-label.json configuration file.

Learn more.

@dicej
Copy link
Contributor Author

dicej commented Jul 13, 2022

I could use some advice about how to address the CI issue. I've added a new wasmtime-component-util crate with code that I wanted to share between wasmtime and wasmtime-component-macro. The publish script doesn't like that, though:

Caused by:
  no matching package named `wasmtime-component-util` found
  location searched: registry `crates-io`
  required by package `wasmtime-component-macro v0.40.0 (/home/runner/work/wasmtime/wasmtime/target/package/wasmtime-component-macro-0.40.0)`
thread 'main' panicked at 'failed to verify "crates/component-macro/Cargo.toml"', scripts/publish.rs:462:9

Is there a better way to reuse that code, or else some way to bootstrap the dependency and make publish happy?

@jameysharp
Copy link
Contributor

I didn't know this, but there's a comment in publish.rs that says "note that this list must be topologically sorted by dependencies". Does it work if you swap the order of wasmtime-component-util and wasmtime-component-macro?

@dicej
Copy link
Contributor Author

dicej commented Jul 13, 2022

I didn't know this, but there's a comment in publish.rs that says "note that this list must be topologically sorted by dependencies". Does it work if you swap the order of wasmtime-component-util and wasmtime-component-macro?

Oh good call. I did know that, but forgot about it. Will try that now.

Copy link
Contributor

@jameysharp jameysharp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it to @alexcrichton to decide whether to approve this, but I have a few small observations scattered through.

Overall I'm happy with this PR. I'm disappointed at how much crates/wasmtime/src/component/values.rs duplicates code that's in crates/wasmtime/src/component/func/typed.rs and elsewhere, but I don't see a solid alternative right now, so let's run with it I guess.

@alexcrichton, I think you can focus your review on the changes in crates/wasmtime/src/component/func.rs. I feel pretty confident approving the rest, but that file has the one new unsafe block and generally the trickiest interactions with other parts of wasmtime.

crates/component-util/src/lib.rs Outdated Show resolved Hide resolved
@@ -1095,7 +1093,7 @@ impl WasmStr {
self._to_str(store.into().0)
}

fn _to_str<'a>(&self, store: &'a StoreOpaque) -> Result<Cow<'a, str>> {
pub(crate) fn _to_str<'a>(&self, store: &'a StoreOpaque) -> Result<Cow<'a, str>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love making a function named _to_str visible outside the current module. But I see all you have at the call site is a &StoreOpaque so you can't call to_str instead. Unless @alexcrichton has a better idea I guess this is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that felt awkward to me, too. Perhaps we could rename it to something like to_str_with_store_opaque? I haven't yet wrapped my head around all the different flavors of store, so perhaps there's a better option.

crates/wasmtime/src/component/values.rs Outdated Show resolved Hide resolved
),
)?;
}
(Val::Flags { count, value }, Type::Flags(_)) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you got count from Type::Flags here instead of from Val::Flags, I don't see that you'd need to store a count in Val::Flags at all. It looks like everywhere that you need the count, you have the type available.

I guess you did it this way to force the host application to specify how many flags they expect to be passing in as parameters, so you can verify in typecheck that their expectation was correct—and so you can inform the host application about how many flags they received.

I go back and forth on whether that kind of checking is worth doing in this sort of dynamic call situation. @alexcrichton, what's your take?

crates/wasmtime/src/component/func.rs Outdated Show resolved Hide resolved
Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Personally I think that this is a really tricky problem and I don't think that there's any good answer right now to how to implement this. On one hand I think that this PR is probably sufficient from and implementation and functionality point of view, but on the other hand I don't think that this is suitable for any performance-conscious scenario. I don't know if this is the best choice of tradeoffs at this time because to me at least we're generally trying to get to a semi-final state for the component model where the known deficiencies are accounted for.

I'm personally wary of throwing in the towel on performance so soon here but I also don't have any alternatives. Even if we go with a strategy like the one you've implemented here there's a lot of open questions to me which I don't think have a clear answer:

  • One thing is how types are represented. I would naively expect to be able to, given a Val, ask for its type along the lines of the wasmtime::Val::ty method. Given this current implementation I don't think that this is possible. I think, though, it might be possible to add this in where a "type index" of some sort stood next to the Record/Variant/Flags types.

  • On the topic of type information one thing that we'll eventually want to add to the component model implementation is the ability to introspect a component for the types of its imports and exports. For example currently Wasmtime has wasmtime::Module::imports and wasmtime::Module::exports but the equivalent for the component model isn't implemented yet. I believe an implementation of this will require something along the lines of the enum Type you've started here but I don't think we want an "owned Type" as you've sketched out which recursively owns all of its contents. Instead I think we'll want a design similar to the ComponentTypesBuilder interface right now where there's a "bag of everything" and types all index into that. I don't think that you need to implement Component::{imports,exports} in this PR here, but at the same time I'd prefer to avoid having another internal representation of types different from what we already have. Adding this type information may be inevitable for this API because I do think that records/variants at least should be created with their type information up-front (rather than "if you have the right number of fields it all works out")

  • One thing I've worried about historically is the size/alignment calculation. Right now in the typed API it's generally all statically done at compile time but with this dynamic API I'm a little worried that with a recursively-defined size calculation it runs the risk of being $O(n^2)$ performance if the size is requested at many layers of the type tree. It might be better to start storing canonical size/align information for aggregates in the ComponentTypes structure rather than calculating it on the fly here.

That's at least an initial dump of thoughts I have on the internals here. I wish I had a good answer for this like this ahead of time. Otherwise though while I don't think we need to get this PR to a perfect spot before landing I do think we should get it into a state where the major pieces of functionality are there. For me those are:

  • There shouldn't be a duplicate internal representation of type information (currently enum Type is crate-local)
  • Personally I think that Val::type(maybe_some_arg) -> Something should exist to get the type from a standalone Val

Those two points may have a significant degree of fallout depending on the implementation direction.


On the topic of code duplication, to some degree that's just life in the component model. I've written the same size/alignment calculation in maybe 4 crates now at this point. It's slightly different each time and I've found it really hard to share code. In general I don't think my development strategy here has been really all that tenable or scalable historically and I'd ideally like to change it.

Where possible I think thinks should be shared. For example reusing Lift::load here and Lower::store I think is a great idea. If possible it might be good to try and reuse ComponentType::typecheck here as well. Otherwise though if something is difficult to share I don't think it's worth bending over backwards to have precisely one implementation of something. To that end that's also why we want a strong fuzzing implementation here eventually.

Specifically for Func::call though I would ideally like to share more of that with TypedFunc::call. There's a fair amount of nontrivial logic, especially around handling post_return, may_enter, etc. If you would be ok with it I think it would be good to try to make a generic helper so that call_unchecked_raw is only called once or something roughly along those lines. Again though I wouldn't bend over backwards too much for this, I just think it's good to try to investigate and see how it works out.


Otherwise as a random final note I think that you'll want to move post_return from TypedFunc to Func. Additionally I think you'll want to assert specific errors on function calls since I think all the .is_err() assertions are passing due to "needs a call to post_return" rather than type-checking errors that should otherwise be happening.

@dicej
Copy link
Contributor Author

dicej commented Jul 14, 2022

Thanks @alexcrichton and @jameysharp for your thoughtful and detailed feedback.

Alex: I agree with what you said about representing types and wanting to be able to get a type from a Val. For my first pass at this, I didn't have a Type enum -- I just used InterfaceType everywhere. That worked pretty well until the borrow checker made its verdict. I couldn't figure out how to pass a &ComponentTypes to Val::lower since it also takes a &mut StoreContextMut<T> since the former was borrowed from the latter. Now that I reflect on it, though -- I see I don't need to pass both of those things since I can always get the former from the latter "just in time" by reborrowing when needed. I'll give that another shot and see what happens. I'll also see what I can do about deduplicating Func::call and TypedFunc::call.

@alexcrichton
Copy link
Member

Hm actually I do think you'll want to pass around &ComponentTypes if you can. One trick I used with instances is that I took it out of the store temporarily, placing it back into the store when I was done with it during the function call.

I think this will want to be an argument because if an end user creates a Val they'll need to create it with a type for cases like a Record, so the end-user may create a foreign ComponentTypes. Hm maybe at least, I'm not sure. They'll always want to use types already registered within the component so perhaps not... Well in any case as long as it works!

@dicej
Copy link
Contributor Author

dicej commented Jul 14, 2022

@alexcrichton Would you please point me to the code where you do that trick of pulling the ComponentTypes out of the store temporarily?

Also, here's a quick sketch of what I'm thinking. Please let me know what you think.

pub enum RecordIndex {
    Unit,
    Record(TypeRecordIndex),
    Tuple(TypeTupleIndex)
}

pub enum VariantIndex {
    Variant(TypeVariantIndex),
    Enum(TypeEnumIndex),
    Union(TypeUnionIndex),
    Option(TypeInterfaceIndex),
    Expected(TypeExpectedIndex)
}

pub enum Val {
    Bool(bool),
    S8(i8),
    U8(u8),
    S16(i16),
    U16(u16),
    S32(i32),
    U32(u32),
    S64(i64),
    U64(u64),
    Float32(u32),
    Float64(u64),
    Char(char),
    String(Rc<str>),
    List {
        element_type_index: TypeInterfaceIndex,
        values: Rc<[Val]>
    },
    Record {
        type_index: RecordIndex,
        fields: Rc<[Val]>
    },
    Variant {
        type_index: VariantIndex,
        discriminant: u32,
        value: Rc<Val>,
    },
    Flags {
        type_index: TypeFlagsIndex,
        count: u32,
        value: Rc<[u32]>,
    },
}

impl Val {
    pub fn ty() -> InterfaceType {
        ...
    }

    ...
}

@dicej
Copy link
Contributor Author

dicej commented Jul 14, 2022

Regarding the code sketch above: we'd need to add methods to ComponentTypes to create composite Vals, I guess.

@alexcrichton
Copy link
Member

That seems like a good start to me. I'd probably go ahead and use Box<T> everywhere instead of Rc<T> for Send/Sync reasons. We're already nowhere near "this is almost optimal perf" so I don't think there's much benefit to making it cheaply clone-able.

Otherwise though the type information will be a bit tricky since I would prefer to not simply reexport everything within wasmtime_environ::component::types. I think the wasmtime crate will need to have its own parallel type hierarchy with methods for us to audit, document, and maintain as part of an embedder API.

@dicej
Copy link
Contributor Author

dicej commented Jul 14, 2022

@alexcrichton Can you elaborate on what you think the parallel type hierarchy in the wasmtime crate would look like? I'm having trouble imagining how to build that in such a way that it reuses ComponentTypes behind the scenes. I also wonder whether it would represent just despecialized types or the full range including specializations like unions, options, etc.

Here's my latest sketch:

pub struct TypeIndex(TypeInterfaceIndex);
pub struct RecordTypeIndex(TypeRecordIndex);
pub struct TupleTypeIndex(TypeTupleIndex);
pub struct VariantTypeIndex(TypeVariantIndex);
pub struct EnumTypeIndex(TypeEnumIndex);
...

pub enum AnyRecordTypeIndex {
    Unit,
    Record(RecordTypeIndex),
    Tuple(TupleTypeIndex)
}

pub enum AnyVariantTypeIndex {
    Variant(VariantTypeIndex),
    Enum(EnumTypeIndex),
    Union(UnionTypeIndex),
    Option(TypeIndex),
    Expected(ExpectedTypeIndex)
}

pub enum Type {
    Bool,
    S8,
    U8,
    S16,
    U16,
    S32,
    U32,
    S64,
    U64,
    Float32,
    Float64,
    Char,
    String,
    List(TypeIndex),
    Record(AnyRecordTypeIndex),
    Variant(AnyVariantTypeIndex),
    Flags(FlagTypeIndex),
}

impl Type {
    fn from(ty: &InterfaceType) -> Self {
        ...
    }
}

pub struct Field<'a> {
    name: &str,
    ty: Type,
}

pub struct Types(ComponentTypes);

impl Types {
    pub fn record_name(&self, index: RecordTypeIndex) -> &str {
        &self.0[index.0].name
    }
    
    pub fn record_fields(&self, index: RecordTypeIndex) -> impl Iterator<Field> {
        &self.0[index.0].fields.iter().map(|field| Field { name: &field.name, ty: Type::from(&field.ty) })
    }

    ...
}

pub enum Val {
    Bool(bool),
    S8(i8),
    U8(u8),
    S16(i16),
    U16(u16),
    S32(i32),
    U32(u32),
    S64(i64),
    U64(u64),
    Float32(u32),
    Float64(u64),
    Char(char),
    String(Box<str>),
    List {
        element_type: Type,
        values: Box<[Val]>
    },
    Record {
        ty: RecordType,
        fields: Box<[Val]>
    },
    Variant {
        ty: VariantType,
        discriminant: u32,
        value: Box<Val>,
    },
    Flags {
        ty: FlagsType,
        count: u32,
        value: Box<[u32]>,
    },
}

impl Val {
    pub fn ty(types: &Types) -> Type {
        ...
    }

    ...
}

The idea is to wrap the relevant wasmtime_environ index types with wasmtime equivalents, plus a Types type which wrap's wasmtime_environ's ComponentTypes and provide methods for getting names, fields, cases, etc. for various composite types. Lots of boilerplate, of course, but happy to do it if that's the right approach.

@alexcrichton
Copy link
Member

That looks along the lines of what I was thinking, but to be clear I haven't fully thought this through. I've got what I think should be the constraints in my head but said constraints don't always translate well to code... In any case what you've described captures all the main constraints I was thinking of. The actual ComponentTypes under the hood will be wrapped up in an Arc so we can "cheaply" clone that and move it around. One possible design tradeoff is to bundle the Arc into the Type itself. For example we could hide all the indices and instead have something liek:

enum Type {
    // ...
    Record(RecordType),
    // ...
}

struct RecordType {
    index: TypeRecordIndex,
    types: Arc<ComponentTypes>,
}

That's a whole lot of Arc cloning though and will lose perf very quickly. Another possible alternative is:

enum Type<'a> {
    // ...
    Record(RecordType<'a>),
    // ...
}

struct RecordType<'a> {
    index: TypeRecordIndex,
    types: &'a ComponentTypes,
}

but then getting that from a Val gets tricky. One possibility though could be:

enum Val {
    // ...
    Record(Record),
    // ...
}

struct Record {
    // ...
    ty: TypeRecordIndex,
    instance: Instance, // just an index
}

or something like that maybe?

I may be too naive also trying to prematurely optimize this. It might make the most sense to go with Type containing Arc<ComponentTypes> for now. I do think ideally we would hide all the indexes used under the hood if we can, but that's not necessarily required.

@dicej
Copy link
Contributor Author

dicej commented Jul 14, 2022

I've tried a few variations. Here's the latest:

pub struct Types<'a>(&'a ComponentTypes);

pub struct Case<'a> {
    name: &'a str,
    ty: Type,
}

#[derive(Copy, Clone)]
pub struct VariantIndex(TypeVariantIndex);

impl VariantIndex {
    pub fn name(self, types: Types) -> &str {
        &types.0[self.0].name
    }

    pub fn cases(self, types: Types) -> impl ExactSizeIterator<Case> {
        types.0[self.0].cases.iter().map(|case| Case {
            name: &case.name,
            ty: Type::from(&case.ty),
        })
    }
}

pub enum Type {
    // ...
    Variant(VariantIndex),
    // ...
}

impl Type {
    fn from(ty: &InterfaceType) -> Self {
        match ty {
            // ...
            InterfaceType::Variant(index) => Type::Variant(VariantIndex(*index)),
            // ...
        }
    }
}

pub enum Val {
    // ...
    Variant(Variant),
    // ...
}

impl Val {
    pub fn ty(&self) -> Type {
        match self {
            // ...
            Self::Variant { ty, .. } => Type::Variant(ty),
            // ...
        }
    }

    // ...
}

pub struct Variant {
    ty: VariantIndex,
    discriminant: u32,
    value: Box<Val>,
}

impl Variant {
    pub fn try_new(
        ty: VariantIndex,
        types: Types,
        discriminant: u32,
        value: Box<Val>,
    ) -> Result<Self> {
        // ... (here we would verify that `discriminant` and `value` match `ty` and throw an error otherwise)

        Ok(Self {
            ty,
            discriminant,
            value,
        })
    }
}

impl Func {
    pub fn types(&self, store: impl AsContext<'a>) -> Types<'a> {
        Types(&store.as_context()[self.0].types)
    }

    pub fn get_param_type(&self, store: impl AsContext, index: u32) -> Type {
        let data = &store.as_context()[self.0];
        Type::from(data.types[data.ty].params[index])
    }

    pub fn get_result_type(&self, store: impl AsContext) -> Type {
        let data = &store.as_context()[self.0];
        Type::from(data.types[data.ty].result)
    }

    // ...
}

#[test]
fn foo() -> Result<()> {
    let engine = super::engine();
    let mut store = Store::new(&engine, ());
    let component = Component::new(
        &engine,
        make_echo_component_with_params(
            r#"(variant (case "A" u32) (case "B" s32))"#,
            &[super::Type::I32, super::Type::I32],
        ),
    )?;
    let instance = Linker::new(&engine).instantiate(&mut store, &component)?;
    let func = instance.get_func(&mut store, "echo").unwrap();
    let ty = func.get_param_type(&store, 0);

    if let Type::Variant(index) = ty {
        let input = [Val::Variant(Variant::try_new(ty, func.types(&store))?)];
        let output = func.call(&mut store, &input)?;

        assert_eq!(input[0], output)
    } else {
        unreachable!()
    }

    Ok(())
}

The ergonomics aren't outstanding, but the efficiency is pretty good. My rationale for still exposing indexes is that Wasm itself uses indexes everywhere, so the paradigm will at least be familiar to users.

Happy to just use an Arc<ComponentTypes> everywhere if we want to prioritize ergonomics, though.

@dicej
Copy link
Contributor Author

dicej commented Jul 15, 2022

@alexcrichton Any thoughts on what I posted above? I want to make sure we agree on the design of Val and Type before I start rewriting things.

Meanwhile, I'll start working on #4307 and come back to this once we have consensus.

@alexcrichton
Copy link
Member

Apologies for the delay, busy day yesterday and this morning! That looks good to me though. Some things may be tweaked over time but I think that's good to land.

@dicej
Copy link
Contributor Author

dicej commented Jul 18, 2022

I started implementing this according to what I proposed above, but now I'm having second thoughts. At first, I thought the natural place to do type checking was when creating the Val. For example:

impl Variant {
    pub fn try_new(
        ty: VariantIndex, // just a wrapper around a TypeVariantIndex
        types: Types,  // just a wrapper around a &ComponentTypes
        discriminant: u32,
        value: Box<Val>,
    ) -> Result<Self> {
        // ... (here we would verify that `discriminant` and `value` match `ty` and throw an error otherwise)

        Ok(Self {
            ty,
            discriminant,
            value,
        })
    }
}

However, that has a big hole: an embedding programmer could accidentally create a Val using a Types from one component instance and then use it with a different one. Since Val only has indexes, there's nothing to tie it to a specific component instance. They might even get confused and try to build new Vals which contain other Vals from other component instances.

Also, my plan was to provide accessor methods for e.g. getting the cases for a Type, such that each accessor would take a Types parameter. That would put the onus on the embedding programmer to make sure to always pass the correct Types instance, which I could imagine might get confusing when wrangling many component instances at once.

So now I think we're better off with @alexcrichton's suggestion of either embedding an Arc<ComponentTypes> or &ComponentTypes inside each relevant Type' variant alongside the index. Each corresponding Val variant would do the same. We'd still need to be careful to prevent the programmer from building Vals which contain Vals from other component instances, and also check once more in Func::call that the parameters all reference the correct ComponentTypes, but at least that's quick and easy to do.

Another crazy idea would be to borrow a trick from GhostCell and tag Vals and Types with a synthetic lifetime which uniquely identifies the component instance they may be used with. That would probably be optimal from a performance standpoint, as well as catch issues at compile time instead of runtime, but perhaps it's a bit too exotic for the problem at hand?

One final thought: @alexcrichton how strongly do you feel that one should be able to get a Type from a Val? I ask because I think there are ergonomic advantages to not tying a Val to a specific type. It certainly makes using Func::call easier -- just pass a &[Val] in without needing to first ask the Func for its parameter Types and match against each of them. It could also be nice to use the output of a Func::call from one component instance as the input to a Func::call in another instance without any explicit conversion.

@alexcrichton
Copy link
Member

Hm yes that is indeed a problem! FWIW this is solved at the store level with the store_id field in Stored<T>, basically a solution for ABA problem (ish). We could do something similar here where each component is assigned a unique index at runtime to prevent mixing types (although we would still want to sort of support equality of types across components which could perhaps get quite nasty quite quickly, unsure).

Otherwise though storing Arc<T> would also be a good solution. I don't know a ton about GhostCell myself but when I skimmed it in the past it looked like it wouldn't ever really lend itself well to an ergonomic API. I haven't tried using it historically though.

I dunno how to feel about getting a Type from a Val. I feel like it should be possible as a primitive operation, but I'm not exactly certain in this opinion. The current iteration where a record is just a list of Val also feels a bit odd to me in that there's no correlation with field names at construction to when the value is used, which seems like it may accidentally be easy to shoot yourself in the foot by mixing up field orders or things like that.

Personally I'm trying to stay analgous to the core wasm API, but I realize how it's a lot easier there than it is here. I don't really know the best answer to any of these questions. Our main use case for this is fuzzing at this point where getting a Type from Val may not matter too much.

This addresses bytecodealliance#4310, introducing a new `component::values::Val` type for
representing component values dynamically, as well as `component::types::Type`
for representing the corresponding interface types. It also adds a `call` method
to `component::func::Func`, which takes a slice of `Val`s as parameters and
returns a `Result<Val>` representing the result.

Note that I've moved `post_return` and `call_raw` from `TypedFunc` to `Func`
since there was nothing specific to `TypedFunc` about them, and I wanted to
reuse them.  The code in both is unchanged beyond the trivial tweaks to make
them fit in their new home.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej
Copy link
Contributor Author

dicej commented Jul 21, 2022

I've overhauled the code according to the above feedback, and I decided to squash and rebase since so much was changing anyway. Please let me know what you think.

Highlights to note:

  • component::types::Type is public, and Val now has a ty method to get the type of that value as a Type. This should also be suitable for use with Component::exports and Component::imports when the time comes to implement those.
  • I manged to reuse call_raw so there's minimal duplication in Func::call and no new unsafe code (but note that call_raw has moved to Func along with post_return).
  • Type checking now mostly happens during Val creation, with one final, quick, linear-time check happening in Func::call.
  • The sad path tests are now checking for specific errors.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
This also removes the redundant `store` parameter from `Type::load`.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! I left a bunch of initial comments and I'll take another pass when they're addressed.

crates/wasmtime/src/component/func/typed.rs Show resolved Hide resolved
crates/wasmtime/src/component/func.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/func.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/values.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/values.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Outdated Show resolved Hide resolved
self.desc(),
value.ty().desc()
))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't necessarily have to be handled here, but I think this typecheck will eventually want to be more recursive for better error mesages. Right now I think you could get expected record, got record which doesn't describe which record fields mismatch or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any e.g. record field mismatch would have been caught already in Type::new_record before we got this far. The only time you'd see expected record, got record would be if you used Vals from one component in another, or if your component had two identical records (i.e. same field names and types) and you mixed up which one you created the Val for. Either way, more recursion wouldn't necessarily help. This is somewhat analogous to when rustc reports a type mismatch between foo::Bar and foo::Bar because you're using two different versions of the foo crate. It's even worse here since types don't really have names in the component model.

But yes, I agree we should give a friendlier diagnostic here, e.g. hinting based on whether Handle::index or Handle::types are different, or both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've improved the error message so you'll never get expected record, got record. I attempted to go much further than that and recursively compare the types, but reusing typed::typecheck_tuple etc. turned out to be difficult in a dynamic context, and I didn't want to duplicate them either.

Also, I'm having trouble finding guidance in https://github.com/WebAssembly/component-model about when interface types are to be considered equal and when not, so I'm not sure what the correct logic is anyway. Are types considered equal if they are structurally identical?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll get a ton of guidance from the component model itself, but for now from Wasmtime's perspective given how trampolines and lifting/lowering/etc are all implemented we require exact structural equality for the embedder API. I think the intention is that subtyping would eventually be used but we don't have a great means of implementing that right now.

crates/wasmtime/src/component/types.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Outdated Show resolved Hide resolved
This fixes a few issues:

- Bad offset calculation when lowering
- Missing variant padding
- Style issues regarding `types::Handle`
- Missed opportunities to reuse `Lift` and `Lower` impls

It also adds forwarding `Lift` impls for `Box<[T]>`, `Vec<T>`, etc.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej
Copy link
Contributor Author

dicej commented Jul 21, 2022

Thanks so much for the thorough review, @alexcrichton. I've addressed some of the issues you raised (and marked the corresponding conversations "resolved") and will look at the others tomorrow.

Per review feedback, I've moved `Type::new_record` to `Record::new_val` and
added a `Type::unwrap_record` method; likewise for the other kinds of types.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
These types should compare as equal across component boundaries as long as their
type parameters are equal.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great, thanks again for all this! I think with some minor fixes here and there this is basically good to go.

Personally I feel that Type::nested should still get removed. I think that the usage in tests can be .unwrap_$thing().field_accessor() perhaps?

crates/wasmtime/src/component/func/typed.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/func/typed.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/func/typed.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Outdated Show resolved Hide resolved
We now distinguish between more failure cases to provide an informative error
message.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
- Remove `WasmStr::to_str_from_memory` and `WasmList::get_from_memory`
- add `try_new` methods to various `values` types
- avoid using `ExactSizeIterator::len` where we can't trust it
- fix over-constrained bounds on forwarded `ComponentType` impls

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej
Copy link
Contributor Author

dicej commented Jul 22, 2022

@alexcrichton I believe I've addressed everything you mentioned. I'm still unsure about how Type equality should work, but I'm hoping the current implementation is good enough to start with and can be refined later.

Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me, thanks again for your work on this! I've got some follow-ups to your comments below and a minor nit or two, but otherwise this looks good-to-go.

crates/wasmtime/src/component/values.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/component/types.rs Outdated Show resolved Hide resolved
self.desc(),
value.ty().desc()
))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll get a ton of guidance from the component model itself, but for now from Wasmtime's perspective given how trampolines and lifting/lowering/etc are all implemented we require exact structural equality for the embedder API. I think the intention is that subtyping would eventually be used but we don't have a great means of implementing that right now.


impl<T: PartialEq> PartialEq for Handle<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && Arc::ptr_eq(&self.types, &other.types)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the tuple/option/expected bits are probably not going to be necessary in the long-term if the original issue here is fixed. I think this is fine to leave as a FIXME with a follow-up issue for now though.

Otherwise though the current logic in tuple/option/expected I would expect to be baked in here or otherwise into some trait impl on T perhaps. I don't think we'd want to duplicate things and have some using PartialEq and some using manual impls.

- Move functions from `types` to `values` module so we can make certain struct fields private
- Rename `try_new` to just `new`

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Instead, I've added a FIXME comment and will open an issue to do recursive
structural equality testing.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej
Copy link
Contributor Author

dicej commented Jul 25, 2022

@alexcrichton I've addressed your latest feedback and opened #4522.

@alexcrichton alexcrichton merged commit 7c67e62 into bytecodealliance:main Jul 25, 2022
@alexcrichton
Copy link
Member

Thanks again for this @dicej!

alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 25, 2022
This commit builds on bytecodealliance/wasm-tools#690 to add support to
testing of the component model to execute functions when running
`*.wast` files. This support is all built on bytecodealliance#4442 as functions are
invoked through a "dynamic" API. Right now the testing and integration
is fairly crude but I'm hoping that we can try to improve it over time
as necessary. For now this should provide a hopefully more convenient
syntax for unit tests and the like.
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 27, 2022
This commit builds on bytecodealliance/wasm-tools#690 to add support to
testing of the component model to execute functions when running
`*.wast` files. This support is all built on bytecodealliance#4442 as functions are
invoked through a "dynamic" API. Right now the testing and integration
is fairly crude but I'm hoping that we can try to improve it over time
as necessary. For now this should provide a hopefully more convenient
syntax for unit tests and the like.
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 27, 2022
This commit builds on bytecodealliance/wasm-tools#690 to add support to
testing of the component model to execute functions when running
`*.wast` files. This support is all built on bytecodealliance#4442 as functions are
invoked through a "dynamic" API. Right now the testing and integration
is fairly crude but I'm hoping that we can try to improve it over time
as necessary. For now this should provide a hopefully more convenient
syntax for unit tests and the like.
alexcrichton added a commit that referenced this pull request Jul 27, 2022
This commit builds on bytecodealliance/wasm-tools#690 to add support to
testing of the component model to execute functions when running
`*.wast` files. This support is all built on #4442 as functions are
invoked through a "dynamic" API. Right now the testing and integration
is fairly crude but I'm hoping that we can try to improve it over time
as necessary. For now this should provide a hopefully more convenient
syntax for unit tests and the like.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wasmtime:api Related to the API of the `wasmtime` crate itself
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants