-
Notifications
You must be signed in to change notification settings - Fork 55
Experiments with Record design #61
Comments
If I understand correctly, you are 'pushing' both the field names and
values on the intermediate stack.
I would suggest a different approach, think of the signature of the record
as a recipe for decoding the stack. With the recipe 'in hand' you pop
elements off the stack as dictated by the signature.
…On Wed, Aug 28, 2019 at 1:26 PM Jacob Gravelle ***@***.***> wrote:
I've been experimenting with a design for structs/records in my polyfill
of interface types (viewable here
<https://github.com/jgravelle-google/wasm-webidl-polyfill/tree/794070f109c38b7c7cb99e1ca2dbcf82031f1476/record>).
This is all open for discussion, but here's a thing I tried and some of the
stuff I ran in to.
Declaring a record type looks like:
***@***.*** type $Foo record
(field $bar string)
(field $baz int)
)
This declares a record type Foo with fields bar and baz.
Note that the fields are represented as indices in instructions to follow,
but in the declaration the names are preserved in the binary. This is so
languages like JS, Python, Lua, etc. can have reasonable string names in
their code, e.g. (JS):
instance.exports.readFoo({bar: "hello", baz: 12});
There's two main instructions needed to interact with record objects in an
interface adapter, creation and destructuring. My version has make-record
and get-field
get-field is straightforward, given a record and a field, get that field
off the record. So get-field $Foo $baz pops a Foo and pushes a string.
get-field takes two immediates, the type index, and the field index.
make-record is straightforward too, but raises some interesting
questions. make-record $Comment pops a string and an int, and pushes a
Foo. In general make-record takes the type index as an immediate, and has
one stack argument per field.
------------------------------
Where it gets interesting is the question: what arguments do we pass to
make-record? Let's say we have a corresponding C struct:
struct Foo {
char* bar;
int bar_len;
int baz;
};
and a function
void readFoo(struct Foo foo);
how does that foo argument translate to C's ABI? One reasonable way is to
destructure the struct into its components, and pass those all as arguments
individually. Another reasonable way is to stack-allocate the argument in
the caller's frame, and pass the pointer in (this is what Clang does, and I
think is a standard C ABI thing).
If we destructure, the adapter is just re-structuring those arguments back
into a record. If we pass by pointer however, we now need some way to read
fields off that pointer.
What I'm doing for the time being is defining an exported getter function
for each field in the C struct. This functions, but can almost-certainly be
improved. I'm not sure how to improve it without respecifying load+store
instructions in interface adapters. We would also need to do similar for gc
objects.
The nice thing about call-export is it lets us defer reimplementing
anything expressible with wasm instructions. The downsides are that it
requires a specific kind of toolchain integration to generate those exports
(not really too bad), and it relies on engine inlining to not be
inefficient.
So that's the general sketch of a design I've been working with. Thoughts?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#61?email_source=notifications&email_token=AAQAXUDFFLR5ZN2UMQEGKBTQG3NRDA5CNFSM4IRSL5BKYY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HIAZVJQ>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAQAXUA32VVY7UPTW5FSO6DQG3NRDANCNFSM4IRSL5BA>
.
--
Francis McCabe
SWE
|
Yes, exactly. Maybe an example makes that clearer (other than the example linked in the polyfill) (@interface func $printFoo (import "" "printFoo")
(param $Foo)
)
(@interface adapt (import "" "printFoo")
(param $ptr i32)
;; read bar string
arg.get $ptr
call-export "get_bar"
arg.get $ptr
call-export "get_bar_len"
read-utf8
;; read baz int
arg.get $ptr
call-export "get_baz"
as int
;; pops string and int, pushes Foo
make-record $Foo ;; $Foo refers to a type index
call $printFoo
) So, all of the record data is implied by the initial declaration, and all following uses just refer to a type index. Ditto for |
I'd expect that for C and Rust the module type will pass the record as a pointer to struct just like you describe. Using export functions to convert from pointer to value isn't ideal as the only way to do it - the common case would be to loading an offset (provided as literal) from the pointer in linear memory. Following the convention of |
I think it may be inevitable that we add enough adapter instructions to handle indirectly passed arguments and/or return pointers for large aggregates, but I also think it's reasonable to have a world with perhaps a special ABI (maybe not the default, but one that had to be explicitly named) which changed the ABI of how aggregates were passed and made them all "splat" to be expanded inline as arguments and passed via multi-value as return values. For example something like this: #[repr(C)]
struct A {
a: i32,
b: f32,
}
extern "C" {
fn roundtrip1(a: A) -> A;
}
extern "wasi" {
fn roundtrip2(a: A) -> A;
} would generate an import where All that to say that I think should probably strive to get by without memory-related instructions in the first pass of stabilization for interface types, and I think it's possible to do from C/Rust as well (assuming LLVM gets enough support for multi-value of course). In the long run though I could see memory instructions coming into existence perhaps, but they seems sort of solely motivated at this time by a lack of support in LLVM and a lack of specificity around the ABI implemented in LLVM currently. |
The "splat" ABI you describe is a great idea! I was talking to @fitzgen about custom Rust ABIs last week and he had some great ideas about how to implement them using procedural macros.
We may be able to use the trick above to avoid memory-related instructions in records, but we may need them for sequences. I don't want to make that an excuse to use them here if another solution will do, but I don't have a strong aversion to including them. Can you help me understand why we should avoid them? |
Note that passing large structs as multivalue returns will not work for self-referential structs and may have negative performance consequences (although that is just speculation until engines more widely support multivalue). I do expect that we will introduce a new C ABI for returning small structs directly on the stack once we have implemented multivalue, but I don't expect that we'll want to do that for structs of more than a few fields. |
Oh sure yeah, let me clarify. If the only motivation for the memory instructions is that LLVM can't do multi-value or the splat-ABI, that doesn't seem to me like it's wortwhile to add the memory instructions. If, however, there's other use cases motivating the memory instructions, that seems totally reasonable to me!
Is it legal in C/C++ to pass a self-referential struct by-value? In Rust at least if you return a whole struct it's changing the address of the struct's storage so it can't be self-referential. That also seems like it's somewhat of a niche use case which may not be a killer motivator for the memory instructions? As for the perf consequences, it'd be interesting to test out and evaluate! I"m also not sure myself what it would be. |
Lurking and wanted to chime in that I've been experimenting with "record as multi-value return or params" in a bespoke language and discovered the current limits set by Chrome, currently 1000 params/return values. Not saying whether the current limits are good or bad, other that all things being equal higher limits are better so larger records could be passed without touching linear memory. I'm not sure how params and multi-value return are actually implemented (could guess) so not sure the pros and cons of higher limits. Haven't checked other VMs. Although they could increase that limit, seems binaries would run into backwards compatibility trouble and would need to ship two binaries, choosing which depending on a runtime check at startup with |
@alexcrichton, good point, the passed copy of the self-referential struct would definitely still point to the source copy. This does make me wonder what happens to pointers generally in this scheme, though. I suppose the adapter would have to know how to lift the pointers to some more generic reference type. |
@jgravelle-google, thanks for writing this up. I'd like to reiterate my sentiment from our last video meeting:
I think this is mostly a concern of the C toolchain that is producing the Wasm and its interface types section, and not a concern for our standardization efforts. It is the toolchain's responsibility to make available the values that will be used in the adapter functions. This doesn't necessarily mean calling exports (and the out-of-line call overhead that implies), nor does it necessarily mean that we must introduce memory-related adapter instructions. The toolchain can use a custom ABI (the "destructuring" approach) for functions that get wrapped in an adapter, instead of LLVM's default C+Wasm ABI. I am very cautious about making standardization decisions based on how LLVM currently maps the C Abi onto Wasm. Toolchains should implement standards, not the other way around. Of course, if we standardize something that is unimplementable (or unimplementable in a performant way) then we must address that and fix the standard, but we shouldn't start with reversing the way things happen to be right now. All this is to say that I 100% agree with what @alexcrichton says here:
Backing up a bit, we clearly want to support some subset of Wasm instructions in adapter functions. Maybe it is only calling exports, maybe it also includes loads and stores. I hope it doesn't include Instead of defining a bunch of instructions that are "like |
Strongly agree. My best argument against is that the complexity of specifying struct packing or alignment is probably not worth the benefit.
It is also the standard's responsibility to map well to common toolchain designs. I use what-Clang-does-today as well as Rust to demonstrate points in the design space, which is where we might want to be flexible. It's easy for me to imagine a non-LLVM toolchain that uses either ABI. In particular I want to minimize the effort needed to implement this across the ecosystem. The more flexible our primitives, the easier it will be to get an existing compiler to use them without invasive changes.
I want to dig in to this more because it covers a broad point. I keep referencing specific things like what LLVM does, how C works, and how I threw together an experiment in my polyfill, not because I think those are the best way, but because they are very specific examples I can point to directly.
I don't actually disagree with this, but it feels like an axiom and that makes me skeptical. An alternate view would be that standards should capture the essence of the best solution for the problem, which may be already present in existing tools, and so we should understand why they do what they do.
Strong agree. My intuition says there will be, but I think it will depend on how we want to handle sequences, or other data structures. In general I think it makes sense to have memory instructions iff it is the case that some modules represent data we need to reason about in memory. And in general I think they do.
I dig that. That feels like the right mechanism to just import a bunch of wasm semantics wholesale. Can do things like That feels like it should be a separate issue to get more visibility and focused discussion. |
I've been experimenting with a design for structs/records in my polyfill of interface types (viewable here). This is all open for discussion, but here's a thing I tried and some of the stuff I ran in to.
Declaring a record type looks like:
This declares a record type Foo with fields bar and baz.
Note that the fields are represented as indices in instructions to follow, but in the declaration the names are preserved in the binary. This is so languages like JS, Python, Lua, etc. can have reasonable string names in their code, e.g. (JS):
There's two main instructions needed to interact with record objects in an interface adapter, creation and destructuring. My version has
make-record
andget-field
get-field
is straightforward, given a record and a field, get that field off the record. Soget-field $Foo $baz
pops a Foo and pushes a string.get-field
takes two immediates, the type index, and the field index.make-record
is straightforward too, but raises some interesting questions.make-record $Comment
pops a string and an int, and pushes a Foo. In generalmake-record
takes the type index as an immediate, and has one stack argument per field.Where it gets interesting is the question: what arguments do we pass to
make-record
? Let's say we have a corresponding C struct:and a function
how does that foo argument translate to C's ABI? One reasonable way is to destructure the struct into its components, and pass those all as arguments individually. Another reasonable way is to stack-allocate the argument in the caller's frame, and pass the pointer in (this is what Clang does, and I think is a standard C ABI thing).
If we destructure, the adapter is just re-structuring those arguments back into a record. If we pass by pointer however, we now need some way to read fields off that pointer.
What I'm doing for the time being is defining an exported getter function for each field in the C struct. This functions, but can almost-certainly be improved. I'm not sure how to improve it without respecifying load+store instructions in interface adapters. We would also need to do similar for gc objects.
The nice thing about
call-export
is it lets us defer reimplementing anything expressible with wasm instructions. The downsides are that it requires a specific kind of toolchain integration to generate those exports (not really too bad), and it relies on engine inlining to not be inefficient.So that's the general sketch of a design I've been working with. Thoughts?
The text was updated successfully, but these errors were encountered: