Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trait-based Web API bidnings #3934

Open
MOZGIII opened this issue Apr 24, 2024 · 2 comments
Open

Trait-based Web API bidnings #3934

MOZGIII opened this issue Apr 24, 2024 · 2 comments

Comments

@MOZGIII
Copy link

MOZGIII commented Apr 24, 2024

Let's take a WritableStream for example.

If it was a trait, could just implement it for a JsValue, like so: impl WritableStream for JsValue { ... } (with some codegen of course).

Then, if needed, we could very elegantly extend WritableStream with WebTransportSendStream with an explicit trait WebTransportSendStream: WritableStream { ... } - and also implement the WebTransportSendStream for JsValue.


With the given WebIDL:

WebIDL

[Exposed=*, Transferable]
interface WritableStream {
  constructor(optional object underlyingSink, optional QueuingStrategy strategy = {});

  readonly attribute boolean locked;

  Promise<undefined> abort(optional any reason);
  Promise<undefined> close();
  WritableStreamDefaultWriter getWriter();
};
[Exposed=(Window,Worker), SecureContext, Transferable]
interface WebTransportSendStream : WritableStream {
  attribute WebTransportSendGroup? sendGroup;
  attribute long long sendOrder;
  Promise<WebTransportSendStreamStats> getStats();
  WebTransportWriter getWriter();
};

The traits would look somewhat like this:

Traits

trait WritableStream: Sized + JsAny {
    fn new() -> Self;
    fn new_with_underlying_sink(underlying_sink: &impl JsObject) -> Self;
    // ...

    fn locked(&self) -> bool;

    fn abort(&self) -> Promise<()>;
    fn abort_with_reason(&self, reason: &impl JsAny) -> Promise<()>;

    fn close(&self) -> Promise<()>;

    fn get_writer(&self) -> Result<impl WritableStreamDefaultWriter>;
}

trait WebTransportSendStream: WritableStream + JsAny {
    fn send_group(&self) -> Option<impl WebTransportSendGroup>;
    fn set_send_group(&self, value: &impl WebTransportSendGroup) -> Option<impl WebTransportSendGroup>;
    fn unset_send_group(&self);

    fn send_order(&self) -> i64;
    fn set_send_order(&self, value: i64);
    fn unset_send_order(&self);
    
    fn get_stats(&self) -> Promise<impl WebTransportSendStreamStats>;

    fn get_writer(&self) -> Result<impl WebTransportWriter>;
}

and impls like this:

Impls

impl WritableStream for JsValue {
    fn new() -> Self {
        __shim_WritableStream_new()
    }

    fn new_with_underlying_sink(underlying_sink: &impl JsObject) -> Self {
        __shim_WritableStream_new_with_underlying_sink(underlying_sink)
    }

    // ... and so on.
}

impl WebTransportSendStream for JsValue {
    fn send_group(&self) -> Option<impl WebTransportSendGroup> {
        __shim_WebTransportSendStream_send_group()
    }

    // ... and so on.
}


A couple things to note:

  • the idea of having a JsAny type, that would work somewhat like a dyn std::any::Any and enable typechecking of the underlying type of any JsValue:

    let random_value: JsValue = ...;
    let stream: impl WebTransportSendStream = JsAny::as_unchecked<dyn WebTransportSendStream>(random_value); // might also have an `instanceof` and/or custom guard variants.

    UPD: no! actually, what we just need is this: trait JsAny { fn as_js_value(self: Self) -> JsValue }; JsValue already has all the impls, so this effectively allows us to cast impl WebTransportSendStream to JsValue and from there to anything:

    let random_value: JsValue = ...;
    // Works cause of impl WebTransportSendStream for JsValue.
    let stream: impl WebTransportSendStream = random_value;
    // Works cause WebTransportSendStream: WritableStream.
    let stream: impl WritableStream = stream;
    //  Doesn't work, as this type of generalization is not necessarily ture: not every `impl WritableStream` is a `impl WebTransportSendStream`
    let stream: impl WebTransportSendStream = stream;
    //  This works though.
    let stream: impl WebTransportSendStream = JsAny::as_js_value(stream);

    This might look scary - but you would just always use generics or trait object if an issue is encountered here.

    Also, we should consider having something like this: JsValue<dyn WebTransportSendStream> - the JsValue is already effectively a pointer to a value, but this way it can also carry on the information about the associated API.

    Then we'd implement the traits for impl WebTransportSendStream for JsValue<dyn WebTransportSendStream> { ... } to provide the type restrictions...

  • the shims being standalone functions instead of being implementation details of a given type.

Originally posted by @MOZGIII in #3933 (comment)

@pablosichert
Copy link

I don't quite understand why the initial random_value has its type erased to JsValue, could you elaborate on that?

For the (persumably main) goal of using a WebTransportSendStream as WritableStream, wouldn't it be better if the WebTransportSendStream trait had a method fn into_writable_stream(self) -> impl WritableStream?

@MOZGIII
Copy link
Author

MOZGIII commented Apr 24, 2024

The idea is that if we implement all the traits for the same type, we can sort of "view" any JsValue as any interface. It is actually somewhat meaningful with the dynamic typing of JS.

It is, however, very inconvenient, as it effectively erases the types - you can still restrict the types as impl Trait - and you can even build the whole app on those impl traits - but the loss of concrete types is actually a huge inconvenience.

But we effectively want to represent a somewhat TypeScript-y thing, where we have any arbitrary types, but we view them as implementing certain interfaces or traits. We don't necessarily want to have a notion of the concrete type - but we want some fixes view of the value type, rather than having a generic JsValue that implements all traits. It would also be nice to have a ways to do a TypeScript-like type calculus in Rust.

So, that's where JsValue<dyn Trait> comes into play - the dyn Trait would merely be a marker of which interface is available at a given value, capturing a particular one with an arguably better type-safety than using just impl Trait and catch-all type-erased JsValue. We'd still want to able to freely case the types across - but we'd also want to combine and compose the interfaces more easily - and this is something the the current design of wasm bindgen is struggling with imo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants