Blanket implementation of a foreign trait over another foreign trait?

I would like to have a function as follows:

fn do_something(a: AsRef<JsValue>, b: AsRef<JsValue>) { ... }

Now, I would like to this function to accept either AsRef<JsValue> or something that implements Serialize and that can be converted to a JsValue as follows:

let bytes = bincode::serialize(&something).unwrap();
let value: JsValue = Uint8Array::from(&bytes[..]).into();

However, I am struggling a bit with creating the right combination of traits to make this happen. If it were possible to do a blanket implementation of a foreign trait over another foreign trait, the answer would be simple:

impl<T> Into<JsValue> for T where T: Serialize {
    fn into(self) -> JsValue {
        let bytes = bincode::serialize(&self).unwrap();
        Uint8Array::from(&bytes[..]).into()
    }    
}

However, this is not allowed and I think I need to create my own trait to get the behaviour that I am looking for. I tried the following but this leads to (potentially) conflicting implementations of the Proxy trait.

trait Proxy {
    fn proxy(self) -> JsValue;
}

impl<T> Proxy for T where T: Serialize {
    fn proxy(self) -> JsValue {
        let encoded = bincode::serialize(&self).unwrap();
        let buffer = Uint8Array::from(&encoded[..]).buffer();
        Array::of1(&buffer).into()
    }
}

impl<T> Proxy for T where T: Into<JsValue> {
    fn proxy(self) -> JsValue {
        self.into()
    }
}

Is there a way to solve this problem, perhaps using a combination associated types on the traits?

The question is, what should happen when the type has both Serialize and Into<JsValue>? Which implementation must be used? AFAIK, there's no way to express this in current Rust.

In this particular case, Into<JsValue> should take priority over Serialize, which is a fallback. I think logically this is ok, but I do not know how I can encode this logic into Rust's trait system... It would be neat if we could do something like this:

impl<T> Proxy for T where T: Into<JsValue> {
    fn proxy(self) -> JsValue {
        // ...
    }
}
else where T: Serialize {
    fn proxy(self) -> JsValue {
        // ...
    }
}

You can't use trait bounds for "either Into<JsValue> or Serialize". You could write two different functions for that though:

/*
[dependencies]
serde = { version = "1", features = ["derive"] }
bincode = "*"
wasm-bindgen = "*"
js-sys = "*"
*/
use serde::Serialize;
use wasm_bindgen::JsValue;
use js_sys::{Uint8Array, Array};

fn serialize_to_js_value(t: impl Serialize) -> JsValue {
    let encoded = bincode::serialize(&t).unwrap();
    let buffer = Uint8Array::from(&encoded[..]).buffer();
    Array::of1(&buffer).into()
}

fn do_something_serialize(a: impl Serialize, b: impl Serialize) {
    let a = serialize_to_js_value(a);
    let b = serialize_to_js_value(b);

    do_something(a, b);    
}

fn do_something(_a: impl AsRef<JsValue>, _b: impl AsRef<JsValue>) {}

#[derive(Serialize, Clone)]
struct Foo {
    a: String,
    b: bool,
}

fn main() {
    let f = Foo {
        a: "hello".to_owned(),
        b: true,
    };

    do_something_serialize(f.clone(), f);
}

Rustexplorer (compiles but panics at runtime, because it isn't executed in a wasm context).

You might want to take a look at the trait specialization RFC for how Rust's trait system should become more flexible in terms of generic parameters and how to determine which implementation of a trait to use. But trait specialization is not going to be implemented very soon.

This solution is not really viable for the problem I am describing. Part of the problem is that I am generating do_something using a macro so it may take the form of:

fn do_something2(
    _a: impl AsRef<JsValue>,
    _b: impl AsRef<JsValue>,
) {}

// or
fn do_something3(
    _a: impl AsRef<JsValue>,
    _b: impl AsRef<JsValue>,
    _c: impl AsRef<JsValue>,    
) {}

// or
fn do_something4(
    _a: impl AsRef<JsValue>,
    _b: impl AsRef<JsValue>,
    _c: impl AsRef<JsValue>,
    _d: impl AsRef<JsValue>,    
) {}

where the call sites look something like:

// argument 2 needs to be serialized
do_something2(true, vec![...]); 

// argument 1 needs to be serialized
do_something3(vec![...], true, 42);

// argument 1 needs to be serialized
do_something4(vec![...], true, 42, "hello");

I can change the AsRef<JsValue> to a different trait, i.e., one that I define (that then defines in turn how I can get a JsValue from the argument), but each parameter of the do_something functions has to be the same and has to be able to accept things that are either Serialize or Into<JsValue>.

I don't believe this is possible.

trait IntoJsValue {
    fn into_js_value(self) -> JsValue;
}

impl<T: Serialize> IntoJsValue for T {
    fn into_js_value(self) -> JsValue {
        let encoded = bincode::serialize(&self).unwrap();
        let buffer = Uint8Array::from(&encoded[..]).buffer();
        Array::of1(&buffer).into()    
    }
} 

impl<T: Into<JsValue>> IntoJsValue for T {
    fn into_js_value(self) -> JsValue {
        self.into()
    }
}

Imagine a type T that implments both Into<JsValue> and Serialize. The compiler can't tell which version of IntoJsValue to choose from for T, telling you there are conflicting trait implementations: E0119.

1 Like

What you want is called specialization and is not currently possible in Rust. There is a nightly feature that implements it, but it's incomplete and unsound (with it you can cause UB without unsafe blocks), so please don't use it.

What you could do though would be providing something like:

struct SerializedJsValue<T>(T);

impl<T: Serialize> IntoJsValue for SerializedJsValue<T> {
    fn into_js_value(self) -> JsValue {
        let encoded = bincode::serialize(&self.0).unwrap();
        let buffer = Uint8Array::from(&encoded[..]).buffer();
        Array::of1(&buffer).into()    
    }
}

Then the user would have to call your functions with do_something2(actual_into_jsvalue, SerializedJsValue(serializable_value))

Another way, arguably much more hacky, would be changing your function into a macro and internally use autoref specialization. Note though that this has major drawbacks like always requiring a macro call and not properly working in generic contexts.

1 Like

Alternative approach (XY-ish)

What I'd personally do, here, would be to have a .js_encode() convenience method (e.g., through an extension trait) to go from an impl Serialize to a JsValue, while keeping the main APIs taking impl AsRef<JsValue> args:

#[extension(trait JsEncode)]
impl<T : Serialize> T {
    fn js_encode(&self)
      -> JsValue
    {
        let bytes = ::bincode::serialize(self).unwrap();
        Uint8Array::from(&bytes[..]).into()
    }
}

and then:

my_js_function(some_js_value, some_serdable.js_encode());
4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.