-
Notifications
You must be signed in to change notification settings - Fork 228
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
Experiment: replace protobuf with uniffi #3900
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I think this approach would work. But also, it's not clear to me how much effort it would save versus fully converting the FFI over to using UniFFI. Maybe it would be useful if there are some tricky bits in the FFI that UniFFI isn't well set up to handle just yet.
Still, an interesting experiment! And the promise of letting us decouple from the firefox-ios Swift version may be a nice motivator.
this.url, | ||
emptyList(), | ||
null | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The protobuf builder pattern is much nicer than a bunch of null
fields, but OTOH, this whole BookmarkNode
class is custom designed to work with the grain of protobuf; more thoughts on that below.
title = title, | ||
url = msg.url | ||
url = node.url!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why the protobuf version of this code didn't have to check for the presence of these fields, similar to the null checks I'm doing here. I feel like maybe protobuf just gives us a default value if some bug means that the expected field was not present? Weird spooky action at a distance...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh, yeah, how the "old" version works here is beyond me
@@ -27,55 +26,55 @@ internal interface LibPlacesFFI : Library { | |||
/** Create a new places api */ | |||
fun places_api_new( | |||
db_path: String, | |||
out_err: RustError.ByReference | |||
out_err: RustError2.ByReference |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably use the uniffi-generated RustError
class here, with a bit of tweaking. I just punted on that for now.
): Pointer? | ||
|
||
fun places_pinned_sites_import_from_fennec( | ||
handle: PlacesApiHandle, | ||
db_path: String, | ||
out_err: RustError.ByReference | ||
out_err: RustError2.ByReference | ||
): RustBuffer.ByValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By contrast, this is using the uniffi-generated RustBuffer
class.
// Different names between Rust and Kotlin, for...reasons? | ||
typealias VisitType = VisitTransition | ||
typealias VisitInfo = HistoryVisitInfo | ||
typealias VisitInfosWithBound = HistoryVisitInfosWithBound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this a lot, where we give things slightly different names on the different sides of the API boundary, but it's not clear to me what motivates any particular naming choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I'm also not sure where these differences come from, but IMO they are imposing a cost we should try and avoid. I'm somewhat use to expecting camelCase etc differences, but these gratuitous naming difference just cause confusion.
/** An array of bookmark nodes, since we can't represent that directly */ | ||
message BookmarkNodeList { | ||
repeated BookmarkNode nodes = 1; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also don't need these special "holder for a list of T" types because we can just have sequence<T>
.
string title; | ||
string? icon_url; | ||
i64 frecency; | ||
sequence<MatchReason> reasons; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of the fields on SearchResult
here not being returned over the FFI, such as icon_url
. Should they be?
}; | ||
|
||
// This just makes sure we have some extra helper functions available, | ||
// to be called from our hand-written bindings. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it's UniFFI's turn for some weirdness workarounds!
UniFFI tries not to generate helper code for types that your API doesn't use. We have some functions that want to return e.g. a sequence<SearchResult>
, so we want UniFFI's helper code for lifting that type. But UniFFI won't generate that helper code unless it sees the type somewhere in the interface. So, we fake it here, at least until the type appears organically in the signature of a uniffied function.
@@ -28,7 +28,7 @@ use sync_guid::Guid as SyncGuid; | |||
use types::Timestamp; | |||
use url::Url; | |||
|
|||
pub use public_node::PublicNode; | |||
pub use public_node::InternalBookmarkNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming here is wierd, I don't think the PublicNode
struct ever appears in a public API, does it? It seems more like an internal transition point between the database and the actual public data (previously the message type, now the uniffi dictionary type).
Some(n.child_nodes.is_some()) | ||
} else { | ||
None | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Option<Vec<T>>
FTW
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome and offers real value in our longer term. I'd also note that if you can resolve the 2
bits, the patch would be a reasonable size.
TL;DR I think it probably could, but I'm not sure sure if it worth the effort vs doing a full uniffication.
I guess I don't have any understanding of how complimentary those efforts would be, and how much extra work is involved in the "full" unification - but if it's significant, or not wasted work if we went "full" later, I'd say there's enough value here to push forward. Given the benefits are longer term, it might also be a good "onboarding" task? (Purposely ambiguous about what "it" is in that last sentence :)
title = title, | ||
url = msg.url | ||
url = node.url!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh, yeah, how the "old" version works here is beyond me
// Different names between Rust and Kotlin, for...reasons? | ||
typealias VisitType = VisitTransition | ||
typealias VisitInfo = HistoryVisitInfo | ||
typealias VisitInfosWithBound = HistoryVisitInfosWithBound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I'm also not sure where these differences come from, but IMO they are imposing a cost we should try and avoid. I'm somewhat use to expecting camelCase etc differences, but these gratuitous naming difference just cause confusion.
}; | ||
use uniffi::ViaFfi; | ||
use places::error::*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like type ByteBuffer = uniffi::RustBuffer
would help keep the diff down
@@ -199,15 +197,15 @@ pub struct SearchResult { | |||
|
|||
/// The URL to open when the user confirms a match. This is | |||
/// equivalent to `nsIAutoCompleteResult.getFinalCompleteValueAt`. | |||
pub url: Url, | |||
pub url: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autofill would (maybe) like this too - there it would be more about 'guid' where we use a guid type internally but strings with uniffi, which jars a little. I haven't looked into how hard that would be though, but just a little +1 for that concept.
}; | ||
|
||
|
||
enum BookmarkType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice alot of the diff above is just the case changing of this enum - is there a particular reason you didn't leave the old names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, yeah, UniFFI automagically uppercases enum variant names in the Kotlin bindings, since this seems to be the prevailing style convention for Kotlin code.
@@ -40,7 +65,7 @@ message HistoryVisitInfosWithBound { | |||
* Note that these docs comments are internal, and don't necessarily impact the actual | |||
* API we expose to Kotlin/Swift (this is particularly true around reads). | |||
*/ | |||
message BookmarkNode { | |||
dictionary BookmarkNode { | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, autofill has landed on using 2 versions of the struct - one for "incoming" (eg, "create an item" where guid might be null) and one for outgoing (eg "fetch an item" where guid is never null). I assume this is used in both directions which is why everything is nullable.
(Not sure why I'm noting this TBH :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, yes, I think separate "incoming" and "outgoing" would make a big difference here.
I think it would mostly not be wasted work, but a concrete step towards more thorough uniffication.
Oooh, I like this idea :-) |
This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's mozilla#3900, but limits the scope to just the new-ish history_metadata module.
This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's mozilla#3900, but limits the scope to just the new-ish history_metadata module.
This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's mozilla#3900, but limits the scope to just the new-ish history_metadata module.
There is a much less "work in progress" version of this in #4183, so I'm going to close this experimental PR in favour of that actually-working-towards-landing one. |
This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's #3900, but limits the scope to just the new-ish history_metadata module.
This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's mozilla#3900, but limits the scope to just the new-ish history_metadata module.
…i. (#4183) This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's #3900, but limits the scope to just the new-ish history_metadata module. Co-authored-by: Ryan Kelly <[email protected]> Co-authored-by: Sammy Khamis <[email protected]>
…i. (#4183) This is intended to be a small but significant step on the road to uniffi-ing places. It takes all the hard stuff from Ryan's #3900, but limits the scope to just the new-ish history_metadata module. Co-authored-by: Ryan Kelly <[email protected]> Co-authored-by: Sammy Khamis <[email protected]>
I timedboxed a little experiment this afternoon, and it went well enough that I wanted to push it for early feedback (although it's a long way off being landable!)
Our dependency on external protobuf libraries is annoying, and it's particularly troublesome for iOS because iOS consumers need to configure
SwiftProtoBuf
into their app as an extra dependency. In addition, the presence ofSwiftProtoBuf
means we can't build our iOS binaries withBUILD_LIBRARY_FOR_DISTRIBUTION
, which means iOS consumers have to be using the exact same version of Swift as our build infra, which is regularly a problem.Our use of protobuf seems fairly entrenched, but what if it didn't have to be?
One of the things I learned from #3876 is that it's pretty easy to have hand-written Kotlin and Swift code side-by-side with code generated by UniFFI. So I wanted to see whether UniFFI could replace our current use of protobuf without having to commit to wholesafe uniffication of all our current crates.
TL;DR I think it probably could, but I'm not sure sure if it worth the effort vs doing a full uniffication.
What we have here is the
places
component with protobuf messages removed and uniffidictionary
types inserted in their place, on both the Rust side and the Kotlin side (I haven't looked at the swift code yet). It builds! The Kotlin tests don't pass yet though.I'm going to leave a couple of thoughts on the draft PR here, and I'd love to hear any feedback others might have. But just to be clear: I'm pushing this entirely as a conversation topic, not for any sort of landing at this stage.