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

Exporting functions returning resources prevents implementing the guest #330

Closed
AaronFriel opened this issue Mar 25, 2024 · 12 comments
Closed

Comments

@AaronFriel
Copy link

AaronFriel commented Mar 25, 2024

A world exporting a static function that returns a resource does not behave like a constructor, despite documentation. It's not clear how to construct a resource in the guest except via constructor calls.

WIT with exported resource and constructor, vs exported function that returns resource
package aici:abi;

world aici {
  export controller;
}

interface controller {
    resource runner {
        constructor(args: list<u8>);
        init-prompt: func(arg: init-prompt-arg) -> init-prompt-result;
    }
}

Versus

package aici:abi;

world aici {
  export run: (args: list<u8>) -> runner;
}

interface controller {
    resource runner {
        init-prompt: func(arg: init-prompt-arg) -> init-prompt-result;
    }
}

In the former, a GuestRunner trait is declared which enables constructing a GuestRunner on the client, and it's clear how to implement this on the guest.

Comparison of generated Rust from `wit_bindgen`
pub trait GuestRunner:'static{
  ... 
  fn new(args:_rt::Vec::<u8>,) -> Self;
                    
  fn init_prompt(&self,arg:InitPromptArg,) -> InitPromptResult;
}
pub trait Guest {
  type Runner:GuestRunner;
}

In the latter, we instead see:

pub type Runner = aici::abi::controller::Runner;

pub trait Guest {
    fn run(args:_rt::Vec::<u8>,) -> Runner;
} 

Where Runner is an opaque resource handle.

In the latter, there is no way to implement Guest safely because there is no way to obtain a valid Runner handle.

I would expect that when a resource occurs in a return position, the bindings to implement the resource is also generated.

@AaronFriel
Copy link
Author

Related to this and #285, I think it's not obvious what -> foo for some resource foo should be.

It could be a reference to a pre-existing component, i.e.:

interface service-factory {
  resource service;

  install-service: func(name: string, service: Service);
  get-service: func(name: string) -> Service;
  create-service: func(name: string) -> Service;
}

world service-factory {
  export service-factory;
}

Can guests own Services and provide them to the host?

Can hosts own Services and provide them to the guest?

This implementation is ambiguous, and it's not clear - except perhaps by naming convention for the latter two - how this should be bound. Should:

  • Guests own services, and on the guest create-service creates resources and registers them into a guest-owned dictionary, get-service provides lookup, and on the server, install-service should be bound so that it takes a handle on the host?

  • Hosts own services, install-service expects a resource implementation, and create-service is some sort of side-effecting operation on the guest which returns a handle to the modified resource by name?

@lukewagner
Copy link
Member

A world exporting a static function that returns a resource does not behave like a constructor, despite documentation. It's not clear how to construct a resource in the guest except via constructor calls.

Ah, I think you're hitting a current-but-temporary limitation of the WIT tooling. (At the component-model level, constructor-vs-static-function is purely a naming different; they have the same types and semantics.)

In particular, in your static function example:

world aici {
  use controller.{runner};
  export run: func(args: list<u8>) -> runner;
}
interface controller {
  resource runner { ... }
}

the use in aici (which I expect is what you're writing) is going to implicitly add an import of controller to aici, which means that a component targeting aici is not defining and implementing runner, which is why there's not a trait for implementing the resource.

Ultimately, this gets to the root problem addressed by #308 and thus I think would be fixed by #308. With #308, you could write:

world aici {
  use export controller.{runner};
  export run: func(args: list<u8>) -> runner;
}
interface controller {
  resource runner { ... }
}

so that now an aici component is defining-and-exporting runner.

FWIW, I believe a short-term workaround is:

world aici {
  export run;
}
interface run {
  use controller.{runner};
  run: func(args: list<u8>) -> runner;
}
interface controller {
  resource runner { ... }
}

@AaronFriel
Copy link
Author

AaronFriel commented Mar 26, 2024

Hmm, that doesn't work yet either - the codegen is:

pub mod exports {
    pub mod aici {
        pub mod abi {
            pub mod runner {
                pub type Controller = super::super::super::super::aici::abi::ctrl::Controller;

                pub trait Guest {
                    fn run(args:_rt::Vec::<u8>,) -> Controller;
                
                    }
            }
        }
    }
}

But there is no guest trait to implement for runner, so this function is impossible to implement. There's no way to obtain a Controller handle in the guest, even if that were the intention.

@lukewagner
Copy link
Member

Ah, that's surprising; maybe I'm forgetting something or maybe it's a bug? @alexcrichton do you know?

@alexcrichton
Copy link
Collaborator

Hm there's a couple versions of WITs in play in this issue, so I'd want to make sure I pick the right one. @AaronFriel can you summarize where you're currently at, e.g. which WIT document generates bindings that you're not expecting?

@AaronFriel
Copy link
Author

Ah, it was the workaround proposed above:

world aici {
  export run;
}
interface run {
  use controller.{runner};
  run: func(args: list<u8>) -> runner;
}
interface controller {
  resource runner { ... }
}

@alexcrichton
Copy link
Collaborator

Ah yes that's because the aici world is exporting the run interface, but run transitively refers to the controller interface. That controller interface is neither imported nor exported, so it ends up being implicitly imported. As it's an imported interface you only get functionality in the interface itself, so there's no way to construct the resource.

If, however, export controller is added to world aici then you should have the ability to construct a custom runner resource.

@lukewagner
Copy link
Member

Ah right, sorry, my mistake above. So then I think the workaround is:

interface run {
  export controller;
  use controller.{runner};
  run: func(args: list<u8>) -> runner;
}

@AaronFriel
Copy link
Author

I see - does export controller change the return type of the binding for run?

Or is this clarified by #308 where the notional direction of runner is clear?

@lukewagner
Copy link
Member

Good question! The implicit use-resolution rules that Alex described (and I forgot) look for an exported version of a used interface before falling back to adding an import, thus the explicit export controller; is found by the use controller, thereby preventing an implicit import from being generated. And yes, #308 should indeed clarify all this and also avoid the need for the explicit export controller;; use export controller.{runner} would do the right thing.

@AaronFriel
Copy link
Author

That's great - thanks for answering my questions! I'm implementing microsoft/aici#70 to simplify the bindings between the host and guest and this has been very helpful.

@lukewagner
Copy link
Member

Great! Closing, but feel free to reopen if there are further questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants