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

rust (question): create resource in host component and share to a guest component #1103

Open
bryanburgers opened this issue Dec 10, 2024 · 5 comments

Comments

@bryanburgers
Copy link

bryanburgers commented Dec 10, 2024

Short: I'm trying to take two components and compose them together. In the first component, I want to create a resource, and in the second component I want to use that resource. I can't quite figure out how to get wasm-bindgen to do what I want.

I'm using cargo component.

Long: I'm trying to use Advent of Code to learn about components. Overall, I have a very basic world/interface that looks like this, and a runner (written in wasmtime) that takes a component and an input and outputs the two answers.

package aoc:base;

interface day {
    run: func(input: string) -> tuple<string, string>;
}

world day-world {
    export day;
}

Then for every day, I create two components called "parser" and "solver", and each one has a custom wit for that particular problem. The wits I currently have for day 10 are the following, which represent parsing the input as a two-dimensional grid of values (a topological map according to the AOC story), and solving the problem with that map.

package aoc2024:day10;

// Shared between the two components which are separate projects
interface solver {    
    solve-a: func(input: list<list<u8>>) -> u64;
    solve-b: func(input: list<list<u8>>) -> u64;
}
package aoc2024:day10-parser;

// In the parser project
world parser {
    export aoc:base/day;
    import aoc2024:day10/solver;
}
package aoc2024:day10-solver;

// In the solver project
world solver {
    export aoc2024:day10/solver;
    import aoc:base/debug;
}

Conceptually, the parser parses the challenge input (string) into something close to the problem definition (list<list<u8>>), and the solver takes the parsed input and solves it for both parts.

I have been combining these with wac plug day10_parser.wasm --plug day10_solver.wasm --out day10.wasm. I would be willing to write a wac script if necessary.

(The only reason these components are separate is because I wanted to learn more about components. It's working so far.)

What I tried to do with Day 10 and couldn't, was to use a resource.

resource topographical-map {
    map-width: func() -> u32;
    map-height: func() -> u32;
    height-at-location: func(x: u32, y: u32) -> u8;
}

I'm not sure exactly whether the topographical-map should be imported or exported.

This seems reasonable to me:

package aoc2024:day10;

interface types {
    resource topographical-map {
        map-width: func() -> u32;
        map-height: func() -> u32;
        height-at-location: func(x: u32, y: u32) -> u8;
    }
}

interface solver {
    use types.{topographical-map};
    solve-a: func(input: borrow<topographical-map>) -> u64;
    solve-b: func(input: borrow<topographical-map>) -> u64;
}

But when I went down this route, I couldn't find any way for the "parser" component to create a topographical-map from the input and then send a reference to it to the "solver" component.

I know this is a very open-ended question, but I tried various things for an hour or two and didn't end up any closer to figuring out how one component could play host of a resource to another component. Is this possible?

https://github.com/bryanburgers/advent-of-code-2024

@bryanburgers bryanburgers changed the title rust (question): create resource in guest rust (question): create resource in guest and share to another guest component Dec 10, 2024
@bryanburgers bryanburgers changed the title rust (question): create resource in guest and share to another guest component rust (question): create resource in host component and share to a guest component Dec 10, 2024
@sunfishcode
Copy link
Member

sunfishcode commented Dec 10, 2024

Would it work to add a constructor to the topographical-map resource?

resource topographical-map {
        /// Construct a new empty map.
        constructor();

        /// Change this to whatever you need to populate the map.
        populate-map: func(with: stuff);

        // existing API...
        map-width: func() -> u32;
        map-height: func() -> u32;
        height-at-location: func(x: u32, y: u32) -> u8;
}

@bryanburgers
Copy link
Author

OK, thanks, so I tried that suggestion.

Two things I notice:

  1. In my parser (host component)'s lib.rs, I never specifically instantiate my own MyTopographicalMap.
  2. The world for my composed component includes the types interface, when it really shouldn't. (I don't want the wasmtime host providing the topographical-map.)

I made the following changes (bryanburgers/advent-of-code-2024@main...day-10-wit-resource)

Shared wit becomes:

shared wit
package aoc2024:day10;

interface types {
    resource topographical-map {
        constructor(map: list<list<u8>>);
        map-width: func() -> u32;
        map-height: func() -> u32;
        height-at-location: func(x: u32, y: u32) -> u8;
    }
}

interface solver {
    use types.{topographical-map};
    solve-a: func(input: borrow<topographical-map>) -> u64;
    solve-b: func(input: borrow<topographical-map>) -> u64;
}

bryanburgers/advent-of-code-2024@main...day-10-wit-resource#diff-2de11ec0645c7778023602c956efba78fb3698f30c3b0cf13127a8337dc2f050

Solver (guest)'s wit becomes

Solver (guest)'s wit
package aoc2024:day10-solver;

world solver {
    export aoc2024:day10/solver;
    // Newly added here?
    import aoc2024:day10/types;
    import aoc:base/debug;
}

bryanburgers/advent-of-code-2024@main...day-10-wit-resource#diff-87f007437233c526c358a1942122c66c5c48d452a4518dca511a6debe21c51e7

Parser (host)'s wit becomes

Parser (host)'s wit
world parser {
    export aoc:base/day;
    import aoc2024:day10/solver;
    // Newly added here?
    export aoc2024:day10/types;
}

bryanburgers/advent-of-code-2024@main...day-10-wit-resource#diff-220116699994a35a9531cdafdbc0fd870387bcc2194c2b290e6402fa2d7595c4

solver's lib.rs becomes:

(And to me it's weird that I'm doing let input = bindings::aoc2024::day10::types::TopographicalMap::new(&map); to satisfy the type checker instead of something like let input = MyTopographicalMap::new(&map);.

solver's lib.rs
#[allow(warnings)]
mod bindings;

struct Component;

impl bindings::exports::aoc::base::day::Guest for Component {
    fn run(input: String) -> (String, String) {
        let mut map = Vec::new();

        for line in input.lines() {
            let row = line.bytes().map(|byte| byte - b'0').collect();
            map.push(row);
        }

        // This is weird to me. Feels like it should be MyTopographicalMap::new(&map);
        let input = bindings::aoc2024::day10::types::TopographicalMap::new(&map);

        let result_a = bindings::aoc2024::day10::solver::solve_a(&input);
        let result_b = bindings::aoc2024::day10::solver::solve_b(&input);

        (result_a.to_string(), result_b.to_string())
    }
}

struct MyTopographicalMap(Vec<Vec<u8>>);

impl bindings::exports::aoc2024::day10::types::Guest for Component {
    type TopographicalMap = MyTopographicalMap;
}

impl bindings::exports::aoc2024::day10::types::GuestTopographicalMap for MyTopographicalMap {
    fn new(map: Vec<Vec<u8>>) -> Self {
        MyTopographicalMap(map)
    }

    fn map_width(&self) -> u32 {
        self.0[0].len() as u32
    }

    fn map_height(&self) -> u32 {
        self.0.len() as u32
    }

    fn height_at_location(&self, x: u32, y: u32) -> u8 {
        self.0[y as usize][x as usize]
    }
}

bindings::export!(Component with_types_in bindings);

bryanburgers/advent-of-code-2024@main...day-10-wit-resource#diff-2645281e7cc23deeba00c2d5b091fa1e7d19605fcb68652ba70d1f09d11a1d7d

Now, all of those build fine. But the wits are interesting.

wasm-tools component wit build/day10_parser.wasm
package root:component;

world root {
  import aoc2024:day10/types;
  import aoc2024:day10/solver;

  export aoc:base/day;
  export aoc2024:day10/types;
}
package aoc2024:day10 {
  interface types {
    resource topographical-map {
      constructor(map: list<list<u8>>);
      map-width: func() -> u32;
      map-height: func() -> u32;
      height-at-location: func(x: u32, y: u32) -> u8;
    }
  }
  interface solver {
    use types.{topographical-map};

    solve-a: func(input: borrow<topographical-map>) -> u64;

    solve-b: func(input: borrow<topographical-map>) -> u64;
  }
}


package aoc:base {
  interface day {
    run: func(input: string) -> tuple<string, string>;
  }
}
wasm-tools component wit build/day10_solver.wasm
package root:component;

world root {
  import aoc2024:day10/types;

  export aoc2024:day10/solver;
}
package aoc2024:day10 {
  interface types {
    resource topographical-map {
      map-width: func() -> u32;
      map-height: func() -> u32;
      height-at-location: func(x: u32, y: u32) -> u8;
    }
  }
  interface solver {
    use types.{topographical-map};

    solve-a: func(input: borrow<topographical-map>) -> u64;

    solve-b: func(input: borrow<topographical-map>) -> u64;
  }
}
$ wac plug build/day10_parser.wasm --plug build/day10_solver.wasm --output build/day10.wasm
wasm-tools component wit build/day10.wasm
package root:component;

world root {
  import aoc2024:day10/types;

  export aoc:base/day;
  export aoc2024:day10/types;
}
package aoc2024:day10 {
  interface types {
    resource topographical-map {
      constructor(map: list<list<u8>>);
      map-width: func() -> u32;
      map-height: func() -> u32;
      height-at-location: func(x: u32, y: u32) -> u8;
    }
  }
}


package aoc:base {
  interface day {
    run: func(input: string) -> tuple<string, string>;
  }
}

In this last one, the composed component, it's still expecting aoc2024:day10/types as both an import and an export.

Comparison to what my composed wits look like for previous days:

wasm-tools component wit build/day09.wasm
package root:component;

world root {
  export aoc:base/day;
}
package aoc:base {
  interface day {
    run: func(input: string) -> tuple<string, string>;
  }
}

@bryanburgers
Copy link
Author

If I don't include aoc2024:day10/types as an export from the parser, I can't implement the trait for it...

cargo component build --target wasm32-unknown-unknown --package day10_parser --release
  Generating bindings for day10_parser (day10/parser/src/bindings.rs)
   Compiling day10_parser v0.1.0 (/Users/bryan/personal/advent-of-code-2024/day10/parser)
error[E0433]: failed to resolve: could not find `aoc2024` in `exports`
  --> day10/parser/src/lib.rs:26:25
   |
26 | impl bindings::exports::aoc2024::day10::types::Guest for Component {
   |                         ^^^^^^^ could not find `aoc2024` in `exports`

error[E0433]: failed to resolve: could not find `aoc2024` in `exports`
  --> day10/parser/src/lib.rs:30:25
   |
30 | impl bindings::exports::aoc2024::day10::types::GuestTopographicalMap for MyTopographicalMap {
   |                         ^^^^^^^ could not find `aoc2024` in `exports`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `day10_parser` (lib) due to 2 previous errors
make: *** [target/wasm32-unknown-unknown/release/day10_parser.wasm] Error 101

@bryanburgers
Copy link
Author

bryanburgers commented Dec 10, 2024

Oh interesting, if I have the guest implement and export the topographical-map then the host can instantiate it and pass it in.

bryanburgers/advent-of-code-2024@main...day-10-wit-resource-defined-in-guest

THIS WORKS!

But in the larger picture, it's not exactly what I'd want: I'd still want the host to be able to define its own implementation and "information hide" everything but the interface, and the guest to just use the interface of the resource to do its work.

@bryanburgers
Copy link
Author

OK, continuing to try things.

If I make parser export types, it doesn't work. Parser ends up with both import and export of types, and it can't be circular.

If I made a third component that just handles the map, then it works.

bryanburgers/advent-of-code-2024@main...day-10-wit-resource-third-component

The caveat is that the components need to be plugged in a very specific order:

First solver and parser need to be plugged. Then the result of that needs to be plugged with the third component.

wac plug build/day10_parser.wasm  --plug build/day10_solver.wasm --output build/day10_without_map.wasm
wac plug build/day10_without_map.wasm --plug build/day10_map.wasm --output build/day10.wasm

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

2 participants