This is the start of an effort to merge zkgroup into libsignal-client,
including its Java, Swift, and TypeScript wrappers. For now we'll just
concentrate on getting the Rust crate to build and pass its tests.
This lets us distinguish between getters that can provide a slice
&[u8] and those that provide a fresh Vec<u8>. The convenience of
converting a Box<[u8]> to a Vec<u8> remains.
The purpose of Env was to avoid copying bytes from a slice &[u8] into
a Vec and then again from a Vec into a Java byte[] or TypeScript
Buffer. However, that's only relevant if the operation being bridged
didn't already produce a Vec. Additionally, if it produces a slice but
that slice is kept alive by one of the parameters, it can just be
returned directly. The only place where we were actually saving a copy
was when the function looked like this:
let intermediate = input.derive_intermediate();
let result: &[u8] = intermediate.access_result();
Ok(env.buffer(result))
And in practice, there were only two of these, one of which was used
only for testing. That doesn't justify the complexity of Env. Look how
much code got deleted!
This commit leaves 'bridge_get_bytearray!' in an inefficient state,
always copying to a Vec even when a slice is available. The next
commit will remedy that.
Unlike bridge_get or bridge_get_bytearray, bridge_deserialize doesn't
do any complicated transformation of the return value to accept
optional or non-optional, failable and non-failable results alike. At
the same time, its syntax has been subtly different from the other
bridge_fn macros, dating from when we were first setting up this
library. Since the extra parameters to rename or disable a particular
bridge's entry point were rarely used, this commit removes them and
replaces those use sites with spelled-out bridge_fns. This in turn
allows removing the custom per-bridge implementations of
bridge_deserialize in favor of a bridge_fn-based implementation like
bridge_get already has.
Unlike the FFI and JNI bridges, the argument handling for wrapped Rust
values in Node can't be handled with a simple blanket trait impl. Add
a set of new traits and helpers to capture most of the complexity so
that the macro doesn't have anything too complicated in it:
- BridgeHandle - mostly a marker trait, like ffi::BridgeHandle and
jni::BridgeHandle, but has a Strategy associated type to choose
between Immutable-style boxing and Mutable-style boxing.
- BridgeHandleStrategy - chooses between Immutable-style boxing (no
extra wrapper) and Mutable-style boxing (wrap in a RefCell for
safety). Can also be used to guard a particular function to only
work with immutable or mutable bridge handles
- JsBoxContentsFor<T> - shorthand for `T::Strategy::JsBoxContents`,
which Rust can't always resolve without fully spelling out the
traits involved. In practice, this will either be `T` or
`RefCell<T>`.
- BorrowedJsBoxedBridgeHandle - a struct that represents a synchronous
borrow of a wrapped value. By making the borrow type a generic
parameter, we can have `BorrowedJsBoxedBridgeHandle<&T>` (immutable
borrow of an immutable value), `BorrowedJsBoxedBridgeHandle<Ref<T>>`
(immutable borrow of a mutable value), and
`BorrowedJsBoxedBridgeHandle<RefMut<T>>` (mutable borrow of a mutable
value).
- PersistentBorrowedJsBoxedBridgeHandle - a struct for
*asynchronously* borrowing a wrapped value (by keeping around its
JavaScript wrapper as a GC root). This type already existed under the
name PersistentBoxedValue, but it's been tweaked so that it's more
similar to BorrowedJsBoxedBridgeHandle and
PersistentArrayOfBorrowedJsBoxedBridgeHandles.
- PersistentArrayOfBorrowedJsBoxedBridgeHandles - a struct for
borrowing a whole array of boxed values and, uh, hoping JavaScript
won't change the array out from under us. This also already existed
under the name PersistentArrayOfBoxedValues, but the safety is more
clearly documented now.
- clone_from_wrapper and clone_from_array_of_wrappers - functions to
clone a bunch of boxed mutable values, so that they won't change
during asynchronous use. Not the most efficient thing, but pretty
straightforward, at least.
All of the convert.rs files also need some reorganizing and the
contents of this commit message should probably turn into a
module-level doc comment once it settles.
Previously, we had HKDF-for-session-version-3, which matches RFC 5869,
and HKDF-for-session-version-2, which produced slightly different
results. However, nothing in the current versions of Signal uses
anything but the RFC-compliant version. Therefore, this commit removes
support for version 2 and deprecates the entry points that take a
version:
- Java: The HKDFv3 class is deprecated in favor of static methods on
the HKDF class.
- Swift: The hkdf function that takes a 'version' parameter is
deprecated in favor of a new overload that does not.
- TypeScript: The HKDF class is deprecated in favor of a top-level
hkdf function.
- Rust: The libsignal-protocol implementation of HKDF has been removed
entirely in favor of the hkdf crate.
There are no significant benchmark deltas from this change, and a
minimal code size increase that's the cost for removing our own
implementation of HKDF. The deprecations can be removed as a later
breaking change.
The Swift version of the NativeHandleGuard change for Java. This one's
mostly being proactive, since the Swift compiler will not optimize
across modules at this time without explicitly marking code as
inlinable, but it's possible that an operation that creates and
destroys an object entirely within the SignalClient module could have
the deinitialization of the Swift wrapper happen before the Rust
object pointer's final use. withExtendedLifetime protects against
this, and withNativeHandle wraps that up to access the native object
pointer at the same time.
Previously some classes used ClonableHandleOwner and some managed
native handles manually. Breaking out the non-cloning parts of
ClonableHandleOwner into a superclass and consistently using inherited
initializers allows us to handle wrappers of Rust objects more
uniformly.
If garbage collection happens at exactly the wrong time, the Java
wrapper around a Rust object (such as SessionRecord) can be finalized
while the Rust object is being used, via its opaque 'nativeHandle'
(address cast as integer). Avoid this by adding a NativeHandleGuard
type that keeps the wrapper alive, as well as a low-level entry point
`Native.keepAlive(...)` that does nothing but serve as a sort of GC
guard, similar to `Reference.reachabilityFence()` in Java 9.
This knocks about 10% off of the built binary for Android (per slice),
to balance out the increased size from the new toolchain and stdlib.
Applying the same `opt-level=s` option for `cargo bench` (on desktop)
gives a roughly 1% slowdown, a trade-off that's worth it.