bytecodealliance / wasmtime-py

Python WebAssembly runtime powered by Wasmtime
https://bytecodealliance.github.io/wasmtime-py/
Apache License 2.0
381 stars 52 forks source link

Initial implemention of bindgen for exported resources #232

Closed jamesls closed 2 months ago

jamesls commented 2 months ago

Issue #197

This adds support for generating bindings for resources exported from a wasm component, so that Python code can interact with these resources.

I looked over jco/componentize-py and followed the same overall approach. The internal structuring of bindgen.rs here is slightly different than jco, so I erred on the side of keeping things as they are as much as possible.

In terms of the generated code itself:

One of the issues I had to work out was how to pass in the root object that contains all the trampolines needed by the resource classes, which is defined in the root object. This is an issue because the constructor of a resource is modeled in WIT, and the root object is typically passed as a param in the __init__. What I ended up doing was using a closure to capture the root component instance and then dynamically generating the class when the interface "scope class" is instantiated. For example, given this WIT file:

package component:basicresource;

interface my-interface-name {
    resource demo-resource-class {
        constructor(name: string);
        greet: func(greeting: string) -> string;
    }
}

world example {
    export my-interface-name;
}

The interface module for my-interface-name would have:


class MyInterfaceName:
    component: 'Root'

    def __init__(self, component: 'Root') -> None:
        self.component = component
        self.DemoResourceClass = _create_demo_resource_class(component)

# ...

def _create_demo_resource_class(component: 'Root') -> type[DemoResourceClass]:
    class _DemoResourceClass:
        # Preserve the signature defined in WIT.
        def __init__(self, caller: wasmtime.Store, name: str) -> None:
            # Capture the root component for access to all the trampolines/canons.
            self.component = component

And the code to instantiate a resource looks like this:

from wasmtime import Store

import mybindings # Whatever name you used.

store = Store()
root = mybindings.Root(store)
interface = root.my_interface_name()
instance = interface.DemoResourceClass(store, "myname")
print(instance.greet(store, "Hello"))

Would love feedback on this, it's unconventional in Python, but I suppose it also maps closer to the idea of a(resource ...) being a type constructor that creates a fresh type for each instance, while still being able to use the the module level protocols for the resource to satisfy type checking of the resources.

Imported resources aren't implemented as part of this change. Trying to bindgen imported resources will still result in an unimplemented!() panic. Wanted to get feedback on the overall approach before going too far.

As part of this change I also added a commit to implement more of the WASI shims needed to run bindgen (same stack trace from #202).

jamesls commented 2 months ago

The main thing I might recommend perhaps bikeshedding a bit on is the testing story here. Tests so far in this repository are raw inline text-format-components which aren't exactly readable or easy to work with. Do you have thoughts/ideas on how to improve this? For example it might be reasonable to use componentize-py here to help generate test cases?

Interesting idea, I'll try and sketch out writing tests using componentize-py and see how it goes. If it works out, it would be easier to write tests compared to writing wat.

alexcrichton commented 2 months ago

I'm really liking how #234 is shaping up, thanks again for that! In the meantime no need to block this on that, so I'm going to go ahead and merge this. Thank you again for your work here!