-
Notifications
You must be signed in to change notification settings - Fork 39
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
RFC - Human friendly HostImports' builders #445
Conversation
@Test | ||
void withIndex() { | ||
var moduleName = "module"; | ||
var fieldName = "filed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo "filed"?
var moduleName = "module"; | ||
var fieldName = "filed"; | ||
HostImports.builder() | ||
.withNewImport(moduleName, fieldName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this style of nested builder to be strange, especially since only one method can be called. For nested builders, I've found that having a Consumer
argument is a good approach, but that doesn't seem useful here. The only thing we are saving by having withNewImport()
is the two name arguments.
All of the with
methods on Builder
replace the entire collection. For example, withGlobals()
replaces all of the globals, whereas addGlobal()
adds additional globals. So the name withNewImport()
seems inconsistent.
Instead, what if we put new methods directly on Builder
:
.addGlobal(moduleName, fieldName, Value.i32(1))
.addGlobal(moduleName, fieldName, MutabilityType.Var, Value.i32(1))
.addMutableGlobal(moduleName, fieldName, Value.i32(1))
.addMemory(moduleName, fieldName)
.addMemory(moduleName, fieldName, new MemoryLimits(1))
.addMemory(moduleName, fieldName, 1) // I'd drop these as the MemoryLimits version seems more clear
.addMemory(moduleName, fieldName, 1, 2)
.addTable(moduleName, fieldName)
.addTable(moduleName, fieldName, ValueType.ExternRef)
.addTable(moduleName, fieldName, new Limits(1))
.addTable(moduleName, fieldName, 1) // same, I'd drop these and use the Limits version
.addTable(moduleName, fieldName, 1, 2)
// does type inference work out if we name all of these "addFunction"?
.addProcedure(moduleName, fieldName, () -> System.out.println("hello world"))
.addProcedure(moduleName, fieldName, (Instance inst) -> () -> System.out.println("hello world"))
.addSupplier(moduleName, fieldName, () -> 1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your input @electrum !
A few different subjects:
add
instead ofwith
: agree will change accordingly, thanks for noticing!two name arguments
: personally, I feel it awkward to have to type those "names" along with the implementation, the nested builder was an attempt to fix it.new methods directly on Builder
: this works for sure, I'm afraid is an improvement less impactful than I originally thought
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel it awkward to have to type those "names" along with the implementation
This test might look worse due to repeating the same variables. In actual usage, the field names will need to be unique (and the builder should validate that)? So if we change it to use constants, does it look better?
.addGlobal("test", "g1", Value.i32(1))
.addGlobal("test", "g2", MutabilityType.Var, Value.i32(1))
.addMutableGlobal("test", "gmut", Value.i32(1))
.addMemory("test", "m1")
.addMemory("test", "mlim", new MemoryLimits(1))
.addTable("test", "t1")
.addTable("test", "tref", ValueType.ExternRef)
.addTable("test", "t3", new Limits(1))
Another problem is that the code formatter forces each chained call to be on a separate line. If we were formatting the code by hand, it would look better:
.add("test", "g1").global(Value.i32(1))
.add("test", "g2").global(MutabilityType.Var, Value.i32(1))
.add("test", "gmut").mutableGlobal(Value.i32(1))
.add("test", "m1").memory()
.add("test", "mlim").memory(new MemoryLimits(1))
.add("test", "t1").table()
.add("test", "tref").table(ValueType.ExternRef)
.add("test", "t3").table(new Limits(1))
Which I agree looks slightly cleaner, but we can't format like that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this!
.add("test", "g1").global(Value.i32(1))
I believe that we should not be limited by our own formatter, and we can always disable it when necessary 😏
List.of(ValueType.I32))); | ||
} | ||
|
||
public Builder withSupplier(Function<Instance, IntSupplier> consumer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I look at this approach, the less I'm convinced this is the way to go.
public Builder withSupplier(Function<Instance, IntSupplier> consumer)
and
public Builder withSupplier(Function<Instance, LongSupplier> consumer)
are going to be not distinguishable after type erasure.
Currently, I think that leaving only:
public Builder withFunction(Function<Instance, Function<Value[], Value[]>> consumer)
and generating the binding out of real modules with something like: https://github.com/andreaTP/chicory-bindgen-poc is the best bet.
Something I wanted to chat about in regards to #441 is that building a imports are really just another module that you link to. It feels like we could only have one way to build a module internally. Then we could maybe have some helpers to quickly build these when you just want a couple host functions. I'm just not sure there needs to be a |
@bhelx thanks for sharing! Super interesting POV indeed.
I'm happy to see further exploration of the idea, thought! Seems appealing! |
A couple of additional cents:
correct, at the moment all the |
The way i see it, host imports are just a type of Module whose instance is not a wasm instance, but lives on the host. But it could / should have the same API as any other module you might want to link (e.g. other wasm modules which must be instantiated as wasm). You can run into situations where you might need to link up a mix of both host and wasm modules to instantiate a module. I need some time to study how others runtimes do it, but ideally there could be a low-level, imperative API for building up an instance, and a high level API for when you just want to throw some host functions at a module. We'd need some things like:
For the higher level, shorthand API, where you maybe just want to pass some host functions. We can support passing functions to the instantiation process (kind of like how we do now). You wouldn't need the linker you could just use the normal module builder and instantiator. |
This is a lower-level reference doc, but here is the API for the wasmtime linker https://docs.wasmtime.dev/api/wasmtime/struct.Linker.html |
I see, at the moment cross-linking modules is possible but challenging for sure. I'm a bit afraid that it might be a bit "too early" to "future-proof" something we don't entirely grasp. |
I'm starting to feel like (*) well not necessarily, we can just let the |
This is too outdated, let's close it and move on, the |
Since code is worth a thousand words, I started coding a bunch of
HostImports
'Builders
to see how far I could get with a small corpus of examples.Notes:
Happy to hear comments!