Repository navigation
What's the naming/conceptual convention around WIT packages, worlds, and interfaces? Are there parallels in other languages? #295
Description
Here's a concrete example: If I want to turn this very simple Rust package https://docs.rs/unicode-math-class/latest/unicode_math_class/ into a WebAssembly component using https://github.com/bytecodealliance/cargo-component, what should I call the package, world, and interface?
For instance in the example Rust package, there's these levels of hierarchy:
- package: unicode-math-class on crates.io
- crate: unicode_math_class is the singular lib crate in the package. Rust supports N bin + 1 lib crate per package which maps well-ish to multiple worlds?
- modules: there's no submodules like
serde::deso it's just the root module. - items: the MathClass enum, REVISION constant, and 'class()' function.
In my brain those map relatively cleanly onto the package/world/interface model like so:
- package:
jcbhmr:unicode-math-classfrom my username + Rust package name. Sorta Docker-like two-part names. - world:
from lib crate name --unicode_math_classunicode-math-classdue to no underscores - interface: since I need to put the
enum math-class {}inside an interface... but theMathClassis in the root module... What should I do? Is there a naming convention likeinterface crate { ... }? Ormod,root,module,iface,exports,main,lib,it,default, or something else? Single metacharacters like$,_,-aren't allowed 😢. If it had any sub-modules I guess those would map well toserde::de=>world serde {}; interface de {}.
WIT snippet of that
package jcbhmr:unicode-math-class;
world unicode-math-class {
// Can't export enum without a wrapping interface! What do you call the interface tho?
export x: interface {
enum math-class { ... }
// No consts; compromise with getter func.
revision: func() -> u8;
// No char type; compromise with string.
class: func(c: string) -> math-class;
}
}Is there a better way? 🤔
I'm looking for the convention. Or just some general "well here's what I do" ideas. I'm having decision paralysis and I'd like to get some other opinions besides my own biased one.
To add another example to my question: what about other languages? Another ezpz to-WASM language besides Rust is JavaScript https://github.com/bytecodealliance/jco#componentize
Here's my guess as to how something like https://tsdocs.dev/docs/string-width might be paralleled to WASM component WIT:
You've roughly got these hierarchical levels:
- npm package: string-width
- subpath-exports: just
./(nostring-width/emojifiersub-exports) - the exported items: so just
defaultfn andOptionstype in this case.
In my brain I think that could be modeled as:
- package:
jcbhmr:string-widthsince I'm publishing it + package name - world: different worlds for each subpath-export. Like
string-width=>world string-width {}butstring-width/emojify=>world string-width-emojify {}. Each world is an "entrypoint" set of imports/exports just like an ES module is. - interface: you can eliminate the need for a redundant
interface exports { default: func(...) }by just putting thefunc()on the world. But that doesn't work for modules that export enums, records, or resources. Specifically theoptionsbag record.
WIT snippet of that
package jcbhmr:string-width;
// 'import {...} from "string-width"'
world string-width {
use options.{options};
// ugh. now the function is free of an interface but the record type
// needs one. you might as well just stick them both in the interface?
export default: func(string: string, options: option<options>);
export options;
}
interface options {
record options {
ambiguous-is-narrow: bool,
count-ansi-escape-codes: bool,
}
}Is there a better way? 🤔
no, the string-width/emojify export doesn't actually exist
As you might be able to tell, I'm a bit puzzled about when/how to use interfaces and how to name them idiomatically when they get in the way of flatter hierarchies. I'm trying to understand the concepts that package/world/interface are supposed to represent and apply them to existing programming concepts that
I know. Thanks for reading my TED talk lol
p.s. has this already been discussed somewhere/somehow? it must've...
Activity
[-]What's the naming/conceptual convention around packages, worlds, and interfaces? Are there parallels in other languages?[/-][+]What's the naming/conceptual convention around WIT packages, worlds, and interfaces? Are there parallels in other languages?[/+]lukewagner commentedon Jan 19, 2024
Good questions! So, when packaging up an implementation (like
unicode-math-classorstring-widthin your examples), there's ultimately only one thing you need to name, which is the package name of the component implementation (just like in npm or cargo, where every package is an implementation package). Clients of this component implementation only need to know this one package name because the client tooling can extract from the component implementation all the necessary WIT information needed to generate client bindings (component binaries embed all the type information needed). You could sortof think of it as-if the package name of a component implementation also implicitly names "the world targeted by this component".This is all in contrast to the situation where we're defining standardized
interfaces andworlds as part of WASI or as part of some platform vendor's public interface. In these scenarios, there is no component implementation (no executable code being published), and thus we want only the type information, and so we have this regular encoding of pure WIT information into a component binary (as named exported component types).Now, with that distinction made, when you are authoring a component that you want to later publish, you may need to write some WIT to specify the world you're targeting. You don't always have to, of course: if you're targeting a standard or platform-defined world, you just name the published WIT world (in your languages' appropriate buildconfig) and you're done. Alternatively (and increasingly over time), your language toolchain may allow you to define your component's type using source-language types and the toolchain then infers the target world from those types (e.g., mapping a Rust
strto a C-Mstring). But in general, you may of course want to explicitly author a target world in WIT to have full explicit control (or express something you can't otherwise). And in this scenario, when you're writing WIT, WIT currently makes you specify a name for the target world, but in fact this world name doesn't actually appear in the final binary -- it's forgotten as part of compilation. For this reason, I've been thinking that we should probably allow a different "mode" of WIT where you (1) don't have to declare apackagename, (2) get to write a single unnamedworldthat is understood to be "the target world". You can't package this WIT but by definition you don't want to -- you just want to feed this WIT into your language toolchain to author a component implementation (that you do want to publish). However, until we do that (or something else to address this use case), I wouldn't care about theworldname (and naming scheme) when authoring a component, since whateverworldname you write is ultimately forgotten.I hope that helps! Happy to discuss more. (Also CC @alexcrichton for other thoughts, since this is something we've talked about before.)
jcbhmr commentedon Jan 19, 2024
You're right! The
world silly-world-name {}doesn't appear when generating things using jco or wasmtime-py's wasmtime.bindgen.It does seem to appear when using Wasmtime's
bindgen!()macro though so it's not completely invisible 🤷♀️ This might be because it's generating bindings from WIT directly instead of from the magic info that's embedded in themy-component.wasmfile like jco and wasmtime-py do.Details
https://github.com/jcbhmr/unicode-math-class.wasm/blob/96deb970f9ba5e80cd271d539929e60e1c599872/tests/rs/tests/common/mod.rs
so you're right; the world name doesn't matter. the thing that multiple worlds or world names are good for in my brain is naming your build targets. point being: I think this actually does map relatively well particularly to the Rust "world <=> crate" analogy. If you have a dual lib & cli Rust package with two crates then that could be modeled well with two worlds: my-app-lib-world and my-app-bin-world.
concrete example: https://github.com/typst/hayagriva https://docs.rs/hayagriva/latest/hayagriva/ has two crates in the single package: a lib.rs and a main.rs
my idea of a wit binding:
then when you run
make buildor./build.shor whatever you'd expect those two worlds to map directly to two .wasm files...right? am i on the right track with that mental model of what a world is?this is really a rant/thought-sneeze but i hope what im thinking about here makes sense and im understanding this stuff ok-ishly correctly.
alright with the world stuff out of the way...
🤷♂️ idc. U can leave it as-is as long as it's documented somewhere lol
the "what should i call the package" not answered directly but i think i get it ✅
so the "what should i call the world" got answered ✅
what about the interface?
if I take the unicode-math-class example again, it has just three ROOT exports: REVISION const, class() func, and MathClass enum. and since you can't put enums, variants, records, resources, etc. on the root world you MUST use a wrapper interface. what's the naming convention for interfaces?
basically, which one of these is best? or is there a better way im not thinking of?
unicode_math_class::class =>
jcbhmr:unicode-math-class/crate#classunicode_math_class::MathClass =>
jcbhmr:unicode-math-class/crate#math-classunicode_math_class::class =>
jcbhmr:unicode-math-class#classunicode_math_class::MathClass =>
jcbhmr:unicode-math-class/types#math-classunicode_math_class::class =>
jcbhmr:unicode-math-class/unicode-math-class#classunicode_math_class::MathClass =>
jcbhmr:unicode-math-class/unicode-math-class#math-classthere's already the
jcbhmr:unicode-math-class/prefix from the package name that will be present on any exports.as another example with a few submodules, take https://docs.rs/hayagriva/latest/hayagriva/
which of these is best? or is there a better way im not thinking of?
hayagriva::entry =>
jcbhmr:hayagriva/crate#entryhayagriva::lang::CaseFolder =>
jcbhmr:hayagriva/lang#case-folderhayagriva::entry =>
jcbhmr:hayagriva/hayagriva#entryhayagriva::lang::CaseFolder =>
jcbhmr:hayagriva/hayagriva#case-folderhayagriva::entry =>
jcbhmr:hayagriva/hayagriva#entryhayagriva::lang::CaseFolder =>
jcbhmr:hayagriva/hayagriva-lang#case-folderto clarify my question im essentially asking: what concept in Rust or JavaScript does an
interfacemap to?right now im sorta thinking it's similar to a specific module in rust (
hayagriva::typesis one module,hayagriva::iois another,hayagrivais the root module, etc.) but i don't know how well that applies to javascript.like in my https://tsdocs.dev/docs/string-width example i guess i could see an interface as each
string-width/*sub export? likestring-widthis one interface and thenstring-width/some-subpath-exportis another interface?but then what do I call that
./root interfacedefault? the name of the package? theOptionstype record is going to show up asjcbhmr:string-width/some-interface-name#options. ideally it'd be on the root directly like you can with functions but alas. 🤷♂️Which one of these looks better? another more different one that's not here?
default export =>
jcbhmr:string-width/string-width#defaultOptions =>
jcbhmr:string-width/string-width#optionsdefault export =>
jcbhmr:string-width#defaultOptions =>
jcbhmr:string-width/types#optionsdefault export =>
jcbhmr:string-width/root#defaultOptions =>
jcbhmr:string-width/root#optionssorry this is getting quite opinion-y and stylistic-y. im just kinda feeling around to see other peoples opinions on things cause up till now it's just been me arguing with myself about how to organize WIT stuffs and that's not constructive. (that's why the question was about conventions too lol)
lukewagner commentedon Jan 22, 2024
Yes, I suppose the world name can show up as an impl detail in the bindings, although probably it shouldn't (even today). If we added the "unnamed world" feature for non-package WIT as I mentioned earlier, than that would effectively force the bindings generator to use a different scheme that didn't care about the world name.
I think your top-right example is on the right track, i.e., having the component export everything directly without wrapping it in an interface. Components are also able to directly export types, so it is just a temporary WIT limitation that you're not able to define and export the
math-classenum directly in the world. We should probably fix that to avoid this question.In the meantime, you can also write:
Here also I'd just export everything directly or, if you want to group things, use the
export foo: interface { ... }syntax I mentioned above.In JS, I'd say an
interfacecorresponds to either a module namespace object or, when they are nested, a plain old JS object (that groups together and namespaces a collection of exports together). In Rust, yeah, I think a module is the closest correspondence. Really, aninterfaceis just about namespacing a collection of related definitions so that they can use the short names they want to without worrying about conflicting with unrelated interfaces. It's up to the bindings generator to use what the language gives it to implement that namespacing.Hope that helps!
jcbhmr commentedon Jan 23, 2024
🙏 omg THANK YOU for providing another opinion. Looking back (hindsight 20/20) i can see that this idea of a
interface types {}is used a bunch in WASI-related stuff! i had coders-block; I had myself convinced "all the funcs also need to be in an idiomatic interface"and that's just a cursory look lol
I like this idea very much. I checked out Wasmer's custom .wai format recently https://wasmerio.github.io/wasmer-pack/user-docs/concepts/wai/index.html and it seems to encourage flat file->type hierarchies instead of package->world->interface->type 🤷♂️
As a beginner WAI initially seemed easier to understand but now that I know what a
package x:y@z;does and how aworld the-world {}works and what aninterface the-interface {}does I prefer WIT to WAI (WIT is FAR more flexible than WAI; WAI is isolated to single file-- no sharing imports/exports!)The weird restriction (I assume due to some practical technical reason) that you can export functions directly but not
enumorrecordorresourceetc. was a bit "well ok does that mean the 'happy path' is to put everything in an interface? should i put everything in an interface? is an interface the convention?" <- that was me last week lol👍
wasi-related: the WASI-defined stuff is very interface-heavy! should i be using more interfaces than just
interface types {}?ex: should i emulate this "pull the error type into a separate interface" idea? https://github.com/WebAssembly/wasi-keyvalue/blob/main/wit/error.wit vs having it in https://github.com/WebAssembly/wasi-keyvalue/blob/main/wit/types.wit ?
this is a "hey i noticed this. whats the reason?" question which is tangential to this discussion. i found the WASI stuff when gathering the links for the above WASI-related link list lol
Follow up question: is there a a "re-export" mechanism to combine interfaces? like take
large-interface-part1.*and-part2.*and merge them intolarge-interfaceto hide the internal splitting? sorta like how rust can dopub use part1::*or js can doexport * from "./part2.js"?probably not given that each interface item has a unique url-like prefix from its containing interface. now that im typing this i think i just answered my own question lol
To clarify, you mean something like TypeScript which has a
import { server } from "typescript"sub-namespace?Or did you mean something like a https://npm.im/semver which has subpath exports?
im not sure how to interpret "module namespace object" given that the
import * as moduleNamespaceObject from "the-module"vsimport * as moduleNamespaceObject from "the-module/sub-module-here"are like two levels(?) of hierarchy? are subpaths like that a separateworld the-subpath {}? are they interfaces? sorry this is a real tangent. you answered the core of my question and clarified the Rust <=> WIT module intuition that I had. its ok if you dont want to continue debating stylistic opinions lolI thank you again for pointing me in the right direction and for being someone to bounce ideas off of. 😊 ❤️
jcbhmr commentedon Jan 23, 2024
as a "completed" hello-world-like rust -> wasm component, how does this look https://github.com/jcbhmr/unicode-math-class.wasm ? is it idiomatic with the recommendations here?
sidenote: why does doing the
interface types {}and thenuses types.{...}create some weird phantom imports?https://github.com/jcbhmr/unicode-math-class.wasm/actions/runs/7631660252
they dont seem to generate any actual "i need this from the host" imports when using jco but they still show up in the
wasm-tools component witoutput as well as thewit-bindgenfor markdown docs: https://jcbhmr.me/unicode-math-class.wasm/unicode-math-class.html is that supposed to happen? is that a glitch? do those imports not matter?lukewagner commentedon Jan 24, 2024
When you're defining a component that's not intending to implement a standardized or separately-published
interface, you don't need to define interfaces just to export functions from your component'sworld. Separately-defined interfaces are valuable when you want to give them a name that stands apart from any particular implementation (which is what WASI is all about, which is why WASI does it all the time).Not yet, but there is an
includestatement in WIT that lets youincludeoneworldfrom another (which is mostly like a copy-paste, leaving behind about the name of the sourceworldin the destinationworld), and so it might make sense to allowincludeto work likewise ininterfaces too. The omission is mostly just that we haven't had a pressing use case for it yet.The former, iiuc.
That looks good. Again, it'd be nice if we'd let you define the
enumdirectly in theworld, but without that support, what you wrote looks reasonable.That does seem like a bug; I would think that the
usewould bind to the exportedtypesinterface. Maybe file a wit-bindgen bug and CC me to see if I'm misunderstanding?jcbhmr commentedon Jan 24, 2024
posted more detailed issue bytecodealliance/wit-bindgen#822
jcbhmr commentedon Jan 24, 2024
you've pretty much addressed all of my questions! thank you again! ❤️ im happy to close this now since you answered my "wait, what should go in an interface/world/package and what should they each be called?" question 🙏 thanks again!
if possible or practical i think it'd be a good idea to summarize and extract a few of the "conceptual revelations" or whatever you wanna call them that occurred here and put them in some kind of document either in this repo or https://github.com/bytecodealliance/component-docs or in cargo-component's docs or whatever
ex:
interface types {}a lot so consider that