WebAssembly / interface-types

Other
641 stars 57 forks source link

Binding types for Nullable #5

Closed magcius closed 5 years ago

magcius commented 6 years ago

WebIDL supports the concept of a Nullable type for both return values and arguments, and it is a popular feature for host bindings. This spec has no current way to point to a null object.

Proposal: For each "typed object table", the 0th index in the table is always the null object for that type. It is an error to set this index in the table to a valid object from either WASM or the host. The rest of the spec language holds, except that in the case where NEXT_SLOT would be incremented and that slot index passed, NEXT_SLOT is not incremented when a null object is passed.

This proposal still leaves no ability to pass a null STRING or ARRAY_BUFFER.

magcius commented 6 years ago

This proposal also has no ability to allow e.g. nullable boolean types, as seen in Example 35 of https://heycam.github.io/webidl/#idl-nullable-type

domenic commented 6 years ago

In general it's important to keep in mind Web IDL is not the target here. JavaScript is the target, and Web IDL might be a useful way of generating tables for some JavaScript objects. But a lot of the issues you've filed recently are very Web IDL focused, which I think is counterproductive toward this spec's goals.

magcius commented 6 years ago

The extreme edges of "WebIDL" I imagine aren't something this spec is interested in handling (#6 is trying to ask whether we should be interested -- these things do come up in practice), but I'm using WebIDL roughly as a proxy for "JS/DOM APIs".

Nullable types felt to me like an obvious gap, and I imagine anyone could very easily come up with web API examples of it in practice.

domenic commented 6 years ago

Well, for example, JS doesn't really have a type system at all. So I think the scope is much more limited than what you're implying. E.g. there's nothing preventing you from passing an object reference to something supposedly accepting a "nullable Boolean". It will just be interpreted as true by that API.

magcius commented 6 years ago

OK, I may be way off about the goals of this spec. Is it a goal so that if I call a host JS function through WebAssembly through some method, I can come up with equivalents to all these calls?

foo(false);
foo(true);
foo(null);
foo(undefined);
foo(1);
foo({});

Currently, I can only make the fifth and sixths calls from this spec, and only by declaring two separate imports for foo (and it seems unspecified to me whether you can have multiple imports for the same name).

If not all of them, how many of them should I be able to make? There's no WebIDL in this question, just JavaScript.

annevk commented 6 years ago

@domenic from discussion with @lukewagner I think IDL or something like it is very much the target. Being able to use the type information in wasm to skip a bunch of IDL overloading and casting should be a goal. Both the web platform and wasm have types, it makes sense to utilize that for performance gains.

domenic commented 6 years ago

Sure, but that's an implemenation detail, and shouldn't affect the proposal's spec. It's important that author JS APIs be first-class, and Web IDL/C++ APIs not be the focus. Perhaps something to discuss on a future CG call.

lukewagner commented 6 years ago

Regardless of WebIDL vs. JS focus, I think there is an interesting expressiveness question here.

So at the moment, our one support Table element type, anyfunc, is nullable. (If you Table.get(i) such an element you get null, and if you try to call it, it safely traps.) And this is also somewhat inherent in the design of tables since grow_table doesn't take initializing elements and we don't require that elem sections initialize all elements of a table defined by the module.

So given that, I wonder if what the proposal has is already sufficient: when wasm uses the OBJECT_HANDLE binding, the associated WebIDL type is inherently nullable. What this means is that a WebIDL method taking a non-nullable type has to do a dynamic null-check, but that should be fine and cheap.

Nullable non-reference types is an interesting question, though. Probably not a critical feature for the "host bindings MVP", but I could imagine we have some NULL_OR_PASS_THROUGH binding that takes a sentinel value as an immediate and if the given i32 (or other scalar type) equals the sentinel, null is passed instead of passing through the value.

magcius commented 6 years ago

I don't like that the compiler has to track the number of live objects to always find a null reference at the end to pass. In several cases, I think it would be undecidable.

lukewagner commented 6 years ago

@magcius I think no such compiler analysis would be required: as with the other host bindings discussed, we'd want to express this statically in the C++ type of the function via magic __attribute__s and whatnot.

magcius commented 6 years ago

How do I compile this function:

import void SomeHostBinding(Object* p);

void MyFunc(int a, Object *p) {
    if (a > 50)
        SomeHostBinding(p);
    else
        SomeHostBinding(nullptr);
}

The compiler has to somehow retrieve a slot index that is guaranteed to be null at all times, no? That depends on the current size of the table, plus what members have been initialized so far.

lukewagner commented 6 years ago

In that example, you can't pass a raw C++ pointer out (well you literally can, but it'll pop out on the other side as an i32 offset into linear memory). What you need instead is some sort of rooting API (similar to the V8 or SM rooting API) which lets C++ indirectly refer to a DOM/JS object created elsewhere and stored in a Table. Brad has some examples in the Toolchains section of the proposal.

magcius commented 6 years ago

Yeah, I was imagining Object* as being backed by some host reference -- think of it like a pimpl. The example code seems to just do slot management at runtime, which I suppose is an option -- if you need a new null, you just allocate a new entry in the table and don't initialize it.

I don't quite understand how WebAssembly is supposed to know whether a host object in this table is initialized or not -- is the host binding supposed to do the table lookup? The Overview document simply says the object is passed to the host, but it's unclear how that interaction happens. Finalization -- freeing a slot -- also seems to be undefined.

magcius commented 6 years ago

There's also a valid question of what happens when a host calls a wasm export with null for an ObjectReference -- there appears to be no way for wasm to check whether an table slot is initialized or not. I still think having the 0th index as a special table index that can't be initialized is a good solution for this.

lukewagner commented 6 years ago

I think the C++ source will need to statically distinguish pointers into linear memory from indices into tables (and which table).

The current proposal leaves allocation and deallocation of a table up to the user code in the wasm module. The binding just gives the binding an index into the table.

It is a good question of whether, in addition to copy_elem and set_elem_null we should have a is_elem_null; I don't see why not and seems symmetric with set_elem_null.

pchickey commented 5 years ago

Closing as out-of-date: these concepts don't map to the current proposal, which has evolved a lot since this issue was opened.