From 734fdd350eba175e7242b13e1b4fe2bfc3d18410 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 17 Sep 2026 18:25:27 -0600 Subject: [PATCH 1/6] Remaining gaps --- bindgen/src/bindings/cpp/gen_cpp/mod.rs | 33 ++++++-- bindgen/src/bindings/cpp/templates/enum.hpp | 7 ++ bindgen/src/bindings/cpp/templates/err.hpp | 5 ++ .../cpp/templates/flat_enum_methods.cpp | 44 +++++++++++ .../cpp/templates/flat_enum_methods.hpp | 30 ++++++++ bindgen/src/bindings/cpp/templates/macros.cpp | 43 ++++++++++- bindgen/src/bindings/cpp/templates/rec.hpp | 4 + bindgen/src/bindings/cpp/templates/types.cpp | 17 +++++ .../bindings/cpp/templates/value_methods.cpp | 44 +++++++++++ .../bindings/cpp/templates/value_methods.hpp | 42 +++++++++++ cpp-tests/tests/trait_methods/main.cpp | 54 ++++++++++++- cpp-tests/tests/type_flattening/main.cpp | 25 +++++++ fixtures/type-flattening/src/lib.rs | 75 +++++++++++++++++++ 13 files changed, 413 insertions(+), 10 deletions(-) create mode 100644 bindgen/src/bindings/cpp/templates/flat_enum_methods.cpp create mode 100644 bindgen/src/bindings/cpp/templates/flat_enum_methods.hpp create mode 100644 bindgen/src/bindings/cpp/templates/value_methods.cpp create mode 100644 bindgen/src/bindings/cpp/templates/value_methods.hpp diff --git a/bindgen/src/bindings/cpp/gen_cpp/mod.rs b/bindgen/src/bindings/cpp/gen_cpp/mod.rs index 2822985..7a4bc6a 100644 --- a/bindgen/src/bindings/cpp/gen_cpp/mod.rs +++ b/bindgen/src/bindings/cpp/gen_cpp/mod.rs @@ -171,19 +171,36 @@ impl<'a> CppWrapperHeader<'a> { // We take into account only the Record and Enum types, as they are the // only types that can have member variables that reference other structures match type_ { - Type::Record { name, .. } => self - .ci - .get_record_definition(name.as_str()) - .map(|record| (name, record.iter_types())), - Type::Enum { name, .. } => self - .ci - .get_enum_definition(name.as_str()) - .map(|enum_| (name, enum_.iter_types())), + Type::Record { name, .. } => { + self.ci.get_record_definition(name.as_str()).map(|record| { + ( + name, + record + .fields() + .iter() + .flat_map(Field::iter_types) + .collect::>(), + ) + }) + } + Type::Enum { name, .. } => { + self.ci.get_enum_definition(name.as_str()).map(|enum_| { + ( + name, + enum_ + .variants() + .iter() + .flat_map(Variant::iter_types) + .collect::>(), + ) + }) + } _ => None, } }) .flat_map(|(name, types)| { types + .into_iter() .filter_map(type_name) .map(|field_name| DependencyLink { prec: field_name, diff --git a/bindgen/src/bindings/cpp/templates/enum.hpp b/bindgen/src/bindings/cpp/templates/enum.hpp index c52150f..6deb25f 100644 --- a/bindgen/src/bindings/cpp/templates/enum.hpp +++ b/bindgen/src/bindings/cpp/templates/enum.hpp @@ -10,6 +10,9 @@ enum class {{ type_name }}: int32_t { {%- endif %} {%- endfor %} }; +{%- let methods = e.methods() %} +{%- let uniffi_trait_methods = e.uniffi_trait_methods() %} +{% include "flat_enum_methods.hpp" %} {%- else %} namespace uniffi { struct {{ ffi_converter_name }}; @@ -56,6 +59,10 @@ struct {{ type_name }} { return variant; } + {%- let methods = e.methods() %} + {%- let uniffi_trait_methods = e.uniffi_trait_methods() %} + {% include "value_methods.hpp" %} + private: std::variant<{% for variant in e.variants() %}{{ variant|variant_name(config.enum_style) }}{% if !loop.last %}, {% endif %}{% endfor %}> variant; diff --git a/bindgen/src/bindings/cpp/templates/err.hpp b/bindgen/src/bindings/cpp/templates/err.hpp index bcec06f..35528ed 100644 --- a/bindgen/src/bindings/cpp/templates/err.hpp +++ b/bindgen/src/bindings/cpp/templates/err.hpp @@ -18,6 +18,11 @@ struct {{ class_name }}: std::runtime_error { throw *this; } + {%- let methods = e.methods() %} + {%- let uniffi_trait_methods = e.uniffi_trait_methods() %} + {%- let type_name = class_name %} + {% include "value_methods.hpp" %} + protected: virtual int32_t get_variant_idx() const { return 0; diff --git a/bindgen/src/bindings/cpp/templates/flat_enum_methods.cpp b/bindgen/src/bindings/cpp/templates/flat_enum_methods.cpp new file mode 100644 index 0000000..5534b8a --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/flat_enum_methods.cpp @@ -0,0 +1,44 @@ +{%- for method in methods %} +{%- if !method.is_async() %} +{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch -%} +{{ method.name()|fn_name }}({{ type_name }} _uniffi_self{% if !method.arguments().is_empty() %}, {% endif %}{% call macros::param_list(method) %}) { + {%- match method.return_type() %} + {%- when Some with (return_type) %} + return uniffi::{{ return_type|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); + {%- when None %} + {% call macros::rust_call_with_value(method, typ) %}; + {%- endmatch %} +} +{%- endif %} +{%- endfor %} + +{%- if let Some(method) = uniffi_trait_methods.display_fmt %} +std::string to_string({{ type_name }} _uniffi_self) { + return uniffi::{{ Type::String.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.debug_fmt %} +std::string to_debug_string({{ type_name }} _uniffi_self) { + return uniffi::{{ Type::String.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.eq_eq %} +bool eq({{ type_name }} _uniffi_self, {{ type_name }} other) { + return uniffi::{{ Type::Boolean.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.eq_ne %} +bool ne({{ type_name }} _uniffi_self, {{ type_name }} other) { + return uniffi::{{ Type::Boolean.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.hash_hash %} +uint64_t hash({{ type_name }} _uniffi_self) { + return uniffi::{{ Type::UInt64.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.ord_cmp %} +int8_t cmp({{ type_name }} _uniffi_self, {{ type_name }} other) { + return uniffi::{{ Type::Int8.borrow()|lift_fn }}({% call macros::rust_call_with_value(method, typ) %}); +} +{%- endif %} diff --git a/bindgen/src/bindings/cpp/templates/flat_enum_methods.hpp b/bindgen/src/bindings/cpp/templates/flat_enum_methods.hpp new file mode 100644 index 0000000..0b2b0e4 --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/flat_enum_methods.hpp @@ -0,0 +1,30 @@ +{%- for method in methods %} +{%- if !method.is_async() %} +{%- call macros::docstring(method, 0) %} +{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch -%} +{{ method.name()|fn_name }}({{ type_name }} _uniffi_self{% if !method.arguments().is_empty() %}, {% endif %}{% call macros::param_list(method) %}); +{%- endif %} +{%- endfor %} + +{%- if uniffi_trait_methods.display_fmt.is_some() %} +/** Calls the value's Rust `Display` implementation. */ +std::string to_string({{ type_name }} _uniffi_self); +{%- endif %} +{%- if uniffi_trait_methods.debug_fmt.is_some() %} +/** Calls the value's Rust `Debug` implementation. */ +std::string to_debug_string({{ type_name }} _uniffi_self); +{%- endif %} +{%- if uniffi_trait_methods.eq_eq.is_some() %} +/** Calls the value's Rust `Eq` implementation. */ +bool eq({{ type_name }} _uniffi_self, {{ type_name }} other); +/** Calls the value's Rust `Ne` implementation. */ +bool ne({{ type_name }} _uniffi_self, {{ type_name }} other); +{%- endif %} +{%- if uniffi_trait_methods.hash_hash.is_some() %} +/** Calls the value's Rust `Hash` implementation. */ +uint64_t hash({{ type_name }} _uniffi_self); +{%- endif %} +{%- if uniffi_trait_methods.ord_cmp.is_some() %} +/** Calls the value's Rust `Ord` implementation. */ +int8_t cmp({{ type_name }} _uniffi_self, {{ type_name }} other); +{%- endif %} diff --git a/bindgen/src/bindings/cpp/templates/macros.cpp b/bindgen/src/bindings/cpp/templates/macros.cpp index 934ac89..0cf6f7c 100644 --- a/bindgen/src/bindings/cpp/templates/macros.cpp +++ b/bindgen/src/bindings/cpp/templates/macros.cpp @@ -25,6 +25,47 @@ {%- call arg_list_lowered(func) -%}) {%- endmacro %} +{% macro rust_call_with_self(func, self_type) -%} + uniffi::rust_call( + {{ func.ffi_func().name() }}, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift, +{%- else %} + nullptr, +{%- endmatch %} + uniffi::{{ self_type|lower_fn }}(*this) +{%- if !func.arguments().is_empty() %}, {% else %}{% endif %} + {%- call arg_list_lowered(func) -%}) +{%- endmacro %} + +{% macro rust_call_with_value(func, self_type) -%} + uniffi::rust_call( + {{ func.ffi_func().name() }}, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift, +{%- else %} + nullptr, +{%- endmatch %} + uniffi::{{ self_type|lower_fn }}(_uniffi_self) +{%- if !func.arguments().is_empty() %}, {% else %}{% endif %} + {%- call arg_list_lowered(func) -%}) +{%- endmacro %} + +{% macro rust_call_with_self_and_other(func, self_type) -%} + uniffi::rust_call( + {{ func.ffi_func().name() }}, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift, +{%- else %} + nullptr, +{%- endmatch %} + uniffi::{{ self_type|lower_fn }}(*this), + uniffi::{{ self_type|lower_fn }}(other)) +{%- endmacro %} + {% macro param_list(func) %} {%- for arg in func.arguments() -%} {{ arg|parameter(ci) }} @@ -42,7 +83,7 @@ v{{- field_num -}} {% macro arg_list_lowered(func) %} {%- for arg in func.arguments() -%} -uniffi::{{ arg|lower_fn }}({{ arg.name()|var_name }}) +uniffi::{{ arg|lower_fn }}({{ arg.as_type()|cpp_deref(ci) }}{{ arg.name()|var_name }}) {%- if !loop.last -%}, {% endif -%} {% endfor -%} {% endmacro %} diff --git a/bindgen/src/bindings/cpp/templates/rec.hpp b/bindgen/src/bindings/cpp/templates/rec.hpp index e0e2902..973bf45 100644 --- a/bindgen/src/bindings/cpp/templates/rec.hpp +++ b/bindgen/src/bindings/cpp/templates/rec.hpp @@ -8,4 +8,8 @@ struct {{ type_name }} { {%- when Some with (literal) %} = {{ literal|literal_cpp(field, config.enum_style, ci) }};{%- else -%}; {%- endmatch %} {%- endfor %} + + {%- let methods = rec.methods() %} + {%- let uniffi_trait_methods = rec.uniffi_trait_methods() %} + {% include "value_methods.hpp" %} }; diff --git a/bindgen/src/bindings/cpp/templates/types.cpp b/bindgen/src/bindings/cpp/templates/types.cpp index 47ba6e0..a486253 100644 --- a/bindgen/src/bindings/cpp/templates/types.cpp +++ b/bindgen/src/bindings/cpp/templates/types.cpp @@ -10,6 +10,23 @@ {%- match typ %} {%- when Type::Object { module_path, name, imp } %} {% include "obj.cpp" %} +{%- when Type::Record { module_path, name } %} +{%- let rec = ci.get_record_definition(name).unwrap() %} +{%- let methods = rec.methods() %} +{%- let uniffi_trait_methods = rec.uniffi_trait_methods() %} +{% include "value_methods.cpp" %} +{%- when Type::Enum { name, module_path } %} +{%- let e = ci.get_enum_definition(name).unwrap() %} +{%- let methods = e.methods() %} +{%- let uniffi_trait_methods = e.uniffi_trait_methods() %} +{%- if ci.is_name_used_as_error(name) %} +{%- let type_name = typ|canonical_name %} +{% include "value_methods.cpp" %} +{%- else if e.is_flat() %} +{% include "flat_enum_methods.cpp" %} +{%- else %} +{% include "value_methods.cpp" %} +{%- endif %} {%- else %} {%- endmatch %} {% endfor ~%} diff --git a/bindgen/src/bindings/cpp/templates/value_methods.cpp b/bindgen/src/bindings/cpp/templates/value_methods.cpp new file mode 100644 index 0000000..5ef16e7 --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/value_methods.cpp @@ -0,0 +1,44 @@ +{%- for method in methods %} +{%- if !method.is_async() %} +{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch -%} +{{ type_name }}::{{ method.name()|fn_name }}({% call macros::param_list(method) %}) const { + {%- match method.return_type() %} + {%- when Some with (return_type) %} + return uniffi::{{ return_type|lift_fn }}({% call macros::rust_call_with_self(method, typ) %}); + {%- when None %} + {% call macros::rust_call_with_self(method, typ) %}; + {%- endmatch %} +} +{%- endif %} +{%- endfor %} + +{%- if let Some(method) = uniffi_trait_methods.display_fmt %} +std::string {{ type_name }}::to_string() const { + return uniffi::{{ Type::String.borrow()|lift_fn }}({% call macros::rust_call_with_self(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.debug_fmt %} +std::string {{ type_name }}::to_debug_string() const { + return uniffi::{{ Type::String.borrow()|lift_fn }}({% call macros::rust_call_with_self(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.eq_eq %} +bool {{ type_name }}::eq(const {{ type_name }} &other) const { + return uniffi::{{ Type::Boolean.borrow()|lift_fn }}({% call macros::rust_call_with_self_and_other(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.eq_ne %} +bool {{ type_name }}::ne(const {{ type_name }} &other) const { + return uniffi::{{ Type::Boolean.borrow()|lift_fn }}({% call macros::rust_call_with_self_and_other(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.hash_hash %} +uint64_t {{ type_name }}::hash() const { + return uniffi::{{ Type::UInt64.borrow()|lift_fn }}({% call macros::rust_call_with_self(method, typ) %}); +} +{%- endif %} +{%- if let Some(method) = uniffi_trait_methods.ord_cmp %} +int8_t {{ type_name }}::cmp(const {{ type_name }} &other) const { + return uniffi::{{ Type::Int8.borrow()|lift_fn }}({% call macros::rust_call_with_self_and_other(method, typ) %}); +} +{%- endif %} diff --git a/bindgen/src/bindings/cpp/templates/value_methods.hpp b/bindgen/src/bindings/cpp/templates/value_methods.hpp new file mode 100644 index 0000000..038a826 --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/value_methods.hpp @@ -0,0 +1,42 @@ +{%- for method in methods %} +{%- if !method.is_async() %} +{%- call macros::docstring(method, 4) %} +{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch -%} +{{ method.name()|fn_name }}({% call macros::param_list(method) %}) const; +{%- endif %} +{%- endfor %} + +{%- if uniffi_trait_methods.display_fmt.is_some() %} + /** + * Returns a string representation of the value, internally calls Rust's `Display` trait. + */ + std::string to_string() const; +{%- endif %} +{%- if uniffi_trait_methods.debug_fmt.is_some() %} + /** + * Returns a string representation of the value, internally calls Rust's `Debug` trait. + */ + std::string to_debug_string() const; +{%- endif %} +{%- if uniffi_trait_methods.eq_eq.is_some() %} + /** + * Equality check, internally calls Rust's `Eq` trait. + */ + bool eq(const {{ type_name }} &other) const; + /** + * Inequality check, internally calls Rust's `Ne` trait. + */ + bool ne(const {{ type_name }} &other) const; +{%- endif %} +{%- if uniffi_trait_methods.hash_hash.is_some() %} + /** + * Returns a hash of the value, internally calls Rust's `Hash` trait. + */ + uint64_t hash() const; +{%- endif %} +{%- if uniffi_trait_methods.ord_cmp.is_some() %} + /** + * Three-way comparison, internally calls Rust's `Ord` trait. + */ + int8_t cmp(const {{ type_name }} &other) const; +{%- endif %} diff --git a/cpp-tests/tests/trait_methods/main.cpp b/cpp-tests/tests/trait_methods/main.cpp index dc866a6..3c07f9c 100644 --- a/cpp-tests/tests/trait_methods/main.cpp +++ b/cpp-tests/tests/trait_methods/main.cpp @@ -66,9 +66,61 @@ void test_proc_methods() { ASSERT_NE(trait->to_debug_string(), trait2->to_debug_string()); } +void test_record_trait_methods() { + trait_methods::TraitRecord first { "same", 1 }; + trait_methods::TraitRecord second { "same", 2 }; + trait_methods::TraitRecord different { "different", 1 }; + + ASSERT_EQ(first.to_debug_string(), "TraitRecord { s: \"same\", i: 1 }"); + ASSERT_TRUE(first.eq(second)); + ASSERT_FALSE(first.ne(second)); + ASSERT_FALSE(first.eq(different)); + ASSERT_EQ(first.hash(), second.hash()); + ASSERT_EQ(first.cmp(second), 0); +} + +void test_enum_trait_methods() { + trait_methods::TraitEnum first = trait_methods::TraitEnum::kS { "one" }; + trait_methods::TraitEnum second = trait_methods::TraitEnum::kS { "two" }; + trait_methods::TraitEnum integer = trait_methods::TraitEnum::kI { 1 }; + + ASSERT_EQ(first.to_string(), "TraitEnum::S(\"one\")"); + ASSERT_EQ(first.to_debug_string(), "S(\"one\")"); + ASSERT_TRUE(first.eq(second)); + ASSERT_EQ(first.hash(), second.hash()); + ASSERT_TRUE(first.cmp(integer) < 0); + + auto flat = trait_methods::get_flat_trait_enum(0); + ASSERT_EQ(trait_methods::to_string(flat), "FlatTraitEnum::flat-alpha"); + ASSERT_EQ(trait_methods::to_debug_string(flat), "Alpha"); +} + +void test_error_trait_methods() { + try { + trait_methods::throw_api_failure(0); + } catch (const trait_methods::ApiFailure &error) { + ASSERT_EQ(error.to_string(), "api network issue"); + ASSERT_EQ(error.to_debug_string(), "NetworkIssue"); + static_cast(error.hash()); + + try { + trait_methods::throw_api_failure(1); + } catch (const trait_methods::ApiFailure &other) { + ASSERT_FALSE(error.eq(other)); + ASSERT_TRUE(error.ne(other)); + ASSERT_TRUE(error.cmp(other) < 0); + return; + } + } + throw std::runtime_error("Expected ApiFailure"); +} + int main() { test_trait_methods(); test_proc_methods(); + test_record_trait_methods(); + test_enum_trait_methods(); + test_error_trait_methods(); return 0; -} \ No newline at end of file +} diff --git a/cpp-tests/tests/type_flattening/main.cpp b/cpp-tests/tests/type_flattening/main.cpp index 7069da3..6acbc65 100644 --- a/cpp-tests/tests/type_flattening/main.cpp +++ b/cpp-tests/tests/type_flattening/main.cpp @@ -8,10 +8,35 @@ int main() { auto structure = type_flattening::get_struct(value); ASSERT_NE(structure.optional_arc, nullptr); ASSERT_EQ(structure.optional_arc->get_value(), value); + ASSERT_TRUE(structure.has_object()); auto roundtrip = type_flattening::struct_roundtrip(structure); ASSERT_NE(roundtrip.optional_arc, nullptr); ASSERT_EQ(roundtrip.optional_arc->get_value(), value); + ASSERT_EQ(structure.adjusted_value(8), 50); + try { + structure.adjusted_value(-1); + ASSERT_TRUE(false); + } catch (const type_flattening::adjustment_error::NegativeAdjustment& error) { + ASSERT_TRUE(error.is_negative()); + ASSERT_EQ(error.adjustment, -1); + } + + auto next = type_flattening::next(type_flattening::FlatValue::kFirst); + ASSERT_EQ(next, type_flattening::FlatValue::kSecond); + + ASSERT_TRUE(type_flattening::eq(type_flattening::FlatValue::kFirst, + type_flattening::FlatValue::kFirst)); + ASSERT_TRUE(type_flattening::ne(type_flattening::FlatValue::kFirst, + type_flattening::FlatValue::kSecond)); + ASSERT_EQ(type_flattening::hash(type_flattening::FlatValue::kFirst), + type_flattening::hash(type_flattening::FlatValue::kFirst)); + ASSERT_TRUE(type_flattening::cmp(type_flattening::FlatValue::kFirst, + type_flattening::FlatValue::kSecond) < 0); + + type_flattening::RichValue rich = type_flattening::RichValue::kText { "hello" }; + ASSERT_EQ(rich.describe(), "text:hello"); + return 0; } diff --git a/fixtures/type-flattening/src/lib.rs b/fixtures/type-flattening/src/lib.rs index ce66939..3024ec0 100644 --- a/fixtures/type-flattening/src/lib.rs +++ b/fixtures/type-flattening/src/lib.rs @@ -18,6 +18,81 @@ pub struct Structure { pub optional_arc: Option>, } +#[uniffi::export] +impl Structure { + pub fn has_object(&self) -> bool { + self.optional_arc.is_some() + } + + pub fn adjusted_value(&self, adjustment: i32) -> Result { + if adjustment < 0 { + return Err(AdjustmentError::NegativeAdjustment { adjustment }); + } + + Ok(self + .optional_arc + .as_ref() + .map_or(adjustment, |object| object.value + adjustment)) + } +} + +#[derive(uniffi::Enum, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[uniffi::export(Eq, Ord, Hash)] +pub enum FlatValue { + First, + Second, +} + +#[uniffi::export] +impl FlatValue { + pub fn next(&self) -> Self { + match self { + Self::First => Self::Second, + Self::Second => Self::First, + } + } +} + +#[derive(uniffi::Enum)] +pub enum RichValue { + Text(String), + Number(i32), +} + +#[uniffi::export] +impl RichValue { + pub fn describe(&self) -> String { + match self { + Self::Text(value) => format!("text:{value}"), + Self::Number(value) => format!("number:{value}"), + } + } +} + +#[derive(Debug, uniffi::Error)] +pub enum AdjustmentError { + NegativeAdjustment { adjustment: i32 }, +} + +impl std::fmt::Display for AdjustmentError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NegativeAdjustment { adjustment } => { + write!(formatter, "negative adjustment: {adjustment}") + } + } + } +} + +impl std::error::Error for AdjustmentError {} + +#[uniffi::export] +impl AdjustmentError { + pub fn is_negative(&self) -> bool { + matches!(self, Self::NegativeAdjustment { .. }) + } +} + #[uniffi::export] pub fn get_struct(value: i32) -> Structure { Structure { From f13484760334865850c6652b1d68e5c15e378f7e Mon Sep 17 00:00:00 2001 From: Alan George Date: Tue, 22 Sep 2026 12:43:20 -0600 Subject: [PATCH 2/6] Add changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd775a..57da3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +#### v0.10.0+v0.31.2 + +---- +- Core: Update bindgen to UniFFI v0.31.2 +- Core: Objects are passed across the FFI as `uint64_t` handles instead of raw pointers +- Core: Trait interfaces distinguish Rust- and C++-implemented objects when lifting and lowering, and implement the new `uniffi_clone` vtable entry +- Core: Support methods and exported `Display`/`Debug`/`Eq`/`Hash`/`Ord` traits on records, enums and errors (flat enums get free functions) +- Core: Support the `Ord` trait (`cmp`) on objects +- Core: Support tuple-style enum variants with unnamed fields +- Core: Support `#[uniffi(default)]` without a literal using C++ value initialization (`{}`) +- Core: Fix optional enum default values rendering with the wrong type +- Core: Fix duplicate type definitions when a record or enum references the same type more than once +- Core: Async functions, constructors and methods are omitted with a warning instead of aborting generation; async callback interfaces are still unsupported + #### v0.9.0+v0.29.4 ---- From 052e041ebd556d1ff02e2a51b5b7876fc2961335 Mon Sep 17 00:00:00 2001 From: Alan George Date: Mon, 14 Sep 2026 18:06:37 -0600 Subject: [PATCH 3/6] Generate C++17-compatible bindings --- README.md | 2 +- .../src/bindings/cpp/templates/callback.hpp | 12 ++-- .../cpp/templates/cpp_scaffolding.cpp | 28 ++++----- .../src/bindings/cpp/templates/enum_tmpl.cpp | 2 +- .../cpp/templates/rust_buf_stream.cpp | 11 +++- .../cpp/templates/scaffolding/callback.cpp | 20 +++---- .../cpp/templates/scaffolding/object_map.cpp | 5 +- .../src/bindings/cpp/templates/wrapper.hpp | 1 - cpp-tests/CMakeLists.txt | 8 ++- cpp-tests/tests/coverall/main.cpp | 7 ++- cpp-tests/tests/rondpoint/main.cpp | 58 +++++++++---------- docs/SCAFFOLDING.md | 4 +- 12 files changed, 83 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index bc06eef..5409f69 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Regardless of the generation method, these files are produced: # How to integrate bindings To integrate the bindings into your projects, simply add the generated bindings files to your project. -C++20 is required to compile the bindings. +C++17 or newer is required to compile the bindings. # Unsupported features diff --git a/bindgen/src/bindings/cpp/templates/callback.hpp b/bindgen/src/bindings/cpp/templates/callback.hpp index ea000f3..2ecfbc1 100644 --- a/bindgen/src/bindings/cpp/templates/callback.hpp +++ b/bindgen/src/bindings/cpp/templates/callback.hpp @@ -24,12 +24,14 @@ namespace uniffi { static uint64_t uniffi_clone(uint64_t uniffi_handle); static void init(); private: - static inline {{ vtable|ffi_type_name }} vtable = {{ vtable|ffi_type_name}} { - .uniffi_free = reinterpret_cast(&uniffi_free), - .uniffi_clone = reinterpret_cast(&uniffi_clone), + static inline {{ vtable|ffi_type_name }} vtable = [] { + {{ vtable|ffi_type_name }} value{}; + value.uniffi_free = reinterpret_cast(&uniffi_free); + value.uniffi_clone = reinterpret_cast(&uniffi_clone); {%- for (ffi_callback, meth) in vtable_methods.iter() %} - .{{ meth.name()|var_name }} = reinterpret_cast(&{{ meth.name()|var_name }}), + value.{{ meth.name()|var_name }} = reinterpret_cast(&{{ meth.name()|var_name }}); {%- endfor %} - }; + return value; + }(); }; } diff --git a/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp b/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp index 55bd69a..d8591e4 100644 --- a/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp +++ b/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -125,11 +124,10 @@ extern "C" { UNIFFI_EXPORT RustBuffer {{ ci.ffi_rustbuffer_alloc().name() }}(int32_t size, RustCallStatus *out_status) { out_status->code = UNIFFI_CALL_STATUS_OK; - RustBuffer buf = { - .capacity = size, - .len = size, - .data = new uint8_t[size], - }; + RustBuffer buf{}; + buf.capacity = size; + buf.len = size; + buf.data = new uint8_t[size]; return std::move(buf); } @@ -137,11 +135,10 @@ UNIFFI_EXPORT RustBuffer {{ ci.ffi_rustbuffer_alloc().name() }}(int32_t size, Ru UNIFFI_EXPORT RustBuffer {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes bytes, RustCallStatus *out_status) { out_status->code = UNIFFI_CALL_STATUS_OK; - RustBuffer buf = { - .capacity = bytes.len, - .len = bytes.len, - .data = new uint8_t[bytes.len], - }; + RustBuffer buf{}; + buf.capacity = bytes.len; + buf.len = bytes.len; + buf.data = new uint8_t[bytes.len]; std::copy(bytes.data, bytes.data + bytes.len, buf.data); @@ -157,11 +154,10 @@ UNIFFI_EXPORT void {{ ci.ffi_rustbuffer_free().name() }}(RustBuffer buf, RustCal UNIFFI_EXPORT RustBuffer {{ ci.ffi_rustbuffer_reserve().name() }}(RustBuffer buffer, int32_t additional, RustCallStatus *out_status) { out_status->code = UNIFFI_CALL_STATUS_OK; - RustBuffer buf = { - .capacity = buffer.capacity + additional, - .len = buffer.len, - .data = (uint8_t*)std::realloc(buffer.data, buffer.capacity + additional), - }; + RustBuffer buf{}; + buf.capacity = buffer.capacity + additional; + buf.len = buffer.len; + buf.data = (uint8_t*)std::realloc(buffer.data, buffer.capacity + additional); return std::move(buf); } diff --git a/bindgen/src/bindings/cpp/templates/enum_tmpl.cpp b/bindgen/src/bindings/cpp/templates/enum_tmpl.cpp index 602890c..faddc61 100644 --- a/bindgen/src/bindings/cpp/templates/enum_tmpl.cpp +++ b/bindgen/src/bindings/cpp/templates/enum_tmpl.cpp @@ -74,7 +74,7 @@ RustBuffer {{ ffi_converter_name }}::lower(const {{ type_name }} &val) { case {{ loop.index }}: return {{ type_name }}::{{ variant|variant_name(config.enum_style) }} { {%- for field in variant.fields() %} - .{% call macros::field_name(field, loop.index) %} = {{ field|read_fn }}(stream), + {{ field|read_fn }}(stream), {%- endfor %} }; {% endfor %} diff --git a/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp b/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp index 194c5d7..c2c248a 100644 --- a/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp +++ b/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp @@ -23,7 +23,7 @@ struct RustStream: std::basic_iostream { RustStream &operator>>(T &val) { read(reinterpret_cast(&val), sizeof(T)); - if (std::endian::native != std::endian::big) { + if (native_byte_order_is_little_endian()) { auto bytes = reinterpret_cast(&val); std::reverse(bytes, bytes + sizeof(T)); @@ -34,7 +34,7 @@ struct RustStream: std::basic_iostream { template >> RustStream &operator<<(T val) { - if (std::endian::native != std::endian::big) { + if (native_byte_order_is_little_endian()) { auto bytes = reinterpret_cast(&val); std::reverse(bytes, bytes + sizeof(T)); @@ -45,6 +45,11 @@ struct RustStream: std::basic_iostream { return *this; } private: + // Avoid platform-specific endian APIs while retaining C++17 compatibility. + static bool native_byte_order_is_little_endian() { + const uint16_t value = 1; + return *reinterpret_cast(&value) == 1; + } + RustStreamBuffer streambuf; }; - diff --git a/bindgen/src/bindings/cpp/templates/scaffolding/callback.cpp b/bindgen/src/bindings/cpp/templates/scaffolding/callback.cpp index 8543ffd..db4582b 100644 --- a/bindgen/src/bindings/cpp/templates/scaffolding/callback.cpp +++ b/bindgen/src/bindings/cpp/templates/scaffolding/callback.cpp @@ -11,11 +11,10 @@ class {{ iface.name() }}Proxy: public {{ iface.name() }} { ~{{ iface.name() }}Proxy() override { ForeignCallback *callback_stub = reinterpret_cast({{ ffi_converter_name|class_name }}::fn_handle.load()); - RustBuffer out_buf = { - .capacity = 0, - .len = 0, - .data = nullptr, - }; + RustBuffer out_buf{}; + out_buf.capacity = 0; + out_buf.len = 0; + out_buf.data = nullptr; callback_stub(this->handle, 0, nullptr, 0, &out_buf); @@ -33,11 +32,10 @@ class {{ iface.name() }}Proxy: public {{ iface.name() }} { throw std::runtime_error("callback stub not initialized, this indicates a bug in the generated code"); } - RustBuffer out_buf = { - .capacity = 0, - .len = 0, - .data = nullptr, - }; + RustBuffer out_buf{}; + out_buf.capacity = 0; + out_buf.len = 0; + out_buf.data = nullptr; int32_t size = 0; {% for arg in m.arguments() %} @@ -131,5 +129,3 @@ int32_t {{ ffi_converter_name|class_name }}::allocation_size(const {{ type_name } std::atomic {{ ffi_converter_name|class_name }}::fn_handle = 0; - - diff --git a/bindgen/src/bindings/cpp/templates/scaffolding/object_map.cpp b/bindgen/src/bindings/cpp/templates/scaffolding/object_map.cpp index a48ca78..f08205f 100644 --- a/bindgen/src/bindings/cpp/templates/scaffolding/object_map.cpp +++ b/bindgen/src/bindings/cpp/templates/scaffolding/object_map.cpp @@ -11,8 +11,9 @@ template struct ObjectMap { std::lock_guard guard(this->mutex); auto key = (uint64_t)impl.get(); - if (this->map.contains(key)) { - this->map.at(key).ref_count += 1; + auto it = this->map.find(key); + if (it != this->map.end()) { + it->second.ref_count += 1; } else { this->map.insert({ key, {impl, 1} }); } diff --git a/bindgen/src/bindings/cpp/templates/wrapper.hpp b/bindgen/src/bindings/cpp/templates/wrapper.hpp index 91f1738..97842e7 100644 --- a/bindgen/src/bindings/cpp/templates/wrapper.hpp +++ b/bindgen/src/bindings/cpp/templates/wrapper.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include diff --git a/cpp-tests/CMakeLists.txt b/cpp-tests/CMakeLists.txt index 0d946cd..1243070 100644 --- a/cpp-tests/CMakeLists.txt +++ b/cpp-tests/CMakeLists.txt @@ -1,9 +1,15 @@ cmake_minimum_required(VERSION 3.10) project(binding-tests VERSION 1.0.0 LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +if (MSVC) + add_compile_options(/permissive-) +else() + add_compile_options(-pedantic-errors) +endif() + include(CTest) enable_testing() diff --git a/cpp-tests/tests/coverall/main.cpp b/cpp-tests/tests/coverall/main.cpp index b2a05a2..2393cd5 100644 --- a/cpp-tests/tests/coverall/main.cpp +++ b/cpp-tests/tests/coverall/main.cpp @@ -261,7 +261,10 @@ void test_interface_in_dicts() { auto coveralls = coverall::Coveralls::init("test_interface_in_dicts"); coveralls->add_patch(coverall::Patch::init(coverall::Color::kRed)); - coveralls->add_repair(coverall::Repair {.when = std::chrono::system_clock::now(), .patch = coverall::Patch::init(coverall::Color::kGreen)}); + coverall::Repair repair; + repair.when = std::chrono::system_clock::now(); + repair.patch = coverall::Patch::init(coverall::Color::kGreen); + coveralls->add_repair(repair); ASSERT_EQ(2, coveralls->get_repairs().size()); } @@ -315,7 +318,7 @@ void test_dict_with_non_string_keys() { void test_return_only_dict() { auto d = coverall::ReturnOnlyDict{ - .e = std::make_shared() + std::make_shared() }; EXPECT_EXCEPTION(coverall::try_input_return_only_dict(d), std::runtime_error); } diff --git a/cpp-tests/tests/rondpoint/main.cpp b/cpp-tests/tests/rondpoint/main.cpp index 5b8ae87..2fa9e13 100644 --- a/cpp-tests/tests/rondpoint/main.cpp +++ b/cpp-tests/tests/rondpoint/main.cpp @@ -26,10 +26,10 @@ void affirm_enchaine(const T& c, const F& func, Args&& ...arg) { void test_copy() { auto dict = rondpoint::Dictionnaire { - .un = rondpoint::Enumeration::kDeux, - .deux = true, - .petit_nombre = 0, - .gros_nombre = 123456789u + rondpoint::Enumeration::kDeux, + true, + 0, + 123456789u }; auto copied_dict = rondpoint::copie_dictionnaire(dict); ASSERT_EQ(dict.un, copied_dict.un); @@ -44,8 +44,8 @@ void test_copy() { auto map = std::unordered_map { { "zero", rondpoint::EnumerationAvecDonnees::kZero {}}, - { "un", rondpoint::EnumerationAvecDonnees::kUn {.premier = 2}}, - { "deux", rondpoint::EnumerationAvecDonnees::kDeux {.premier= 1, .second = "test"}} + { "un", rondpoint::EnumerationAvecDonnees::kUn {2}}, + { "deux", rondpoint::EnumerationAvecDonnees::kDeux {1, "test"}} }; auto copied_map = rondpoint::copie_carte(map); ASSERT_EQ(map.size(), copied_map.size()); @@ -100,10 +100,10 @@ void test_roundtrip() { for (auto i : {-1, 0, 1}) { auto nombres_signes = rondpoint::DictionnaireNombresSignes { - .petit_nombre = (int8_t)i, - .court_nombre = (int16_t)i, - .nombre_simple = (int32_t)i, - .gros_nombre = (int64_t)i, + (int8_t)i, + (int16_t)i, + (int32_t)i, + (int64_t)i, }; auto ret = rt->identique_nombres_signes(nombres_signes); @@ -115,10 +115,10 @@ void test_roundtrip() { for (auto i : {0, 1}) { auto nombres = rondpoint::DictionnaireNombres { - .petit_nombre = (uint8_t)i, - .court_nombre = (uint16_t)i, - .nombre_simple = (uint32_t)i, - .gros_nombre = (uint64_t)i, + (uint8_t)i, + (uint16_t)i, + (uint32_t)i, + (uint64_t)i, }; auto ret = rt->identique_nombres(nombres); @@ -181,21 +181,21 @@ void test_default_parameter_literals_in_record() { auto rt = rondpoint::Retourneur::init(); auto dict = rondpoint::OptionneurDictionnaire { - .i8_var = -8, - .u8_var = 8, - .i16_var = -16, - .u16_var = 0x10, - .i32_var = -32, - .u32_var = 32, - .i64_var = -64, - .u64_var = 64, - .float_var = 4.0f, - .double_var = 8.0, - .boolean_var = true, - .string_var = "default", - .list_var = std::vector{}, - .enumeration_var = rondpoint::Enumeration::kDeux, - .dictionnaire_var = std::nullopt + -8, + 8, + -16, + 0x10, + -32, + 32, + -64, + 64, + 4.0f, + 8.0, + true, + "default", + std::vector{}, + rondpoint::Enumeration::kDeux, + std::nullopt }; auto copied_dict = rt->identique_optionneur_dictionnaire(dict); ASSERT_EQ(dict.i8_var, copied_dict.i8_var); diff --git a/docs/SCAFFOLDING.md b/docs/SCAFFOLDING.md index 6c79b83..3bd8440 100644 --- a/docs/SCAFFOLDING.md +++ b/docs/SCAFFOLDING.md @@ -34,7 +34,7 @@ This file should be included once in the C++ library, and it will contain all of To ensure that the generated code is able to interface with the target C++ library, the following guidelines should be followed: -- The library should be compiled with C++20 or later. +- The library should be compiled with C++17 or later. - When exposing a struct or class, all of the members mentioned in the UDL file should be public. - For object types, all constructors mentioned in the UDL file should have a matching public constructor in the C++ library. - When exposing callback interfaces, it is recommended to not have any processing-intensive or global data modifying logic in the constructors and destructors of the backing C++ class, as due to the way uniffi internally handles callback interfaces, callback instances may be dynamically constructed multiple times during the runtime of the application. @@ -61,7 +61,7 @@ struct Handle { struct UniffiCustomTypeConverterHandle { static Handle into_custom(uint64_t val) { - Handle handle = Handle { .inner = val } // or some other more intricate logic + Handle handle{val}; // or some other more intricate logic return handle; } From 7d04286c0372bb3806f6184c2498b3619d696cb5 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 16 Sep 2026 10:18:39 -0600 Subject: [PATCH 4/6] Clarify native byte order detection --- bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp | 1 + bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp b/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp index d8591e4..f3f6a41 100644 --- a/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp +++ b/bindgen/src/bindings/cpp/templates/cpp_scaffolding.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp b/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp index c2c248a..4752303 100644 --- a/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp +++ b/bindgen/src/bindings/cpp/templates/rust_buf_stream.cpp @@ -47,8 +47,9 @@ struct RustStream: std::basic_iostream { private: // Avoid platform-specific endian APIs while retaining C++17 compatibility. static bool native_byte_order_is_little_endian() { - const uint16_t value = 1; - return *reinterpret_cast(&value) == 1; + const std::uint16_t value = 1; + const auto *bytes = reinterpret_cast(&value); + return bytes[0] == 1; } RustStreamBuffer streambuf; From 9d86abe0dd91f65ec7341d068606597146a8b494 Mon Sep 17 00:00:00 2001 From: Alan George Date: Mon, 14 Sep 2026 21:38:50 -0600 Subject: [PATCH 5/6] Add Rust-to-C++ async function support --- Cargo.lock | 59 ++++ README.md | 9 +- .../src/bindings/cpp/gen_cpp/filters/mod.rs | 1 + bindgen/src/bindings/cpp/gen_cpp/mod.rs | 6 +- bindgen/src/bindings/cpp/mod.rs | 7 - bindgen/src/bindings/cpp/templates/async.cpp | 281 ++++++++++++++++++ bindgen/src/bindings/cpp/templates/async.hpp | 62 ++++ bindgen/src/bindings/cpp/templates/fn_def.cpp | 12 +- bindgen/src/bindings/cpp/templates/macros.cpp | 86 ++++++ bindgen/src/bindings/cpp/templates/obj.cpp | 28 +- bindgen/src/bindings/cpp/templates/obj.hpp | 13 +- .../bindings/cpp/templates/scaffolding.hpp | 2 + .../src/bindings/cpp/templates/wrapper.cpp | 7 +- .../src/bindings/cpp/templates/wrapper.hpp | 19 +- cpp-tests/CMakeLists.txt | 1 + cpp-tests/tests/futures/main.cpp | 102 +++++++ fixtures/Cargo.toml | 1 + fixtures/async-calls/Cargo.toml | 20 ++ fixtures/async-calls/README.md | 12 + fixtures/async-calls/build.rs | 9 + fixtures/async-calls/src/async-calls.udl | 15 + fixtures/async-calls/src/lib.rs | 205 +++++++++++++ .../async-calls/src/platform_specific/mod.rs | 8 + .../src/platform_specific/native.rs | 29 ++ fixtures/src/lib.rs | 1 + 25 files changed, 954 insertions(+), 41 deletions(-) create mode 100644 bindgen/src/bindings/cpp/templates/async.cpp create mode 100644 bindgen/src/bindings/cpp/templates/async.hpp create mode 100644 cpp-tests/tests/futures/main.cpp create mode 100644 fixtures/async-calls/Cargo.toml create mode 100644 fixtures/async-calls/README.md create mode 100644 fixtures/async-calls/build.rs create mode 100644 fixtures/async-calls/src/async-calls.udl create mode 100644 fixtures/async-calls/src/lib.rs create mode 100644 fixtures/async-calls/src/platform_specific/mod.rs create mode 100644 fixtures/async-calls/src/platform_specific/native.rs diff --git a/Cargo.lock b/Cargo.lock index d7cc9de..bca593c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,19 @@ dependencies = [ "winnow 0.7.13", ] +[[package]] +name = "async-compat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c97d7ff3c25d6c10d64170c12acaf5d4245e76dece3779c1d92b153a64f11df" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -300,6 +313,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "glob" version = "0.3.1" @@ -430,6 +461,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "plain" version = "0.2.3" @@ -673,6 +710,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -765,6 +811,17 @@ dependencies = [ "uniffi_pipeline", ] +[[package]] +name = "uniffi-async-calls" +version = "0.21.0" +dependencies = [ + "futures-timer", + "once_cell", + "thiserror 1.0.50", + "tokio", + "uniffi", +] + [[package]] name = "uniffi-bindgen-cpp" version = "0.10.0+v0.31.2" @@ -787,6 +844,7 @@ dependencies = [ name = "uniffi-bindgen-cpp-fixtures" version = "1.0.0" dependencies = [ + "uniffi-async-calls", "uniffi-custom-types-builtin", "uniffi-empty-type", "uniffi-enum-style-test", @@ -1033,6 +1091,7 @@ version = "0.31.2" source = "git+https://github.com/mozilla/uniffi-rs.git?tag=v0.31.2#05b45b110f652d58d94c653992aeb3adc3a094f4" dependencies = [ "anyhow", + "async-compat", "bytes", "once_cell", "static_assertions", diff --git a/README.md b/README.md index 5409f69..c4446ea 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,18 @@ Regardless of the generation method, these files are produced: To integrate the bindings into your projects, simply add the generated bindings files to your project. C++17 or newer is required to compile the bindings. +# Async functions + +Rust async functions, constructors, and object methods return a move-only `uniffi::Future`. +The future supports `get()`, `wait()`, `wait_for()`, and `cancel()`. Destroying an incomplete future +also requests cancellation. Each in-flight Rust future uses a dedicated worker thread to serialize +its UniFFI poll, cancel, complete, and free operations. + # Unsupported features The following uniffi features are unsupported. -* Async functions +* Async callback-interface methods # Configuration options diff --git a/bindgen/src/bindings/cpp/gen_cpp/filters/mod.rs b/bindgen/src/bindings/cpp/gen_cpp/filters/mod.rs index 1aa195a..c795cbd 100644 --- a/bindgen/src/bindings/cpp/gen_cpp/filters/mod.rs +++ b/bindgen/src/bindings/cpp/gen_cpp/filters/mod.rs @@ -350,6 +350,7 @@ pub(crate) fn ffi_type_name(ffi_type: &FfiType) -> Result { FfiType::VoidPointer => "void *".into(), FfiType::RustBuffer(_) => "RustBuffer".into(), FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::Callback(name) if name == "RustFutureContinuationCallback" => name.clone(), FfiType::Callback(_) => "void *".into(), FfiType::Struct(name) => ffi_struct_name(name)?, FfiType::RustCallStatus => "RustCallStatus*".into(), diff --git a/bindgen/src/bindings/cpp/gen_cpp/mod.rs b/bindgen/src/bindings/cpp/gen_cpp/mod.rs index 7a4bc6a..66aeb0f 100644 --- a/bindgen/src/bindings/cpp/gen_cpp/mod.rs +++ b/bindgen/src/bindings/cpp/gen_cpp/mod.rs @@ -118,11 +118,7 @@ impl<'a> ScaffoldingHeader<'a> { .flat_map(|o| o.vtable_definition()), ) .map(Into::into) - .chain( - self.ci - .iter_ffi_function_definitions_non_async() - .map(Into::into), - ) + .chain(self.ci.iter_ffi_function_definitions().map(Into::into)) } } diff --git a/bindgen/src/bindings/cpp/mod.rs b/bindgen/src/bindings/cpp/mod.rs index 7ccd27d..ec0ffad 100644 --- a/bindgen/src/bindings/cpp/mod.rs +++ b/bindgen/src/bindings/cpp/mod.rs @@ -100,13 +100,6 @@ impl BindingGenerator for CppBindingGenerator { ); } - if ci.has_async_fns() { - eprintln!( - "Warning: omitting unsupported async functions from namespace: {}", - ci.namespace() - ); - } - if self.scaffolding_mode { unimplemented!("Cpp scaffolding is not supported yet!"); } else { diff --git a/bindgen/src/bindings/cpp/templates/async.cpp b/bindgen/src/bindings/cpp/templates/async.cpp new file mode 100644 index 0000000..99108fb --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/async.cpp @@ -0,0 +1,281 @@ +constexpr int8_t UNIFFI_RUST_FUTURE_POLL_READY = 0; +constexpr int8_t UNIFFI_RUST_FUTURE_POLL_WAKE = 1; + +template +class RustFutureState: public std::enable_shared_from_this> { +public: + RustFutureState( + uint64_t handle, + Poll poll, + Cancel cancel, + Complete complete, + Free free, + Lift lift, + ErrorHandler error_handler + ): + handle_(handle), + poll_(std::move(poll)), + cancel_(std::move(cancel)), + complete_(std::move(complete)), + free_(std::move(free)), + lift_(std::move(lift)), + error_handler_(std::move(error_handler)) {} + + std::future get_future() { + return promise_.get_future(); + } + + void start() noexcept { + try { + auto state = this->shared_from_this(); + std::thread([state = std::move(state)]() { + state->run(); + }).detach(); + } catch (...) { + auto error = std::current_exception(); + free_(handle_); + { + std::lock_guard lock(mutex_); + finished_ = true; + } + try { + promise_.set_exception(std::move(error)); + } catch (...) { + } + } + } + + void cancel() noexcept { + { + std::lock_guard lock(mutex_); + if (finished_) { + return; + } + cancel_requested_ = true; + } + wake_.notify_one(); + } + +private: + enum class Work { + PollFuture, + CancelFuture, + CompleteFuture, + }; + + using State = RustFutureState; + + static void continuation(uint64_t callback_data, int8_t poll_result) noexcept { + auto callback_state = std::unique_ptr>( + reinterpret_cast *>(callback_data) + ); + (*callback_state)->resume(poll_result); + } + + void resume(int8_t poll_result) noexcept { + { + std::lock_guard lock(mutex_); + if (finished_) { + return; + } + + if (poll_result == UNIFFI_RUST_FUTURE_POLL_READY) { + ready_ = true; + } else if (poll_result == UNIFFI_RUST_FUTURE_POLL_WAKE) { + poll_requested_ = true; + } else { + failure_ = std::make_exception_ptr( + std::runtime_error("Unexpected UniFFI Rust future poll result") + ); + cancel_requested_ = true; + } + } + wake_.notify_one(); + } + + void run() noexcept { + for (;;) { + Work work; + { + std::unique_lock lock(mutex_); + wake_.wait(lock, [this]() { + return finished_ || ready_ || poll_requested_ || + (cancel_requested_ && !cancel_sent_); + }); + + if (finished_) { + return; + } + + if (cancel_requested_ && !cancel_sent_) { + cancel_sent_ = true; + work = Work::CancelFuture; + } else if (ready_) { + ready_ = false; + work = Work::CompleteFuture; + } else { + poll_requested_ = false; + work = Work::PollFuture; + } + } + + if (work == Work::PollFuture) { + poll(); + } else if (work == Work::CancelFuture) { + cancel_rust_future(); + } else { + complete(); + return; + } + } + } + + void poll() noexcept { + std::shared_ptr *callback_state = nullptr; + try { + callback_state = new std::shared_ptr(this->shared_from_this()); + poll_(handle_, &RustFutureState::continuation, reinterpret_cast(callback_state)); + } catch (...) { + delete callback_state; + fail(std::current_exception()); + } + } + + void cancel_rust_future() noexcept { + try { + cancel_(handle_); + } catch (...) { + fail(std::current_exception()); + } + + // Cancellation makes a Rust future ready even if it had not yet been polled. + { + std::lock_guard lock(mutex_); + ready_ = true; + } + wake_.notify_one(); + } + + void fail(std::exception_ptr error) noexcept { + { + std::lock_guard lock(mutex_); + if (!failure_) { + failure_ = std::move(error); + } + cancel_requested_ = true; + } + wake_.notify_one(); + } + + void complete() noexcept { + bool cancelled; + std::exception_ptr failure; + { + std::lock_guard lock(mutex_); + cancelled = cancel_requested_; + failure = failure_; + } + + try { + RustCallStatus status = { 0 }; + if constexpr (std::is_void_v) { + complete_(handle_, &status); + if (failure || cancelled) { + try { + check_rust_call(status, error_handler_); + } catch (...) { + } + set_terminal_exception(failure, cancelled); + } else { + check_rust_call(status, error_handler_); + promise_.set_value(); + } + } else { + auto value = complete_(handle_, &status); + if (failure || cancelled) { + try { + check_rust_call(status, error_handler_); + static_cast(lift_(value)); + } catch (...) { + } + set_terminal_exception(failure, cancelled); + } else { + check_rust_call(status, error_handler_); + promise_.set_value(lift_(value)); + } + } + } catch (...) { + try { + promise_.set_exception(std::current_exception()); + } catch (...) { + } + } + + free_(handle_); + { + std::lock_guard lock(mutex_); + finished_ = true; + } + } + + void set_terminal_exception(const std::exception_ptr &failure, bool cancelled) noexcept { + try { + if (failure) { + promise_.set_exception(failure); + } else if (cancelled) { + promise_.set_exception(std::make_exception_ptr(::uniffi::AsyncCancelledError())); + } + } catch (...) { + } + } + + uint64_t handle_; + Poll poll_; + Cancel cancel_; + Complete complete_; + Free free_; + Lift lift_; + ErrorHandler error_handler_; + std::promise promise_; + std::mutex mutex_; + std::condition_variable wake_; + std::exception_ptr failure_; + bool poll_requested_ = true; + bool cancel_requested_ = false; + bool cancel_sent_ = false; + bool ready_ = false; + bool finished_ = false; +}; + +template +Future rust_call_async( + RustFuture rust_future, + Poll poll, + Cancel cancel, + Complete complete, + Free free, + Lift lift, + ErrorHandler error_handler +) { + initialize(); + const auto handle = rust_future(); + using State = RustFutureState; + + auto state = std::make_shared( + handle, + std::move(poll), + std::move(cancel), + std::move(complete), + std::move(free), + std::move(lift), + std::move(error_handler) + ); + auto future = state->get_future(); + state->start(); + + return Future(std::move(future), [weak_state = std::weak_ptr(state)]() { + if (auto state = weak_state.lock()) { + state->cancel(); + } + }); +} diff --git a/bindgen/src/bindings/cpp/templates/async.hpp b/bindgen/src/bindings/cpp/templates/async.hpp new file mode 100644 index 0000000..13d9a02 --- /dev/null +++ b/bindgen/src/bindings/cpp/templates/async.hpp @@ -0,0 +1,62 @@ +class AsyncCancelledError: public std::runtime_error { +public: + AsyncCancelledError(): std::runtime_error("UniFFI async call cancelled") {} +}; + +template +class Future { +public: + Future(std::future future, std::function cancel): + future_(std::move(future)), cancel_(std::move(cancel)) {} + + Future(const Future &) = delete; + Future &operator=(const Future &) = delete; + + Future(Future &&other) noexcept: + future_(std::move(other.future_)), cancel_(std::move(other.cancel_)) { + other.cancel_ = nullptr; + } + + Future &operator=(Future &&other) noexcept { + if (this != &other) { + cancel(); + future_ = std::move(other.future_); + cancel_ = std::move(other.cancel_); + other.cancel_ = nullptr; + } + return *this; + } + + ~Future() { + cancel(); + } + + bool valid() const noexcept { + return future_.valid(); + } + + decltype(auto) get() { + return future_.get(); + } + + void wait() const { + future_.wait(); + } + + template + std::future_status wait_for(const std::chrono::duration &timeout) const { + return future_.wait_for(timeout); + } + + void cancel() noexcept { + if (cancel_) { + auto cancel_callback = std::move(cancel_); + cancel_ = nullptr; + cancel_callback(); + } + } + +private: + std::future future_; + std::function cancel_; +}; diff --git a/bindgen/src/bindings/cpp/templates/fn_def.cpp b/bindgen/src/bindings/cpp/templates/fn_def.cpp index 4f9cb1d..ba3a51c 100644 --- a/bindgen/src/bindings/cpp/templates/fn_def.cpp +++ b/bindgen/src/bindings/cpp/templates/fn_def.cpp @@ -1,12 +1,20 @@ {%- match func.return_type() %} {%- when Some with (return_type) %} -{{ return_type|type_name(ci) }} {{ func.name()|fn_name }}({% call macros::param_list(func) %}) { +{% if func.is_async() %}uniffi::Future<{% endif %}{{ return_type|type_name(ci) }}{% if func.is_async() %}>{% endif %} {{ func.name()|fn_name }}({% call macros::param_list(func) %}) { + {%- if func.is_async() %} + return {% call macros::rust_call_async(func, return_type|type_name(ci)) %}; + {%- else %} auto ret = {% call macros::rust_call(func) %}; return uniffi::{{ return_type|lift_fn }}(ret); + {%- endif %} } {%- when None -%} -void {{ func.name()|fn_name }}({% call macros::param_list(func) %}) { +{% if func.is_async() %}uniffi::Future<{% endif %}void{% if func.is_async() %}>{% endif %} {{ func.name()|fn_name }}({% call macros::param_list(func) %}) { + {%- if func.is_async() %} + return {% call macros::rust_call_async_void(func) %}; + {%- else %} {% call macros::rust_call(func) %}; + {%- endif %} } {%- endmatch -%} diff --git a/bindgen/src/bindings/cpp/templates/macros.cpp b/bindgen/src/bindings/cpp/templates/macros.cpp index 0cf6f7c..3bd6fbf 100644 --- a/bindgen/src/bindings/cpp/templates/macros.cpp +++ b/bindgen/src/bindings/cpp/templates/macros.cpp @@ -66,6 +66,92 @@ uniffi::{{ self_type|lower_fn }}(other)) {%- endmacro %} +{% macro rust_call_async(func, return_type) -%} + uniffi::rust_call_async<{{ return_type }}>( + [&]() { + return {{ func.ffi_func().name() }}( + {%- call arg_list_lowered(func) -%} + ); + }, + {{ func.ffi_rust_future_poll(ci) }}, + {{ func.ffi_rust_future_cancel(ci) }}, + {{ func.ffi_rust_future_complete(ci) }}, + {{ func.ffi_rust_future_free(ci) }}, + [](auto value) { return uniffi::{{ func.return_type().unwrap()|lift_fn }}(value); }, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift +{%- else %} + nullptr +{%- endmatch %} + ) +{%- endmacro %} + +{% macro rust_call_async_void(func) -%} + uniffi::rust_call_async( + [&]() { + return {{ func.ffi_func().name() }}( + {%- call arg_list_lowered(func) -%} + ); + }, + {{ func.ffi_rust_future_poll(ci) }}, + {{ func.ffi_rust_future_cancel(ci) }}, + {{ func.ffi_rust_future_complete(ci) }}, + {{ func.ffi_rust_future_free(ci) }}, + nullptr, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift +{%- else %} + nullptr +{%- endmatch %} + ) +{%- endmacro %} + +{% macro rust_call_async_with_prefix(prefix, func, return_type) -%} + uniffi::rust_call_async<{{ return_type }}>( + [&]() { + return {{ func.ffi_func().name() }}( + {{ prefix }}{% if !func.arguments().is_empty() %}, {% endif %} + {%- call arg_list_lowered(func) -%} + ); + }, + {{ func.ffi_rust_future_poll(ci) }}, + {{ func.ffi_rust_future_cancel(ci) }}, + {{ func.ffi_rust_future_complete(ci) }}, + {{ func.ffi_rust_future_free(ci) }}, + [](auto value) { return uniffi::{{ func.return_type().unwrap()|lift_fn }}(value); }, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift +{%- else %} + nullptr +{%- endmatch %} + ) +{%- endmacro %} + +{% macro rust_call_async_void_with_prefix(prefix, func) -%} + uniffi::rust_call_async( + [&]() { + return {{ func.ffi_func().name() }}( + {{ prefix }}{% if !func.arguments().is_empty() %}, {% endif %} + {%- call arg_list_lowered(func) -%} + ); + }, + {{ func.ffi_rust_future_poll(ci) }}, + {{ func.ffi_rust_future_cancel(ci) }}, + {{ func.ffi_rust_future_complete(ci) }}, + {{ func.ffi_rust_future_free(ci) }}, + nullptr, +{%- match func.throws_type() %} +{% when Some with (e) %} + uniffi::{{ e|ffi_error_converter_name }}::lift +{%- else %} + nullptr +{%- endmatch %} + ) +{%- endmacro %} + {% macro param_list(func) %} {%- for arg in func.arguments() -%} {{ arg|parameter(ci) }} diff --git a/bindgen/src/bindings/cpp/templates/obj.cpp b/bindgen/src/bindings/cpp/templates/obj.cpp index a818598..053ff3e 100644 --- a/bindgen/src/bindings/cpp/templates/obj.cpp +++ b/bindgen/src/bindings/cpp/templates/obj.cpp @@ -30,37 +30,47 @@ namespace uniffi { {% match obj.primary_constructor() -%} {%- when Some with (ctor) %} -{%- if !ctor.is_async() %} -{{ type_name }} {{ impl_class_name }}::init({% call macros::param_list(ctor) %}) { +{% if ctor.is_async() %}uniffi::Future<{% endif %}{{ type_name }}{% if ctor.is_async() %}>{% endif %} {{ impl_class_name }}::init({% call macros::param_list(ctor) %}) { + {%- if ctor.is_async() %} + return {% call macros::rust_call_async(ctor, type_name) %}; + {%- else %} return {{ type_name }}( new {{ impl_class_name }}({%- call macros::rust_call(ctor) -%}) ); + {%- endif %} } -{%- endif %} {% else -%} {% endmatch -%} {% for ctor in obj.alternate_constructors() %} -{%- if !ctor.is_async() %} -{{ type_name }} {{ impl_class_name }}::{{ ctor.name() }}({% call macros::param_list(ctor) %}) { +{% if ctor.is_async() %}uniffi::Future<{% endif %}{{ type_name }}{% if ctor.is_async() %}>{% endif %} {{ impl_class_name }}::{{ ctor.name() }}({% call macros::param_list(ctor) %}) { + {%- if ctor.is_async() %} + return {% call macros::rust_call_async(ctor, type_name) %}; + {%- else %} return {{ type_name }}(new {{ impl_class_name }}({% call macros::rust_call(ctor) %})); + {%- endif %} } -{%- endif %} {% endfor %} {%- for method in obj.methods() %} -{%- if !method.is_async() %} -{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch -%} +{% if method.is_async() %}uniffi::Future<{% endif %}{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }}{% else %}void{% endmatch %}{% if method.is_async() %}>{% endif %} {{ impl_class_name }}::{{ method.name()|fn_name }}({% call macros::param_list(method) %}) { auto ptr = this->_uniffi_internal_clone_pointer(); {%- match method.return_type() %} {% when Some with (return_type) %} + {%- if method.is_async() %} + return {% call macros::rust_call_async_with_prefix("ptr", method, return_type|type_name(ci)) %}; + {%- else %} return uniffi::{{ return_type|lift_fn }}({% call macros::rust_call_with_prefix("ptr", method) %}); + {%- endif %} + {%- else %} + {%- if method.is_async() %} + return {% call macros::rust_call_async_void_with_prefix("ptr", method) %}; {%- else %} {% call macros::rust_call_with_prefix("ptr", method) -%}; + {%- endif %} {%- endmatch %} } -{%- endif %} {%- endfor %} {{ impl_class_name }}::~{{ impl_class_name }}() { diff --git a/bindgen/src/bindings/cpp/templates/obj.hpp b/bindgen/src/bindings/cpp/templates/obj.hpp index c0398ac..c047977 100644 --- a/bindgen/src/bindings/cpp/templates/obj.hpp +++ b/bindgen/src/bindings/cpp/templates/obj.hpp @@ -38,26 +38,19 @@ struct {{ impl_class_name }} {%- match obj.primary_constructor() %} {%- when Some with (ctor) %} - {%- if !ctor.is_async() %} {%- call macros::docstring(ctor, 4) %} - static {{ type_name }} init({% call macros::param_list(ctor) %}); - {%- endif %} + static {% if ctor.is_async() %}::uniffi::Future<{% endif %}{{ type_name }}{% if ctor.is_async() %}>{% endif %} init({% call macros::param_list(ctor) %}); {%- else %} {%- endmatch %} {%- for ctor in obj.alternate_constructors() %} - {%- if !ctor.is_async() %} {%- call macros::docstring(ctor, 4) %} - static {{ type_name }} {{ ctor.name() }}({% call macros::param_list(ctor) %}); - {%- endif %} + static {% if ctor.is_async() %}::uniffi::Future<{% endif %}{{ type_name }}{% if ctor.is_async() %}>{% endif %} {{ ctor.name() }}({% call macros::param_list(ctor) %}); {%- endfor %} {%- for method in obj.methods() %} - {%- if !method.is_async() %} {%- call macros::docstring(method, 4) %} - {% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }} {% else %}void {% endmatch %} - {{- method.name()|fn_name }}({% call macros::param_list(method) %}); - {%- endif %} + {% if method.is_async() %}::uniffi::Future<{% endif %}{% match method.return_type() %}{% when Some with (return_type) %}{{ return_type|type_name(ci) }}{% else %}void{% endmatch %}{% if method.is_async() %}>{% endif %} {{ method.name()|fn_name }}({% call macros::param_list(method) %}); {%- endfor %} {%- for method in obj.uniffi_traits() %} diff --git a/bindgen/src/bindings/cpp/templates/scaffolding.hpp b/bindgen/src/bindings/cpp/templates/scaffolding.hpp index 216436d..d33b54c 100644 --- a/bindgen/src/bindings/cpp/templates/scaffolding.hpp +++ b/bindgen/src/bindings/cpp/templates/scaffolding.hpp @@ -26,6 +26,8 @@ struct RustCallStatus { RustBuffer error_buf; }; +typedef void (*RustFutureContinuationCallback)(uint64_t callback_data, int8_t poll_result); + #endif {%- for def in self.scaffolding_definitions() %} diff --git a/bindgen/src/bindings/cpp/templates/wrapper.cpp b/bindgen/src/bindings/cpp/templates/wrapper.cpp index 2473e82..1bf89e3 100644 --- a/bindgen/src/bindings/cpp/templates/wrapper.cpp +++ b/bindgen/src/bindings/cpp/templates/wrapper.cpp @@ -55,6 +55,9 @@ void check_rust_call(const RustCallStatus &status, F error_cb) { } throw std::runtime_error("A Rust panic has occurred"); + + case 3: + throw ::uniffi::AsyncCancelledError(); } throw std::runtime_error("Unexpected Rust call status"); @@ -77,6 +80,8 @@ R rust_call(F f, EF error_cb, Args... args) { } } +{% include "async.cpp" %} + template void rust_call_trait_interface(RustCallStatus* status, F make_call, W write_value) { initialize(); @@ -127,8 +132,6 @@ void rust_call_trait_interface_with_error(RustCallStatus* status, F make_call, W {{ type_helper_code }} {%- for func in ci.function_definitions() %} -{%- if !func.is_async() %} {% include "fn_def.cpp" %} -{%- endif %} {% endfor -%} } // namespace {{ namespace }} diff --git a/bindgen/src/bindings/cpp/templates/wrapper.hpp b/bindgen/src/bindings/cpp/templates/wrapper.hpp index 97842e7..dfef103 100644 --- a/bindgen/src/bindings/cpp/templates/wrapper.hpp +++ b/bindgen/src/bindings/cpp/templates/wrapper.hpp @@ -2,9 +2,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +38,13 @@ namespace uniffi { } #endif +#ifndef UNIFFI_CPP_ASYNC_FUTURE +#define UNIFFI_CPP_ASYNC_FUTURE +namespace uniffi { +{% include "async.hpp" %} +} +#endif + {%- import "macros.cpp" as macros %} {% call macros::docstring_value(ci.namespace_docstring(), 0) %} namespace {{ namespace }} { @@ -99,6 +109,7 @@ typedef {{ type_name }} {{ name }}; namespace uniffi { using ::uniffi::RustStream; using ::uniffi::RustStreamBuffer; +using ::uniffi::Future; {%- for converter in self.external_ffi_converters() %} using {{ converter }}; {%- endfor %} @@ -169,14 +180,12 @@ void rustbuffer_free(RustBuffer); } // namespace uniffi {%~ for func in ci.function_definitions() %} -{%- if !func.is_async() %} {%- call macros::docstring(func, 0) %} -{%- match func.return_type() %} +{% if func.is_async() %}uniffi::Future<{% endif %}{% match func.return_type() %} {%- when Some with (return_type) %} -{{ return_type|type_name(ci) }} {{ func.name()|fn_name }}({% call macros::param_list(func) %}); +{{ return_type|type_name(ci) }}{% if func.is_async() %}>{% endif %} {{ func.name()|fn_name }}({% call macros::param_list(func) %}); {%- when None %} -void {{ func.name()|fn_name }}({% call macros::param_list(func) %}); +void{% if func.is_async() %}>{% endif %} {{ func.name()|fn_name }}({% call macros::param_list(func) %}); {%- endmatch %} -{%- endif %} {%- endfor %} } // namespace {{ namespace }} diff --git a/cpp-tests/CMakeLists.txt b/cpp-tests/CMakeLists.txt index 1243070..7d39923 100644 --- a/cpp-tests/CMakeLists.txt +++ b/cpp-tests/CMakeLists.txt @@ -79,6 +79,7 @@ test_case(empty_type) test_case(error_types_builtin) test_case(reserved_field_name) test_case(type_flattening) +test_case(futures) add_executable(ext_types_import-test tests/ext_types_import/main.cpp diff --git a/cpp-tests/tests/futures/main.cpp b/cpp-tests/tests/futures/main.cpp new file mode 100644 index 0000000..e69a355 --- /dev/null +++ b/cpp-tests/tests/futures/main.cpp @@ -0,0 +1,102 @@ +#include + +#include + +#include +#include + +using namespace std::chrono_literals; + +int main() { + ASSERT_EQ("Hello, C++", futures::greet("C++")); + ASSERT_TRUE(futures::always_ready().get()); + futures::void_return().get(); + ASSERT_EQ("Hello, Future!", futures::say().get()); + ASSERT_EQ("Hello, Future!", futures::say_after(1, "Future").get()); + ASSERT_TRUE(futures::sleep(1).get()); + futures::sleep_no_return(1).get(); + + auto waitable = futures::say_after(20, "Wait"); + ASSERT_EQ(std::future_status::timeout, waitable.wait_for(1ms)); + waitable.wait(); + ASSERT_EQ("Hello, Wait!", waitable.get()); + + { + auto pending = futures::pending(); + for (auto i = 0; i < 100 && futures::pending_count() == 0; ++i) { + std::this_thread::sleep_for(1ms); + } + ASSERT_EQ(1, futures::pending_count()); + pending.cancel(); + EXPECT_EXCEPTION(pending.get(), uniffi::AsyncCancelledError); + for (auto i = 0; i < 100 && futures::pending_count() != 0; ++i) { + std::this_thread::sleep_for(1ms); + } + ASSERT_EQ(0, futures::pending_count()); + } + + { + auto pending = futures::pending(); + for (auto i = 0; i < 100 && futures::pending_count() == 0; ++i) { + std::this_thread::sleep_for(1ms); + } + ASSERT_EQ(1, futures::pending_count()); + } + for (auto i = 0; i < 100 && futures::pending_count() != 0; ++i) { + std::this_thread::sleep_for(1ms); + } + ASSERT_EQ(0, futures::pending_count()); + + for (auto i = 0; i < 100; ++i) { + auto racing = futures::say_after(1, "Race"); + if (i % 2 == 0) { + std::this_thread::sleep_for(1ms); + } + racing.cancel(); + try { + ASSERT_EQ("Hello, Race!", racing.get()); + } catch (const uniffi::AsyncCancelledError &) { + } + } + + ASSERT_EQ(42, futures::fallible_me(false).get()); + EXPECT_EXCEPTION(futures::fallible_me(true).get(), futures::my_error::Foo); + + auto megaphone = futures::new_megaphone(); + ASSERT_EQ("HELLO, WORLD!", megaphone->say_after(1, "World").get()); + ASSERT_EQ("", megaphone->silence().get()); + ASSERT_EQ(42, megaphone->fallible_me(false).get()); + EXPECT_EXCEPTION(megaphone->fallible_me(true).get(), futures::my_error::Foo); + + ASSERT_TRUE(futures::fallible_struct(false).get() != nullptr); + EXPECT_EXCEPTION(futures::fallible_struct(true).get(), futures::my_error::Foo); + ASSERT_TRUE(futures::async_new_megaphone().get() != nullptr); + ASSERT_TRUE(futures::async_maybe_new_megaphone(true).get() != nullptr); + ASSERT_TRUE(futures::async_maybe_new_megaphone(false).get() == nullptr); + ASSERT_EQ( + "HELLO, FRIEND!", + futures::say_after_with_megaphone(megaphone, 1, "Friend").get() + ); + + ASSERT_TRUE(futures::Megaphone::init().get() != nullptr); + ASSERT_TRUE(futures::Megaphone::secondary().get() != nullptr); + EXPECT_EXCEPTION(futures::FallibleMegaphone::init().get(), futures::my_error::Foo); + + auto udl_megaphone = futures::UdlMegaphone::init().get(); + ASSERT_EQ("HELLO, UDL!", udl_megaphone->say_after(1, "Udl").get()); + ASSERT_TRUE(futures::UdlMegaphone::secondary().get() != nullptr); + + auto record = futures::new_my_record("value", 42).get(); + ASSERT_EQ("value", record.a); + ASSERT_EQ(42, record.b); + + auto holding = futures::use_shared_resource({2000, 4000}); + std::this_thread::sleep_for(500ms); + EXPECT_EXCEPTION( + futures::use_shared_resource({0, 250}).get(), + futures::async_error::Timeout + ); + holding.get(); + + return 0; +} diff --git a/fixtures/Cargo.toml b/fixtures/Cargo.toml index 197551d..a18f32e 100644 --- a/fixtures/Cargo.toml +++ b/fixtures/Cargo.toml @@ -21,6 +21,7 @@ uniffi-fixture-docstring = { git = "https://github.com/mozilla/uniffi-rs.git", t uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.2" } uniffi-fixture-trait-methods = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.2" } +uniffi-async-calls = { path = "async-calls" } uniffi-error-types-builtin = { path = "error-types-builtin" } uniffi-custom-types-builtin = { path = "custom-types-builtin" } uniffi-enum-style-test = { path = "enum-style-test" } diff --git a/fixtures/async-calls/Cargo.toml b/fixtures/async-calls/Cargo.toml new file mode 100644 index 0000000..e03383d --- /dev/null +++ b/fixtures/async-calls/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uniffi-async-calls" +version = "0.21.0" +edition = "2021" +license = "MPL-2.0" +publish = false + +[lib] +name = "uniffi_fixture_async_calls" +crate-type = ["lib"] + +[dependencies] +once_cell = "1.18.0" +futures-timer = "3.0" +thiserror = "1.0" +tokio = { version = "1.24.1", features = ["time", "sync"] } +uniffi = { workspace = true, features = ["tokio"] } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/fixtures/async-calls/README.md b/fixtures/async-calls/README.md new file mode 100644 index 0000000..368a5f5 --- /dev/null +++ b/fixtures/async-calls/README.md @@ -0,0 +1,12 @@ +# Async calls fixture + +This fixture is adapted from the MPL-2.0 licensed +[`async-calls` fixture](https://github.com/jhugman/uniffi-bindgen-react-native/tree/main/fixtures/async-calls) +in `uniffi-bindgen-react-native`. + +The public async cases are retained, including the hybrid UDL/proc-macro API, functions, +constructors, object methods, records, optional objects, errors, and timeouts. The adaptation: + +- uses `futures-timer` in place of the upstream repository's private timer service; +- includes only the native shared-resource implementation needed by the C++ tests; and +- exports the upstream `void` function as `void_return`, since `void` is a C++ keyword. diff --git a/fixtures/async-calls/build.rs b/fixtures/async-calls/build.rs new file mode 100644 index 0000000..9a673b0 --- /dev/null +++ b/fixtures/async-calls/build.rs @@ -0,0 +1,9 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +fn main() { + uniffi::generate_scaffolding("src/async-calls.udl").unwrap(); +} diff --git a/fixtures/async-calls/src/async-calls.udl b/fixtures/async-calls/src/async-calls.udl new file mode 100644 index 0000000..e567386 --- /dev/null +++ b/fixtures/async-calls/src/async-calls.udl @@ -0,0 +1,15 @@ +namespace futures { + [Async] + boolean always_ready(); +}; + +interface UdlMegaphone { + [Async] + constructor(); + + [Async, Name="secondary"] + constructor(); + + [Async] + string say_after(u16 ms, string who); +}; diff --git a/fixtures/async-calls/src/lib.rs b/fixtures/async-calls/src/lib.rs new file mode 100644 index 0000000..00f352c --- /dev/null +++ b/fixtures/async-calls/src/lib.rs @@ -0,0 +1,205 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +mod platform_specific; +use platform_specific::acquire_with_timeout; + +use futures_timer::Delay; +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; + +static PENDING_COUNT: AtomicU64 = AtomicU64::new(0); + +struct PendingGuard; + +impl Drop for PendingGuard { + fn drop(&mut self) { + PENDING_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} + +#[uniffi::export] +pub fn greet(who: String) -> String { + format!("Hello, {who}") +} + +pub async fn always_ready() -> bool { + true +} + +#[uniffi::export] +pub async fn void_return() {} + +#[uniffi::export] +pub async fn say() -> String { + Delay::new(Duration::from_secs(2)).await; + "Hello, Future!".to_string() +} + +#[uniffi::export] +pub async fn say_after(ms: u16, who: String) -> String { + Delay::new(Duration::from_millis(ms.into())).await; + format!("Hello, {who}!") +} + +#[uniffi::export] +pub async fn sleep(ms: u16) -> bool { + Delay::new(Duration::from_millis(ms.into())).await; + true +} + +#[uniffi::export] +pub async fn sleep_no_return(ms: u16) { + Delay::new(Duration::from_millis(ms.into())).await; +} + +#[uniffi::export] +pub async fn pending() { + PENDING_COUNT.fetch_add(1, Ordering::SeqCst); + let _guard = PendingGuard; + std::future::pending::<()>().await; +} + +#[uniffi::export] +pub fn pending_count() -> u64 { + PENDING_COUNT.load(Ordering::SeqCst) +} + +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum MyError { + #[error("Foo")] + Foo, +} + +#[uniffi::export] +pub async fn fallible_me(do_fail: bool) -> Result { + if do_fail { + Err(MyError::Foo) + } else { + Ok(42) + } +} + +#[uniffi::export] +pub async fn fallible_struct(do_fail: bool) -> Result, MyError> { + if do_fail { + Err(MyError::Foo) + } else { + Ok(new_megaphone()) + } +} + +#[uniffi::export] +pub fn new_megaphone() -> Arc { + Arc::new(Megaphone) +} + +#[uniffi::export] +pub async fn async_new_megaphone() -> Arc { + new_megaphone() +} + +#[uniffi::export] +pub async fn async_maybe_new_megaphone(value: bool) -> Option> { + value.then(new_megaphone) +} + +#[uniffi::export] +pub async fn say_after_with_megaphone(megaphone: Arc, ms: u16, who: String) -> String { + megaphone.say_after(ms, who).await +} + +#[derive(uniffi::Object)] +pub struct Megaphone; + +#[uniffi::export] +impl Megaphone { + #[uniffi::constructor] + pub async fn new() -> Arc { + Delay::new(Duration::from_millis(0)).await; + Arc::new(Self) + } + + #[uniffi::constructor] + pub async fn secondary() -> Arc { + Delay::new(Duration::from_millis(0)).await; + Arc::new(Self) + } + + pub async fn say_after(self: Arc, ms: u16, who: String) -> String { + say_after(ms, who).await.to_uppercase() + } + + pub async fn silence(&self) -> String { + String::new() + } + + pub async fn fallible_me(self: Arc, do_fail: bool) -> Result { + fallible_me(do_fail).await + } +} + +#[derive(uniffi::Object)] +pub struct FallibleMegaphone; + +#[uniffi::export] +impl FallibleMegaphone { + #[uniffi::constructor] + pub async fn new() -> Result, MyError> { + Err(MyError::Foo) + } +} + +pub struct UdlMegaphone; + +impl UdlMegaphone { + pub async fn new() -> Self { + Self + } + + pub async fn secondary() -> Self { + Self + } + + pub async fn say_after(self: Arc, ms: u16, who: String) -> String { + say_after(ms, who).await.to_uppercase() + } +} + +#[derive(uniffi::Record)] +pub struct MyRecord { + pub a: String, + pub b: u32, +} + +#[uniffi::export] +pub async fn new_my_record(a: String, b: u32) -> MyRecord { + MyRecord { a, b } +} + +#[derive(uniffi::Record)] +pub struct SharedResourceOptions { + pub release_after_ms: u16, + pub timeout_ms: u16, +} + +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum AsyncError { + #[error("Timeout")] + Timeout, +} + +#[uniffi::export(async_runtime = "tokio")] +pub async fn use_shared_resource(options: SharedResourceOptions) -> Result<(), AsyncError> { + acquire_with_timeout(options).await +} + +uniffi::include_scaffolding!("async-calls"); diff --git a/fixtures/async-calls/src/platform_specific/mod.rs b/fixtures/async-calls/src/platform_specific/mod.rs new file mode 100644 index 0000000..226fa51 --- /dev/null +++ b/fixtures/async-calls/src/platform_specific/mod.rs @@ -0,0 +1,8 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +mod native; +pub(crate) use native::*; diff --git a/fixtures/async-calls/src/platform_specific/native.rs b/fixtures/async-calls/src/platform_specific/native.rs new file mode 100644 index 0000000..9750869 --- /dev/null +++ b/fixtures/async-calls/src/platform_specific/native.rs @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +use std::time::Duration; + +use crate::{AsyncError, SharedResourceOptions}; + +pub(crate) async fn acquire_with_timeout(options: SharedResourceOptions) -> Result<(), AsyncError> { + use once_cell::sync::Lazy; + use tokio::{ + sync::Mutex, + time::{sleep, timeout}, + }; + + static MUTEX: Lazy> = Lazy::new(|| Mutex::new(())); + + let _guard = timeout( + Duration::from_millis(options.timeout_ms.into()), + MUTEX.lock(), + ) + .await + .map_err(|_| AsyncError::Timeout)?; + + sleep(Duration::from_millis(options.release_after_ms.into())).await; + Ok(()) +} diff --git a/fixtures/src/lib.rs b/fixtures/src/lib.rs index 9c31212..c78f32d 100644 --- a/fixtures/src/lib.rs +++ b/fixtures/src/lib.rs @@ -19,6 +19,7 @@ mod uniffi_fixtures { uniffi_empty_type::uniffi_reexport_scaffolding!(); uniffi_reserved_field_name::uniffi_reexport_scaffolding!(); uniffi_type_flattening::uniffi_reexport_scaffolding!(); + uniffi_fixture_async_calls::uniffi_reexport_scaffolding!(); uniffi_ext_types_export::uniffi_reexport_scaffolding!(); uniffi_ext_types_import::uniffi_reexport_scaffolding!(); From c4ad947de118a30b1698c5d036d2bf590166d49a Mon Sep 17 00:00:00 2001 From: Alan George Date: Tue, 15 Sep 2026 11:23:50 -0600 Subject: [PATCH 6/6] Guard Rust future handle initialization --- bindgen/src/bindings/cpp/templates/async.cpp | 42 ++++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/bindgen/src/bindings/cpp/templates/async.cpp b/bindgen/src/bindings/cpp/templates/async.cpp index 99108fb..2132eed 100644 --- a/bindgen/src/bindings/cpp/templates/async.cpp +++ b/bindgen/src/bindings/cpp/templates/async.cpp @@ -1,6 +1,35 @@ constexpr int8_t UNIFFI_RUST_FUTURE_POLL_READY = 0; constexpr int8_t UNIFFI_RUST_FUTURE_POLL_WAKE = 1; +template +class RustFutureHandleGuard { +public: + // Own the Rust handle until all potentially-throwing C++ wrappers exist. + RustFutureHandleGuard(uint64_t handle, Free &free) noexcept: + handle_(handle), free_(free) {} + + RustFutureHandleGuard(const RustFutureHandleGuard &) = delete; + RustFutureHandleGuard &operator=(const RustFutureHandleGuard &) = delete; + + ~RustFutureHandleGuard() { + if (active_) { + try { + free_(handle_); + } catch (...) { + } + } + } + + void release() noexcept { + active_ = false; + } + +private: + uint64_t handle_; + Free &free_; + bool active_ = true; +}; + template class RustFutureState: public std::enable_shared_from_this> { public: @@ -259,6 +288,7 @@ Future rust_call_async( ) { initialize(); const auto handle = rust_future(); + RustFutureHandleGuard handle_guard(handle, free); using State = RustFutureState; auto state = std::make_shared( @@ -266,16 +296,20 @@ Future rust_call_async( std::move(poll), std::move(cancel), std::move(complete), - std::move(free), + // Keep the guard's cleanup callable valid until ownership transfers. + free, std::move(lift), std::move(error_handler) ); auto future = state->get_future(); - state->start(); - - return Future(std::move(future), [weak_state = std::weak_ptr(state)]() { + Future result(std::move(future), [weak_state = std::weak_ptr(state)]() { if (auto state = weak_state.lock()) { state->cancel(); } }); + + // start() assumes cleanup responsibility, including thread-start failure. + handle_guard.release(); + state->start(); + return result; }