uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

Ability to specify foreign type when exporting via proc macro (e.g. bytes for Vec<u8>)

Open ijc opened this issue 2 years ago • 9 comments

I have a struct with Vec<u8> fields which is exported with #[derivce(uniffi::Record)] as well as exported methods which receive Vec<u8>. In Kotlin these get rendered as List<UByte>, as if they were sequence u8 in UDL.

If I were using UDL then I could name the type bytes and this would be rendered as ByteArray in Kotlin which is a little easier to work with Kotlin side (AIUI it's more idiomatic generally but it also avoids signed vs unsigned bytes disconnect from Rust v Kotlin -- right now I seem to have to write foo.toUByteArray().toByteArray() on the Kotlin side).

I don't think that proc macros currently allow picking the types.

Would it be possible/desirable to have the proc macros default to bytes for Vec<u8> as a special case? (I don't think this makes sense as a per language choice, but I suppose that's an option too).

If not, could there be some other method for overriding the types, e.g. a #[uniffi(type = bytes)] type attribute on struct fields and method arguments perhaps? (not sure how that works for return types though?). This seems more general but is surely harder to implement.

ijc avatar Jul 06 '23 10:07 ijc

This seems tricky.

Would it be possible/desirable to have the proc macros default to bytes for Vec as a special case?

Allowing a default in uniffi.toml or via some other mechanism makes some sense, but probably still leaves the general question open - how to specify when you don't want the default used?

Supporting only Bytes seems a bit heavy-handed, and (IMO) is something we'd want to do in UDL too, and for some consumers might be a significant b/w compat burden. Maybe there's a path to that future which can be made less painful though?

#[uniffi(type = bytes)] gets a bit ugly for Option, Vec and Map - but maybe that's doable?

I had a vague idea of using a type alias - eg, type UniffiBytes = Vec<u8>; and saying that you need to use that, but that only goes so far - there are still parts which are unable to differentiate given it's a simple alias - specifically, it will be (IIUC) impossible in the current code to have a different FfiConverter for UniffiBytes than for Vec<u8> which I think would be a requirement to make this work. Using a "new type" (ie, type UniffiBytes(Vec<u8>);) isn't going to fly either.

mhammond avatar Jul 14 '23 16:07 mhammond

#[uniffi(type = bytes)] gets a bit ugly for Option, Vec and Map - but maybe that's doable?

Could we say that type can be any valid UDL type definition so support Option via ? and vec via sequence etc? e.g.

#[uniffi(type = "bytes?")]
Option<Vec<u8>>

???

Perhaps taking inspiration from serde there is space for a #[uniffi(as = SomeType)] (where SomeType is an FfiConverter or #[uniffi(with = SomeFn)] where SomeFn is some sort of function helper thing) type constructs too (I don't have an immediate use case for either though).

I can see why type UniffiBytes = Vec<u8> wouldn't work, but what is the problem with type UniffiBytes(Vec<u8>) having a different FfiConverter to Vec<u8>?

ijc avatar Jul 17 '23 10:07 ijc

#[uniffi(type = bytes)] gets a bit ugly for Option, Vec and Map - but maybe that's doable?

Could we say that type can be any valid UDL type definition so support Option via ? and vec via sequence etc? e.g.

#[uniffi(type = "bytes?")]
Option<Vec<u8>>

Maybe? I'm not even sure how possible it is to put attributes on individual args and on a result type, which this would require.

I can see why type UniffiBytes = Vec<u8> wouldn't work, but what is the problem with type UniffiBytes(Vec<u8>) having a different FfiConverter to Vec<u8>?

That probably forces every arg/result which uses UniffiBytes to take that type, whereas they are expecting a plain old Vec<u8>

mhammond avatar Jul 17 '23 13:07 mhammond

I'm not even sure how possible it is to put attributes on individual args and on a result type, which this would require.

Ah, yes, I was focused on Records since that is where I was having Vec<u8>.

https://doc.rust-lang.org/reference/attributes.html says "Function, closure and function pointer parameters accept outer attributes", so I guess that is allowed. I'm not sure about result types either.

Perhaps this could be done as an attribute on the function itself, accepting UDL syntax, maybe:

#[uniffi::export(prototype="bytes(u32, sequence u8)")]
fn foo(a: u32, b: Vec<u8>) -> Vec<u8>

(syntax is like the UDL function syntax but without the function name since that is redundant, I don't know if that is possible syntactically in whatever parsing library is used).

That probably forces every arg/result which uses UniffiBytes to take that type

Got it.

ijc avatar Jul 17 '23 14:07 ijc

I would guess bytes is more common for Vec<u8> than sequence u8, so maybe it'd be good to default to bytes and not sequence u8.

heinrich5991 avatar Jul 24 '23 21:07 heinrich5991

so maybe it'd be good to default to bytes and not sequence u8.

I believe the problem is that we simply can't differentiate at binding generation time - ie, "default to" implies we are able to select between one or the other, but I don't think we can.

We could stop supporting sequence u8 entirely, in which case we could just remove the bytes type entirely.

mhammond avatar Jul 25 '23 14:07 mhammond

We could stop supporting sequence u8 entirely, in which case we could just remove the bytes type entirely.

That seems like a non-optimal outcome. Going forward, this would mean that uniffi can only do distinctions that the Rust type system can, but as we can see in this case, sometimes other language's type systems are more expressive than Rust's.

heinrich5991 avatar Jul 25 '23 18:07 heinrich5991

That seems like a non-optimal outcome.

Yeah, I agree, and I'm not advocating for that. I was really just trying to point out the complication and why the concept of having a "default" doesn't really make sense here.

mhammond avatar Jul 25 '23 18:07 mhammond

Sorry, my use of the phrase "default to" seems to have complicated matters. My actual requirements would be satisfied by some way (e.g. in uniffi.toml) to tell uniffi to always use bytes for Vec<u8>, or for it to just always do so (that's what I meant by default to, although I see how that implies some other alternative must exist). I don't currently have a use case for a project with a mixture of bytes and sequence u8's, I feel like that would be a slightly unusual use case (tbh beyond possible back compatibility concerns I don't see much use for sequence u8 over bytes at all).

ijc avatar Jul 25 '23 21:07 ijc