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

Extern ref types #586

Merged
merged 22 commits into from
Oct 26, 2020
Merged

Extern ref types #586

merged 22 commits into from
Oct 26, 2020

Conversation

ethanfrey
Copy link
Member

@ethanfrey ethanfrey commented Oct 19, 2020

Closes #584

Adding types and showing the diff for hackatom contract.

It was just changing the function definitions, and then search replace in the test code: &mut deps -> deps.as_mut() and &deps -> deps.as_ref()


Update: After some nice conversations with an advanced Rust coder, I realized I could remove all the <S: Storage, A: Api, Q: Querier> generics in contract code if I just use eg. api: &dyn Api instead of api: &A. This definitely remove a lot of visual complexity from the contracts and makes them more approachable for Rust beginners.

This does involve some more changes to the contract code, but mainly removing complexity.

I would love some feedback when you can review. I think the newer contract code is much easier for devs starting with CosmWasm. And note that the vm code doesn't have to change at all, so this is backwards compatible with the wasm code generated from 0.11 (so we can stick with cosmwasm_version_4)

@ethanfrey ethanfrey marked this pull request as ready for review October 24, 2020 21:54
@ethanfrey ethanfrey added this to the 0.12.0 milestone Oct 24, 2020
@ethanfrey ethanfrey requested a review from maurolacy October 25, 2020 09:41
@@ -22,3 +22,43 @@ impl<S: Storage, A: Api, Q: Querier> Deps<S, A, Q> {
}
}
}

pub struct DepsMut<'a, S: Storage, A: Api, Q: Querier> {
pub storage: &'a mut S,
Copy link
Contributor

@maurolacy maurolacy Oct 25, 2020

Choose a reason for hiding this comment

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

Not sure I totally understand the problem (or the full space of the problem) but, if you want to provide discretionary mutable access to Storage, why not wrap Storage itself in a RefCell, i. e.:

pub storage: RefCell<&'a S>

That way, you can continue to use Deps as before (i.e. like &deps), and just borrow_mut() storage when mutable access is required.

Copy link
Member Author

@ethanfrey ethanfrey Oct 25, 2020

Choose a reason for hiding this comment

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

I believe I would need RefCell<S> to be able to get a &mut S from &RefCell.

The cell can't give out something it never had

Copy link
Member Author

Choose a reason for hiding this comment

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

One reason motivating this (before removing generics) was that previously the caller had to own S and Q to be able to give a reference out. If the owner has &mut S and &Q that is all the access the handle function needs, but it could not actually call it. (I hit this trying to build a test framework).

Once I started using references, the idea of removing generics and using &dyn came up and got rid of a lot of noise

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe I would need RefCell<S> to be able to get a &mut S from &RefCell.

I see, and you're right. And owning those objects is precisely what you're trying to avoid.

Copy link
Contributor

Choose a reason for hiding this comment

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

Kept thinking about this... I guess what I don't like (besides the multiplication of entities), is that this looks like a Rust "antipattern". I mean, in Rust the access mode (mut vs. const) is usually resolved by modifiers, or through a method call (as_mut() and such); not through object / struct definition.

Also, wouldn't make sense to just own Storage, or a copy of it? You can still have references for Querier and Api, but wouldn't make sense to pass Storage by value (or inside a RefCell if needed)?
I know this is against what you're trying to do here. Just pondering.

And, finally, maybe this can be resolved by passing a reference to a generic container, like Deps, but one that is not built on purpose, just to pass references, but is the original handler / container for those types. Not sure if this can be done given the current architecture / design decisions, though; and, if this wouldn't bring their own share of problems with multiple mutable access, borrowing, etc.

Copy link
Contributor

@maurolacy maurolacy left a comment

Choose a reason for hiding this comment

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

API looks good to me, and it's clearer/simpler than the previous one.

My only concern would be the general one I posted as a comment, i.e. if mutability selection could be restricted only to the storage member somehow.

@webmaster128
Copy link
Member

Haha, I see Mauro is stuggeling with this problem in a similar way I do. This concern is not about getting it compile, but this feeling that when things get super complicationd, you are probably doing something wrong. It feels like Rust is screeming don't try to code Java with me.

But looking at the facts, I think we can agree on the following:

  1. A contract only needs a reference to a Storage and does not need to own it
  2. For contract developers it is desireable to group Storage, Api and Queries into one thing.
  3. There is no generics for mutability, such that handle and query need to receive different structs, one with a mutable and one with an immutable storage reference.

This at the end of the day make Ethan's solution the only one I see working. And I send a lot of time thinking about alternatives.

One change I'd like to propose (I can send a PR for the implementation) is to make the set of references the primary structure Deps: the "external dependencies" of a contract are a set of references. This gives us Deps/DepsMut as better names for the primariy structures (right now DepsRef is not a ref to Deps but something different). Owning dependencies is really a testing problem. In the production code we just have

fn _do_query<M, E>(/* … */) -> ContractResult<QueryResponse> {
    // ...
    let storage = ExternalStorage::new();
    let api = ExternalApi::new();
    let querier = ExternalQuerier::new();
    let deps = Deps {
        storage: &storage,
        api: &api,
        querier: &querier,
    };
    query_fn(deps, env, msg).into()
}

We then can have a cosmwasm_std::testing::OwnedDeps which is the result type of cosmwasm_std::testing::mock_dependencies.


Nice change using &dyn Storage instead of the generics.

Copy link
Member

@webmaster128 webmaster128 left a comment

Choose a reason for hiding this comment

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

A few things here. Biggest wish is the renaming (and change of perspective) as described in #586 (comment)

packages/std/src/deps.rs Show resolved Hide resolved
packages/std/src/traits.rs Show resolved Hide resolved
MIGRATING.md Outdated Show resolved Hide resolved
#[derive(Copy, Clone)]
pub struct QuerierWrapper<'a> {
querier: &'a dyn Querier,
}
Copy link
Member

Choose a reason for hiding this comment

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

Why did you create this extra type?

Copy link
Member Author

Choose a reason for hiding this comment

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

To solve a headache.

The Querier trait was implementing a lot of automatic functions (beyond the one we implement). These are generic functions. &dyn Querier as a trait objects does not support traits with generic functions (as there is not a fixed vtable with all possible trait functions). You can look in the history, there was a place where I pulled out S and A to &dyn Storage and &dyn Api, but left Querier. I had to add this type to get Querier to compile.

All those Querier "helper functions" can no longer be on the trait. Wrapping it in a struct seemed the easiest way to add it. But I guess using a newtype pub struct QuerierWrapper<'a>(&'a dyn Querer) is actually a clearer representation of how we wrap - like Binary/CanonicalAddr wrap Vec<u8>.

Secondly, we can adjust naming. Maybe QuerierWrapper=>Querier (as this is what contract devs will be seeing), and Querier => QuerierTrait or such, which is only used internally by core devs. Same way to make Deps, DepsRef and simplify the naming in the public API by giving the internals a more clunky naming.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did the rename in a7efaa3. Happy to revert it if you don't like it, but I think this solves the issue with a cleaner public API.

@maurolacy
Copy link
Contributor

maurolacy commented Oct 26, 2020

Good analysis. I would like to challenge your point 3 above. There's a way to handle a reference with dynamically checked borrowing rules, and that's a RefCell.

Mutably borrowing from your code above, what I'm proposing is something along:

fn _do_query<M, E>(/* … */) -> ContractResult<QueryResponse> {
    // ...
    let storage = ExternalStorage::new();
    let api = ExternalApi::new();
    let querier = ExternalQuerier::new();
    let deps = Deps {
        storage: RefCell::new(storage),
        api: &api,
        querier: &querier,
    };
    query_fn(&deps, env, msg).into()
}

So, basically, passing a reference to Deps in query_fn() (instead of passing Deps by value), and mutably (and dynamically) borrowing Storage when needed.

This still copies Storage in RefCell::new(), but if the only Deps around is that one, which is passed by reference everywhere, that's OK.
You can even do something like

RefCell::new(ExternalStorage::new())

at initialization. Which is the logical thing to do, by the way.

@ethanfrey
Copy link
Member Author

Mauro, I explicitly only want to pass in the minimal ownership needed - not move in storage (or even &mut Storage) if I will only be using &Storage. A lot of the practical type wrangling came from CosmWasm/cw-plus#130 which shows the issue with passing in owned values, and was my first draft on the DepsMut, DepsRef idea.

If I run recursive queries on a contract, that would be allowed in the Rust type system if they all have &Storage, but no more. I guess I could pass in &RefCell<Storage> and trust none of them try to get it mutable at runtime. But I also see &Storage as a much simpler API for contract developers than &RefCell<Storage>.

The point is this is not a normal struct, it really is just combining three args into one as they are commonly passed together. Simon did a spike to just pull them into 3 arguments and it made the API much messier. There has been quite some fiddling around to look for designs here, and supporting 2 different structs in the public API (Deps and DepsRef now) seems to be adding minimal complexity in order to save other issues.

@maurolacy
Copy link
Contributor

But I also see &Storage as a much simpler API for contract developers than &RefCell<Storage>.

True. And, if the goal is precisley to make the API simpler, this is an important point.
Also, in terms of safety, what you say about restricting mutable access to static / compile time borrows makes sense.

I tend to forget we're dealing with an API here, and that it must not only work with the existing code base, but also be easy to use in future contracts / for future users.

@webmaster128
Copy link
Member

The issue with RefCell<Storage> is that it represents ownership of the storage. The contract implementation is then free to destroy the storage and never give it back to the caller. It can even disallow the caller to have a read-only reference.

Copy link
Member

@webmaster128 webmaster128 left a comment

Choose a reason for hiding this comment

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

Nice, we are getting there :)

Thanks for the explanation around QuerierWrapper. I'd revert a7efaa3. I think the old name was better, or at least more consistent with the rest. We'll find a way to nicelify it. We can use a newtype or a struct, no real preference. The newtype might be better to express the intend you explained.

}

#[derive(Copy, Clone)]
pub struct DepsRef<'a> {
Copy link
Member

Choose a reason for hiding this comment

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

I'd call this Deps and the mutable version DepsMut. I think those names are better than the "Ref" suffix. This also uses the typical immutable default.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, doing that now, then preparing for merge

packages/std/src/deps.rs Outdated Show resolved Hide resolved
Copy link
Member

@webmaster128 webmaster128 left a comment

Choose a reason for hiding this comment

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

Alright, cool. Happy when CI is

@@ -1,12 +1,12 @@
use cosmwasm_std::{
attr, Api, BankMsg, Binary, Deps, Env, HandleResponse, InitResponse, MessageInfo,
MigrateResponse, Order, Querier, StdError, StdResult, Storage,
attr, BankMsg, Binary, Deps, DepsMutMut, Env, HandleResponse, InitResponse, MessageInfo,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
attr, BankMsg, Binary, Deps, DepsMutMut, Env, HandleResponse, InitResponse, MessageInfo,
attr, BankMsg, Binary, Deps, DepsMut, Env, HandleResponse, InitResponse, MessageInfo,

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, fixed this. And also rebuilt the test contract.

Getting ready to merge.

@webmaster128 webmaster128 added the automerge See mergify.io label Oct 26, 2020
@mergify mergify bot merged commit fe8557e into master Oct 26, 2020
@mergify mergify bot deleted the extern-ref-types branch October 26, 2020 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge See mergify.io
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rethink Extern
3 participants