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:
(@interface 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?