Repository navigation
Link guests importing semver-compatible interface versions - #69
Conversation
Phoenixxo
left a comment
There was a problem hiding this comment.
Overall, looks good. Just some simplifications that might be better to keep in mind
| Map.Entry<String, Object> resolved = builder.instance().resolveImport(imp.name()); | ||
| try { | ||
| processImport(builder, imp, resolved); | ||
| } catch (LinkageException e) { | ||
| if (resolved == null || resolved.getKey().equals(imp.name())) { | ||
| throw e; | ||
| } |
There was a problem hiding this comment.
resolveImport now runs for every import before the kind-specific code. An ambiguous set of host names fails the link even when the import wouldn’t have used a host value. An eq-bound type import or an instance type that demands nothing is synthesized when nothing is supplied, and it used to link regardless of what else the host provided. I think raising the ambiguity error only on the paths that consume the value would be best here.
There was a problem hiding this comment.
With this change a compatible host name counts as supplied for every kind, so an eq-bound type or a demand-nothing instance that finds one checks it the same way it would check an exact name. With two candidates tied on version there's no single value to check, and falling back to the synthesized one would quietly ignore both. It also takes a host that registers two names differing only in build metadata, which I'd rather report than work around. Happy to revisit if you have a real case.
| processImport(builder, imp, resolved); | ||
| } catch (LinkageException e) { | ||
| if (resolved == null || resolved.getKey().equals(imp.name())) { | ||
| throw e; |
There was a problem hiding this comment.
Small nitpick, but I think the rethrown exception puts e.getMessage() in its own message and also passes e as the cause, so the original text shows up twice in a stack trace.
There was a problem hiding this comment.
This follows the existing wrap at ComponentLinker.java:107. Keeping the reason in the message lets callers that only log getMessage() see it, and the cause keeps the frames where the mismatch was found.
| static String resolve(String requested, Collection<String> provided) { | ||
| if (provided.contains(requested)) { | ||
| return requested; | ||
| } | ||
| String key = compatibleKey(requested); | ||
| if (key == null) { | ||
| return null; | ||
| } | ||
| Version best = null; | ||
| Version tied = null; | ||
| for (String name : provided) { | ||
| Version candidate = Version.parse(name); | ||
| if (candidate == null || !candidate.key.equals(key)) { | ||
| continue; | ||
| } | ||
| int order = best == null ? 1 : PRECEDENCE.compare(candidate, best); | ||
| if (order > 0) { | ||
| best = candidate; | ||
| tied = null; | ||
| } else if (order == 0) { | ||
| tied = candidate; | ||
| } | ||
| } | ||
| if (tied != null) { | ||
| throw new LinkageException( | ||
| "Import \"" | ||
| + requested | ||
| + "\" is ambiguous between \"" | ||
| + best.name | ||
| + "\" and \"" | ||
| + tied.name | ||
| + "\", which differ only in build metadata"); | ||
| } | ||
| return best == null ? null : best.name; | ||
| } |
There was a problem hiding this comment.
When there’s no exact match, resolve will run the semver regex over every provided name, and it does this for each import. A host with a few dozen WASI interfaces and a guest importing most of them at an older patch means well over a thousand matches per link. We could move to using an index on the host names by compatible key once, in addImports or lazily on first lookup, which would make each import a single map lookup.
There was a problem hiding this comment.
Good catch. Older patch versions are exactly what Rust guests import, so every WASI import took the scan. Host names are now indexed by compatible key once per instantiation, lazily on the first lookup that misses an exact name, and each import is one map lookup. 3933ee7
| */ | ||
| Map.Entry<String, Object> resolveImport(String name) { | ||
| String supplied; | ||
| if (parent == null) { |
There was a problem hiding this comment.
parent == null repeats the check in ComponentLinker.atHostBoundary. If the definition of the host boundary ever changes, the two could disagree, so version matching and the host-only type checks would no longer apply at the same level. Can both go through one method?
There was a problem hiding this comment.
Moved to ComponentInstance.atHostBoundary(). The linker uses it everywhere now, including the parent() == null check that was already in processComponentImport. 67237ef
| default: | ||
| throw new LinkageException( | ||
| "Import type " + imp.externDesc().kind() + " not supported yet"); | ||
| Map.Entry<String, Object> resolved = builder.instance().resolveImport(imp.name()); |
There was a problem hiding this comment.
This adds a Map.Entry parameter to five process*Import methods and allocates an entry per import, but the key is only used to build the error message in the catch block. Wouldn't it be simpler to resolve to the supplied name and read the value from the instance, or to keep the supplied name in one place for the error path?
There was a problem hiding this comment.
The process*Import signatures are back to what they were. hasImport/getImport resolve compatible names at the host boundary, and processImportSection keeps the supplied name only to word the error. 67237ef
|
@Phoenixxo I addressed most of your comments, I'll move forward and merge this, feel free to add any additional comment and we can fix on top in follow up PRs |
Guests built by real toolchains import older patch versions of WASI interfaces, for example Rust 1.96's std imports
wasi:io/poll@0.2.6, while our host provides@0.2.12. Import names were matched exactly, so linking failed.This resolves host-provided imports by the Component Model's canonical interface name rule, the same one Wasmtime (
alternate_lookup_key) and jco use:@1.2.3→@1) or, for0.x, after the minor (@0.2.6→@0.2), and the highest provided version with the same key is used. Versions are compared numerically.0.0.x, pre-releases and unversioned names only match exactly. Build metadata is ignored, and two candidates that differ only in it fail as ambiguous.Not included: export-side matching, bindgen version selection (the first half of #53), and parsing the
versionsuffixattribute.Tests:
InterfaceVersionsTests(48) andCompatibleVersionImportTests(20). Refs #53.