From 2f69e7344e14dece504807e7c74555f08f10c212 Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Fri, 22 Mar 2024 19:07:24 +0100 Subject: [PATCH 1/2] [uniffi] Fix compilation under `mls_build_async` UniFFI added support for async traits in mozilla/uniffi-rs#1981, but this is not yet released. This commit temporarily introduces a Git dependency on UniFFI to let us test the async functionality which was removed in #86. An unrelated UniFFI change (mozilla/uniffi-rs#1840) made our existing test fail. --- mls-rs-uniffi/Cargo.toml | 4 ++-- mls-rs-uniffi/src/config.rs | 20 +++++++++-------- mls-rs-uniffi/src/config/group_state.rs | 25 ++++++++++++++++++++-- mls-rs-uniffi/src/lib.rs | 13 +++++++++-- mls-rs-uniffi/tests/custom_storage_sync.py | 2 +- mls-rs-uniffi/tests/scenarios.rs | 4 ---- mls-rs-uniffi/uniffi-bindgen/Cargo.toml | 2 +- 7 files changed, 49 insertions(+), 21 deletions(-) diff --git a/mls-rs-uniffi/Cargo.toml b/mls-rs-uniffi/Cargo.toml index 62ab5fc3..7e7a0c9b 100644 --- a/mls-rs-uniffi/Cargo.toml +++ b/mls-rs-uniffi/Cargo.toml @@ -21,10 +21,10 @@ mls-rs = { version = "0.39.0", path = "../mls-rs" } mls-rs-core = { version = "0.18.0", path = "../mls-rs-core" } mls-rs-crypto-openssl = { version = "0.9.0", path = "../mls-rs-crypto-openssl" } thiserror = "1.0.57" -uniffi = "0.26.0" +uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "6b09f11", version = "0.26.0" } [target.'cfg(mls_build_async)'.dependencies] tokio = { version = "1.36.0", features = ["sync"] } [dev-dependencies] -uniffi_bindgen = "0.26.0" +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs/", rev = "6b09f11", version = "0.26.0" } diff --git a/mls-rs-uniffi/src/config.rs b/mls-rs-uniffi/src/config.rs index a7716672..4bf2e6b0 100644 --- a/mls-rs-uniffi/src/config.rs +++ b/mls-rs-uniffi/src/config.rs @@ -28,11 +28,11 @@ impl mls_rs_core::group::GroupStateStorage for ClientGroupStorage { type Error = Error; async fn state(&self, group_id: &[u8]) -> Result>, Self::Error> { - self.0.state(group_id.to_vec()) + self.0.state(group_id.to_vec()).await } async fn epoch(&self, group_id: &[u8], epoch_id: u64) -> Result>, Self::Error> { - self.0.epoch(group_id.to_vec(), epoch_id) + self.0.epoch(group_id.to_vec(), epoch_id).await } async fn write( @@ -41,16 +41,18 @@ impl mls_rs_core::group::GroupStateStorage for ClientGroupStorage { inserts: Vec, updates: Vec, ) -> Result<(), Self::Error> { - self.0.write( - state.id, - state.data, - inserts.into_iter().map(Into::into).collect(), - updates.into_iter().map(Into::into).collect(), - ) + self.0 + .write( + state.id, + state.data, + inserts.into_iter().map(Into::into).collect(), + updates.into_iter().map(Into::into).collect(), + ) + .await } async fn max_epoch_id(&self, group_id: &[u8]) -> Result, Self::Error> { - self.0.max_epoch_id(group_id.to_vec()) + self.0.max_epoch_id(group_id.to_vec()).await } } diff --git a/mls-rs-uniffi/src/config/group_state.rs b/mls-rs-uniffi/src/config/group_state.rs index e4fc54a0..8d20ff1f 100644 --- a/mls-rs-uniffi/src/config/group_state.rs +++ b/mls-rs-uniffi/src/config/group_state.rs @@ -1,6 +1,9 @@ use mls_rs::error::IntoAnyError; use std::fmt::Debug; +#[cfg(not(mls_build_async))] use std::sync::Mutex; +#[cfg(mls_build_async)] +use tokio::sync::Mutex; use crate::Error; @@ -28,9 +31,13 @@ impl From for mls_rs_core::group::EpochRecord { } } -#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +// When building for async, uniffi::export has to be applied _after_ +// maybe-async's injection of the async trait. When building for sync, +// the order has to be the opposite. +#[cfg_attr(mls_build_async, uniffi::export(with_foreign))] #[cfg_attr(mls_build_async, maybe_async::must_be_async)] -#[uniffi::export(with_foreign)] +#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(not(mls_build_async), uniffi::export(with_foreign))] pub trait GroupStateStorage: Send + Sync + Debug { async fn state(&self, group_id: Vec) -> Result>, Error>; async fn epoch(&self, group_id: Vec, epoch_id: u64) -> Result>, Error>; @@ -59,9 +66,15 @@ impl GroupStateStorageAdapter { Self(Mutex::new(group_state_storage)) } + #[cfg(not(mls_build_async))] fn inner(&self) -> std::sync::MutexGuard<'_, S> { self.0.lock().unwrap() } + + #[cfg(mls_build_async)] + async fn inner(&self) -> tokio::sync::MutexGuard<'_, S> { + self.0.lock().await + } } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] @@ -73,13 +86,17 @@ where { async fn state(&self, group_id: Vec) -> Result>, Error> { self.inner() + .await .state(&group_id) + .await .map_err(|err| err.into_any_error().into()) } async fn epoch(&self, group_id: Vec, epoch_id: u64) -> Result>, Error> { self.inner() + .await .epoch(&group_id, epoch_id) + .await .map_err(|err| err.into_any_error().into()) } @@ -91,17 +108,21 @@ where epoch_updates: Vec, ) -> Result<(), Error> { self.inner() + .await .write( mls_rs_core::group::GroupState { id, data }, epoch_inserts.into_iter().map(Into::into).collect(), epoch_updates.into_iter().map(Into::into).collect(), ) + .await .map_err(|err| err.into_any_error().into()) } async fn max_epoch_id(&self, group_id: Vec) -> Result, Error> { self.inner() + .await .max_epoch_id(&group_id) + .await .map_err(|err| err.into_any_error().into()) } } diff --git a/mls-rs-uniffi/src/lib.rs b/mls-rs-uniffi/src/lib.rs index a7a027a1..7fbfaa91 100644 --- a/mls-rs-uniffi/src/lib.rs +++ b/mls-rs-uniffi/src/lib.rs @@ -262,6 +262,7 @@ impl TryFrom for CipherSuite { /// See [`mls_rs::CipherSuiteProvider::signature_key_generate`] /// for details. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(mls_build_async, maybe_async::must_be_async)] #[uniffi::export] pub async fn generate_signature_keypair( cipher_suite: CipherSuite, @@ -292,6 +293,7 @@ pub struct Client { } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(mls_build_async, maybe_async::must_be_async)] #[uniffi::export] impl Client { /// Create a new client. @@ -495,6 +497,7 @@ pub struct Group { } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(mls_build_async, maybe_async::must_be_async)] impl Group { #[cfg(not(mls_build_async))] fn inner(&self) -> std::sync::MutexGuard<'_, mls_rs::Group> { @@ -520,6 +523,7 @@ fn index_to_identity( /// Extract the basic credential identifier from a from a key package. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(mls_build_async, maybe_async::must_be_async)] async fn signing_identity_to_identifier( signing_identity: &identity::SigningIdentity, ) -> Result, Error> { @@ -531,6 +535,7 @@ async fn signing_identity_to_identifier( } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +#[cfg_attr(mls_build_async, maybe_async::must_be_async)] #[uniffi::export] impl Group { /// Write the current state of the group to storage defined by @@ -545,8 +550,9 @@ impl Group { /// This function is used to provide the current group tree to new /// members when `use_ratchet_tree_extension` is set to false in /// `ClientConfig`. - pub fn export_tree(&self) -> Result { - self.inner().export_tree().try_into() + pub async fn export_tree(&self) -> Result { + let group = self.inner().await; + group.export_tree().try_into() } /// Perform a commit of received proposals (or an empty commit). @@ -703,8 +709,11 @@ impl Group { #[cfg(test)] mod tests { + #[cfg(not(mls_build_async))] use super::*; + #[cfg(not(mls_build_async))] use crate::config::group_state::{EpochRecord, GroupStateStorage}; + #[cfg(not(mls_build_async))] use std::collections::HashMap; #[test] diff --git a/mls-rs-uniffi/tests/custom_storage_sync.py b/mls-rs-uniffi/tests/custom_storage_sync.py index a8bec343..f192a992 100644 --- a/mls-rs-uniffi/tests/custom_storage_sync.py +++ b/mls-rs-uniffi/tests/custom_storage_sync.py @@ -62,7 +62,7 @@ def max_epoch_id(self, group_id: bytes): group_state_storage = PythonGroupStateStorage() -client_config = ClientConfig(group_state_storage, +client_config = ClientConfig(group_state_storage=group_state_storage, use_ratchet_tree_extension=True) key = generate_signature_keypair(CipherSuite.CURVE25519_AES128) diff --git a/mls-rs-uniffi/tests/scenarios.rs b/mls-rs-uniffi/tests/scenarios.rs index 6cceafad..a7989483 100644 --- a/mls-rs-uniffi/tests/scenarios.rs +++ b/mls-rs-uniffi/tests/scenarios.rs @@ -45,9 +45,5 @@ macro_rules! generate_python_tests { generate_python_tests!(generate_signature_keypair, None); generate_python_tests!(client_config_default_sync, None); generate_python_tests!(custom_storage_sync, None); - -// TODO(mulmarta): it'll break if we use async trait which will be -// supported in the next UniFFI release -// TODO(mgeisler): add back simple_scenario_async generate_python_tests!(simple_scenario_sync, None); generate_python_tests!(ratchet_tree_sync, None); diff --git a/mls-rs-uniffi/uniffi-bindgen/Cargo.toml b/mls-rs-uniffi/uniffi-bindgen/Cargo.toml index aa7a923c..a82c083b 100644 --- a/mls-rs-uniffi/uniffi-bindgen/Cargo.toml +++ b/mls-rs-uniffi/uniffi-bindgen/Cargo.toml @@ -7,4 +7,4 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -uniffi = { version = "0.26.0", features = ["cli"] } +uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "6b09f11", version = "0.26.0", features = ["cli"] } From 6d75bbd8bf27ae837881391733b43f0e04df7e47 Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Fri, 22 Mar 2024 20:02:39 +0100 Subject: [PATCH 2/2] [uniffi] Re-introduce `async` Python tests This creates async tests for most of our sync tests. --- .../tests/client_config_default_async.py | 15 +++++++++ .../tests/generate_signature_keypair_async.py | 11 +++++++ ....py => generate_signature_keypair_sync.py} | 0 mls-rs-uniffi/tests/ratchet_tree_async.py | 20 ++++++++++++ mls-rs-uniffi/tests/scenarios.rs | 11 ++++--- mls-rs-uniffi/tests/simple_scenario_async.py | 31 +++++++++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 mls-rs-uniffi/tests/client_config_default_async.py create mode 100644 mls-rs-uniffi/tests/generate_signature_keypair_async.py rename mls-rs-uniffi/tests/{generate_signature_keypair.py => generate_signature_keypair_sync.py} (100%) create mode 100644 mls-rs-uniffi/tests/ratchet_tree_async.py create mode 100644 mls-rs-uniffi/tests/simple_scenario_async.py diff --git a/mls-rs-uniffi/tests/client_config_default_async.py b/mls-rs-uniffi/tests/client_config_default_async.py new file mode 100644 index 00000000..2c0cc8d5 --- /dev/null +++ b/mls-rs-uniffi/tests/client_config_default_async.py @@ -0,0 +1,15 @@ +import asyncio + +from mls_rs_uniffi import Client, CipherSuite, generate_signature_keypair, client_config_default + + +async def scenario(): + client_config = client_config_default() + key = await generate_signature_keypair(CipherSuite.CURVE25519_AES128) + alice = Client(b'alice', key, client_config) + + group = await alice.create_group(None) + await group.write_to_storage() + + +asyncio.run(scenario()) diff --git a/mls-rs-uniffi/tests/generate_signature_keypair_async.py b/mls-rs-uniffi/tests/generate_signature_keypair_async.py new file mode 100644 index 00000000..76a29a35 --- /dev/null +++ b/mls-rs-uniffi/tests/generate_signature_keypair_async.py @@ -0,0 +1,11 @@ +from mls_rs_uniffi import CipherSuite, generate_signature_keypair +import asyncio + + +async def scenario(): + signature_keypair = await generate_signature_keypair( + CipherSuite.CURVE25519_AES128) + assert signature_keypair.cipher_suite == CipherSuite.CURVE25519_AES128 + + +asyncio.run(scenario()) diff --git a/mls-rs-uniffi/tests/generate_signature_keypair.py b/mls-rs-uniffi/tests/generate_signature_keypair_sync.py similarity index 100% rename from mls-rs-uniffi/tests/generate_signature_keypair.py rename to mls-rs-uniffi/tests/generate_signature_keypair_sync.py diff --git a/mls-rs-uniffi/tests/ratchet_tree_async.py b/mls-rs-uniffi/tests/ratchet_tree_async.py new file mode 100644 index 00000000..b707e264 --- /dev/null +++ b/mls-rs-uniffi/tests/ratchet_tree_async.py @@ -0,0 +1,20 @@ +import asyncio + +from mls_rs_uniffi import CipherSuite, generate_signature_keypair, Client, \ + client_config_default + + +async def scenario(): + client_config = client_config_default() + client_config.use_ratchet_tree_extension = False + + key = await generate_signature_keypair(CipherSuite.CURVE25519_AES128) + alice = Client(b'alice', key, client_config) + + group = await alice.create_group(None) + commit = await group.commit() + + assert commit.ratchet_tree is not None + + +asyncio.run(scenario()) diff --git a/mls-rs-uniffi/tests/scenarios.rs b/mls-rs-uniffi/tests/scenarios.rs index a7989483..c1c73d18 100644 --- a/mls-rs-uniffi/tests/scenarios.rs +++ b/mls-rs-uniffi/tests/scenarios.rs @@ -42,8 +42,11 @@ macro_rules! generate_python_tests { }; } -generate_python_tests!(generate_signature_keypair, None); -generate_python_tests!(client_config_default_sync, None); +generate_python_tests!( + generate_signature_keypair_sync, + generate_signature_keypair_async +); +generate_python_tests!(client_config_default_sync, client_config_default_async); generate_python_tests!(custom_storage_sync, None); -generate_python_tests!(simple_scenario_sync, None); -generate_python_tests!(ratchet_tree_sync, None); +generate_python_tests!(simple_scenario_sync, simple_scenario_async); +generate_python_tests!(ratchet_tree_sync, ratchet_tree_async); diff --git a/mls-rs-uniffi/tests/simple_scenario_async.py b/mls-rs-uniffi/tests/simple_scenario_async.py new file mode 100644 index 00000000..fb00c6c2 --- /dev/null +++ b/mls-rs-uniffi/tests/simple_scenario_async.py @@ -0,0 +1,31 @@ +import asyncio + +from mls_rs_uniffi import CipherSuite, generate_signature_keypair, Client, \ + client_config_default + + +async def scenario(): + client_config = client_config_default() + + key = await generate_signature_keypair(CipherSuite.CURVE25519_AES128) + alice = Client(b'alice', key, client_config) + + key = await generate_signature_keypair(CipherSuite.CURVE25519_AES128) + bob = Client(b'bob', key, client_config) + + alice = await alice.create_group(None) + message = await bob.generate_key_package_message() + + commit = await alice.add_members([message]) + await alice.process_incoming_message(commit.commit_message) + bob = (await bob.join_group(None, commit.welcome_messages[0])).group + + msg = await alice.encrypt_application_message(b'hello, bob') + output = await bob.process_incoming_message(msg) + + await alice.write_to_storage() + + assert output.data == b'hello, bob' + + +asyncio.run(scenario())