diff --git a/engine/src/conversion/analysis/fun/function_wrapper.rs b/engine/src/conversion/analysis/fun/function_wrapper.rs index 7bbcd0aae..f0f6d152d 100644 --- a/engine/src/conversion/analysis/fun/function_wrapper.rs +++ b/engine/src/conversion/analysis/fun/function_wrapper.rs @@ -60,6 +60,7 @@ pub(crate) enum RustConversionType { FromRValueParamToPtr, FromReferenceWrapperToPointer, // unwrapped_type is always Type::Ptr FromPointerToReferenceWrapper, // unwrapped_type is always Type::Ptr + WrapResult(Box), } impl RustConversionType { @@ -159,6 +160,14 @@ impl TypeConversionPolicy { } } + pub(crate) fn wrap_result(self) -> Self { + let inner_conversion = Box::new(self.rust_conversion); + TypeConversionPolicy { + rust_conversion: RustConversionType::WrapResult(inner_conversion), + ..self + } + } + pub(crate) fn cpp_work_needed(&self) -> bool { !matches!(self.cpp_conversion, CppConversionType::None) } diff --git a/engine/src/conversion/analysis/fun/mod.rs b/engine/src/conversion/analysis/fun/mod.rs index 6dd46b57d..76457e268 100644 --- a/engine/src/conversion/analysis/fun/mod.rs +++ b/engine/src/conversion/analysis/fun/mod.rs @@ -167,6 +167,8 @@ pub(crate) struct FnAnalysis { /// subclass entries for them. But we do not want to have them /// be externally callable. pub(crate) ignore_reason: Result<(), ConvertErrorWithContext>, + /// Whether this method can throw an exception + pub(crate) throwable: bool, /// Whether this can be called by external code. Not so for /// protected methods. pub(crate) externally_callable: bool, @@ -825,7 +827,7 @@ impl<'a> FnAnalyzer<'a> { ); let (kind, error_context, rust_name) = if let Some(trait_details) = trait_details { trait_details - } else if let Some(self_ty) = self_ty { + } else if let Some(ref self_ty) = self_ty { // Some kind of method or static method. let type_ident = self_ty.get_final_item(); // bindgen generates methods with the name: @@ -837,7 +839,7 @@ impl<'a> FnAnalyzer<'a> { let mut rust_name = ideal_rust_name; let nested_type_ident = self .nested_type_name_map - .get(&self_ty) + .get(self_ty) .map(|s| s.as_str()) .unwrap_or_else(|| self_ty.get_final_item()); if matches!( @@ -851,7 +853,7 @@ impl<'a> FnAnalyzer<'a> { } rust_name = predetermined_rust_name .unwrap_or_else(|| self.get_overload_name(ns, type_ident, rust_name)); - let error_context = self.error_context_for_method(&self_ty, &rust_name); + let error_context = self.error_context_for_method(self_ty, &rust_name); // If this is 'None', then something weird is going on. We'll check for that // later when we have enough context to generate useful errors. @@ -883,7 +885,7 @@ impl<'a> FnAnalyzer<'a> { ( FnKind::TraitMethod { kind, - impl_for: self_ty, + impl_for: self_ty.clone(), details: Box::new(TraitMethodDetails { trt: TraitImplSignature { ty: ty.into(), @@ -904,7 +906,7 @@ impl<'a> FnAnalyzer<'a> { } else { ( FnKind::Method { - impl_for: self_ty, + impl_for: self_ty.clone(), method_kind: MethodKind::Constructor { is_default: false }, }, error_context, @@ -914,12 +916,12 @@ impl<'a> FnAnalyzer<'a> { } else if matches!(fun.special_member, Some(SpecialMemberKind::Destructor)) { rust_name = predetermined_rust_name .unwrap_or_else(|| self.get_overload_name(ns, type_ident, rust_name)); - let error_context = self.error_context_for_method(&self_ty, &rust_name); + let error_context = self.error_context_for_method(self_ty, &rust_name); let ty = Type::Path(self_ty.to_type_path()); ( FnKind::TraitMethod { kind: TraitMethodKind::Destructor, - impl_for: self_ty, + impl_for: self_ty.clone(), details: Box::new(TraitMethodDetails { trt: TraitImplSignature { ty: ty.into(), @@ -973,10 +975,10 @@ impl<'a> FnAnalyzer<'a> { // Disambiguate overloads. let rust_name = predetermined_rust_name .unwrap_or_else(|| self.get_overload_name(ns, type_ident, rust_name)); - let error_context = self.error_context_for_method(&self_ty, &rust_name); + let error_context = self.error_context_for_method(self_ty, &rust_name); ( FnKind::Method { - impl_for: self_ty, + impl_for: self_ty.clone(), method_kind, }, error_context, @@ -1196,6 +1198,16 @@ impl<'a> FnAnalyzer<'a> { } let mut cxxbridge_name = make_ident(&cxxbridge_name); + let friendly_name = { + let ns_pfx = (!ns.is_empty()) + .then_some(format!("{ns}::")) + .unwrap_or_default(); + let ty_pfx = self_ty.map_or(String::default(), |ty| format!("{ty}::")); + + format!("{ns_pfx}{ty_pfx}{}", name.cpp_name()) + }; + let throwable = self.config.is_throwable(&friendly_name); + // Analyze the return type, just as we previously did for the // parameters. let mut return_analysis = self @@ -1432,6 +1444,12 @@ impl<'a> FnAnalyzer<'a> { _ => RustRenameStrategy::None, }; + let ret_type_conversion = if throwable { + ret_type_conversion.map(|c| c.wrap_result()) + } else { + ret_type_conversion + }; + let analysis = FnAnalysis { cxxbridge_name: cxxbridge_name.clone(), rust_name: rust_name.clone(), @@ -1444,6 +1462,7 @@ impl<'a> FnAnalyzer<'a> { requires_unsafe, vis: vis.into(), cpp_wrapper, + throwable, deps, ignore_reason, externally_callable, diff --git a/engine/src/conversion/codegen_rs/fun_codegen.rs b/engine/src/conversion/codegen_rs/fun_codegen.rs index 1b68e8c2a..119f64533 100644 --- a/engine/src/conversion/codegen_rs/fun_codegen.rs +++ b/engine/src/conversion/codegen_rs/fun_codegen.rs @@ -119,6 +119,7 @@ pub(super) fn gen_function( param_details: ¶m_details, cxxbridge_name: &cxxbridge_name, rust_name, + throwable: analysis.throwable, unsafety: &analysis.requires_unsafe, always_unsafe_due_to_trait_definition, doc_attrs: &doc_attrs, @@ -143,7 +144,8 @@ pub(super) fn gen_function( .. } => { // Constructor. - impl_entry = Some(fn_generator.generate_constructor_impl(impl_for)); + impl_entry = + Some(fn_generator.generate_constructor_impl(impl_for, analysis.throwable)); } FnKind::Method { ref impl_for, @@ -196,6 +198,16 @@ pub(super) fn gen_function( // which the user has declared. let params = unqualify_params(params); let ret_type = unqualify_ret_type(ret_type.into_owned()); + + let ret_type = if analysis.throwable { + match ret_type { + ReturnType::Default => parse_quote!(-> Result<()>), + ReturnType::Type(_, ty) => parse_quote!(-> Result< #ty >), + } + } else { + ret_type + }; + // And we need to make an attribute for the namespace that the function // itself is in. let namespace_attr = if ns.is_empty() || wrapper_function_needed { @@ -232,6 +244,7 @@ struct FnGenerator<'a> { param_details: &'a [ArgumentAnalysis], ret_conversion: &'a Option, ret_type: &'a ReturnType, + throwable: bool, cxxbridge_name: &'a Ident, rust_name: &'a str, unsafety: &'a UnsafetyNeeded, @@ -293,8 +306,14 @@ impl<'a> FnGenerator<'a> { } RustParamConversion::ReturnValue { ty } => { ptr_arg_name = Some(pd.name.to_token_stream()); - ret_type = Cow::Owned(parse_quote! { - -> impl autocxx::moveit::new::New + ret_type = Cow::Owned(if self.throwable { + parse_quote! { + -> impl autocxx::moveit::new::TryNew + } + } else { + parse_quote! { + -> impl autocxx::moveit::new::New + } }); arg_list.push(pd.name.to_token_stream()); } @@ -369,10 +388,18 @@ impl<'a> FnGenerator<'a> { )); closure_stmts.push(call_body); let closure_stmts = maybe_unsafes_to_tokens(closure_stmts, true); - vec![MaybeUnsafeStmt::needs_unsafe(parse_quote! { - autocxx::moveit::new::by_raw(move |#ptr_arg_name| { - #closure_stmts - }) + vec![MaybeUnsafeStmt::needs_unsafe(if self.throwable { + parse_quote! { + autocxx::moveit::new::try_by_raw(move |#ptr_arg_name| { + #closure_stmts + }) + } + } else { + parse_quote! { + autocxx::moveit::new::by_raw(move |#ptr_arg_name| { + #closure_stmts + }) + } })] } else { let mut call_stmts = local_variables; @@ -452,8 +479,13 @@ impl<'a> FnGenerator<'a> { fn generate_constructor_impl( &self, impl_block_type_name: &QualifiedName, + throwable: bool, ) -> Box { - let ret_type: ReturnType = parse_quote! { -> impl autocxx::moveit::new::New }; + let ret_type: ReturnType = if throwable { + parse_quote! { -> impl autocxx::moveit::new::TryNew } + } else { + parse_quote! { -> impl autocxx::moveit::new::New } + }; let (lifetime_tokens, wrapper_params, ret_type, call_body) = self.common_parts(true, &None, Some(ret_type)); let rust_name = make_ident(self.rust_name); diff --git a/engine/src/conversion/codegen_rs/function_wrapper_rs.rs b/engine/src/conversion/codegen_rs/function_wrapper_rs.rs index 4afe3c722..cbdc347dd 100644 --- a/engine/src/conversion/codegen_rs/function_wrapper_rs.rs +++ b/engine/src/conversion/codegen_rs/function_wrapper_rs.rs @@ -33,7 +33,17 @@ pub(super) enum RustParamConversion { impl TypeConversionPolicy { pub(super) fn rust_conversion(&self, var: Expr, counter: &mut usize) -> RustParamConversion { - match self.rust_conversion { + let mut wrap_result = false; + let rust_conversion = + if let RustConversionType::WrapResult(ref inner) = self.rust_conversion { + wrap_result = true; + inner.as_ref() + } else { + &self.rust_conversion + }; + + let base_conversion = match rust_conversion { + RustConversionType::WrapResult(_) => panic!("Nested Results are not supported!"), RustConversionType::None => RustParamConversion::Param { ty: self.converted_rust_type(), local_variables: Vec::new(), @@ -213,6 +223,30 @@ impl TypeConversionPolicy { conversion_requires_unsafe: false, } } + }; + + if wrap_result { + match base_conversion { + RustParamConversion::ReturnValue { ty } => RustParamConversion::ReturnValue { + ty: parse_quote!( ::std::result::Result< #ty, ::cxx::Exception> ), + }, + RustParamConversion::Param { + ty, + local_variables, + conversion, + conversion_requires_unsafe, + } => { + let ty = parse_quote!( impl autocxx::moveit::new::TryNew ); + RustParamConversion::Param { + ty, + local_variables, + conversion, + conversion_requires_unsafe, + } + } + } + } else { + base_conversion } } } diff --git a/integration-tests/tests/integration_test.rs b/integration-tests/tests/integration_test.rs index be7f62397..265c9c626 100644 --- a/integration-tests/tests/integration_test.rs +++ b/integration-tests/tests/integration_test.rs @@ -118,6 +118,225 @@ fn test_take_i32() { run_test(cxx, hdr, rs, &["take_int"], &[]); } +#[test] +fn test_exception_pod() { + let hdr = indoc! {r#" + #include + #include + #include + + struct B { uint32_t x; }; + + void do_nothing(); + uint32_t get_integer(); + uint32_t get_wrapped_integer(); + B get_wrapped_struct(); + uint32_t throw_exception(); + "#}; + let cxx = indoc! {r#" + void do_nothing() { + } + + uint32_t get_integer() { + return 5; + } + + uint32_t get_wrapped_integer() { + return 5; + } + + B get_wrapped_struct() { + return B { 5 }; + } + + uint32_t throw_exception() { + throw std::invalid_argument("EXCEPTION"); + } + "#}; + let hexathorpe = Token![#](Span::call_site()); + let rs = quote! { + mod a { + use autocxx::prelude::*; + include_cpp! { + #hexathorpe include "input.h" + safety!(unsafe_ffi) + pod!("B") + generate!("do_nothing") + generate!("get_integer") + generate!("get_wrapped_integer") + generate!("get_wrapped_struct") + generate!("throw_exception") + throws!("do_nothing") + throws!("get_wrapped_integer") + throws!("get_wrapped_struct") + throws!("throw_exception") + } + pub use ffi::*; + } + fn main() { + let maybe_nothing = a::do_nothing(); + assert!(maybe_nothing.is_ok()); + assert_eq!(maybe_nothing.unwrap(), ()); + + assert_eq!(a::get_integer(), 5); + + let maybe_int = a::get_wrapped_integer(); + assert!(maybe_int.is_ok()); + assert_eq!(maybe_int.unwrap(), 5); + + let maybe_b = a::get_wrapped_struct(); + assert!(maybe_b.is_ok()); + let b = maybe_b.unwrap(); + assert_eq!(b.x, 5); + + assert!(a::throw_exception().is_err()); + } + }; + do_run_test_manual(cxx, hdr, rs, None, None).unwrap(); +} + +#[test] +fn test_exception_uniqueptr() { + let hdr = indoc! {r#" + #include + #include + + std::string get_str(); + std::string try_get_str(); + std::string throw_exception(); + "#}; + let cxx = indoc! {r#" + std::string get_str() { + return std::string("foobar"); + } + + std::string try_get_str() { + return get_str(); + } + + std::string throw_exception() { + throw std::invalid_argument("EXCEPTION"); + } + "#}; + let hexathorpe = Token![#](Span::call_site()); + let rs = quote! { + mod a { + use autocxx::prelude::*; + include_cpp! { + #hexathorpe include "input.h" + safety!(unsafe_ffi) + generate!("get_str") + generate!("try_get_str") + generate!("throw_exception") + throws!("try_get_str") + throws!("throw_exception") + } + pub use ffi::*; + } + fn main() { + assert!(a::try_get_str().is_ok()); + assert!(a::throw_exception().is_err()); + } + }; + do_run_test_manual(cxx, hdr, rs, None, None).unwrap(); +} + +#[test] +fn test_exception_moveit() { + let hdr = indoc! {r#" + #include + #include + class B { + public: + B() { throw std::invalid_argument("EXCEPTION"); } + B(std::string s) : s(s) {} + private: + std::string s; + }; + + B get_b(); + B try_get_b(); + B throw_exception(); + "#}; + let cxx = indoc! {r#" + B get_b() { + return B("foobar"); + } + + B try_get_b() { + return get_b(); + } + + B throw_exception() { + return B(); + } + "#}; + let hexathorpe = Token![#](Span::call_site()); + let rs = quote! { + mod a { + use autocxx::prelude::*; + include_cpp! { + #hexathorpe include "input.h" + safety!(unsafe_ffi) + generate!("B") + generate!("get_b") + generate!("try_get_b") + generate!("throw_exception") + throws!("try_get_b") + throws!("throw_exception") + } + pub use ffi::*; + } + fn main() { + use autocxx::WithinBox; + use autocxx::TryWithinBox; + let _ = a::get_b().within_box(); + let maybe_b = a::try_get_b().try_within_box(); + assert!(maybe_b.is_ok()); + let maybe_b = a::throw_exception().try_within_box(); + assert!(maybe_b.is_err()); + } + }; + do_run_test_manual(cxx, hdr, rs, None, None).unwrap(); +} + +#[test] +fn test_exception_constructor() { + let hdr = indoc! {r#" + #include + #include + class B { + public: + B() { throw std::invalid_argument("EXCEPTION"); } + B(std::string s) : s(s) {} + private: + std::string s; + }; + "#}; + let cxx = indoc! {""}; + let hexathorpe = Token![#](Span::call_site()); + let rs = quote! { + mod a { + use autocxx::prelude::*; + include_cpp! { + #hexathorpe include "input.h" + safety!(unsafe_ffi) + generate!("B") + throws!("B::B") + } + pub use ffi::*; + } + fn main() { + use autocxx::TryWithinBox; + let b = a::B::new().try_within_box(); + assert!(b.is_err()); + let b = a::B::new1("foobar").try_within_box(); + assert!(b.is_ok()); + } + }; + do_run_test_manual(cxx, hdr, rs, None, None).unwrap(); +} + #[test] fn test_nested_module() { let cxx = indoc! {" diff --git a/parser/src/config.rs b/parser/src/config.rs index 2421e09a6..7bb8dcb78 100644 --- a/parser/src/config.rs +++ b/parser/src/config.rs @@ -219,6 +219,7 @@ pub struct IncludeCppConfig { pub allowlist: Allowlist, pub(crate) blocklist: Vec, pub(crate) constructor_blocklist: Vec, + pub(crate) throwlist: Vec, pub instantiable: Vec, pub(crate) exclude_utilities: bool, pub(crate) mod_name: Option, @@ -365,6 +366,7 @@ impl IncludeCppConfig { || self.is_rust_fun(cpp_name) || self.is_rust_type_name(cpp_name) || self.is_concrete_type(cpp_name) + || self.is_throwable(cpp_name) || match &self.allowlist { Allowlist::Unspecified(_) => panic!("Eek no allowlist yet"), Allowlist::All => true, @@ -414,6 +416,10 @@ impl IncludeCppConfig { self.is_rust_type_name(&id_string) || self.is_subclass_holder(&id_string) } + pub fn is_throwable(&self, fun: &str) -> bool { + self.throwlist.iter().any(|f| f == fun) + } + fn is_rust_type_name(&self, possible_ty: &str) -> bool { self.rust_types .iter() diff --git a/parser/src/directives.rs b/parser/src/directives.rs index 7dbfa7124..6a29d6b96 100644 --- a/parser/src/directives.rs +++ b/parser/src/directives.rs @@ -93,6 +93,13 @@ pub(crate) fn get_directives() -> &'static DirectivesMap { |config| &config.exclude_utilities, )), ); + need_exclamation.insert( + "throws".into(), + Box::new(StringList( + |config| &mut config.throwlist, + |config| &config.throwlist, + )), + ); need_exclamation.insert("name".into(), Box::new(ModName)); need_exclamation.insert("concrete".into(), Box::new(Concrete)); need_exclamation.insert("rust_type".into(), Box::new(RustType { output: false })); diff --git a/src/lib.rs b/src/lib.rs index b0eeec17a..411decb0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -369,6 +369,12 @@ macro_rules! subclass { ($($tt:tt)*) => { $crate::usage!{$($tt)*} }; } +/// See [`subclass::subclass`]. +#[macro_export] +macro_rules! throws { + ($($tt:tt)*) => { $crate::usage!{$($tt)*} }; +} + /// Indicates that a C++ type can definitely be instantiated. This has effect /// only in a very specific case: /// * the type is a typedef to something else @@ -583,6 +589,11 @@ pub trait WithinUniquePtr { fn within_unique_ptr(self) -> cxx::UniquePtr; } +pub trait TryWithinUniquePtr { + type Inner: UniquePtrTarget + MakeCppStorage; + fn try_within_unique_ptr(self) -> Result, cxx::Exception>; +} + /// Provides utility functions to emplace any [`moveit::New`] into a /// [`Box`]. Automatically imported by the autocxx prelude /// and implemented by any (autocxx-related) [`moveit::New`]. @@ -601,6 +612,12 @@ pub trait WithinBox { fn within_cpp_pin(self) -> CppPin; } +pub trait TryWithinBox { + type Inner; + fn try_within_box(self) -> Result>, cxx::Exception>; + fn try_within_cpp_pin(self) -> Result, cxx::Exception>; +} + use cxx::kind::Trivial; use cxx::ExternType; use moveit::Emplace; @@ -630,6 +647,62 @@ where } } +impl TryWithinUniquePtr for N +where + N: TryNew, + T: UniquePtrTarget + MakeCppStorage, +{ + type Inner = T; + fn try_within_unique_ptr(self) -> Result, cxx::Exception> { + UniquePtr::try_emplace(self) + } +} + +impl TryWithinBox for N +where + N: TryNew, +{ + type Inner = T; + + fn try_within_box(self) -> Result>, cxx::Exception> { + Box::try_emplace(self) + } + + fn try_within_cpp_pin(self) -> Result, cxx::Exception> { + Box::try_emplace(self).map(CppPin::from_pinned_box) + } +} + +pub trait TryWithinUniquePtrTrivial { + type Inner: UniquePtrTarget + Sized + Unpin; + fn try_within_unique_ptr(self) -> Result, cxx::Exception>; +} + +impl TryWithinUniquePtrTrivial for Result +where + T: UniquePtrTarget + ExternType + Sized + Unpin, +{ + type Inner = T; + fn try_within_unique_ptr(self) -> Result, cxx::Exception> { + self.map(UniquePtr::new) + } +} + +pub trait TryWithinBoxTrivial { + type Inner: Sized + Unpin; + fn try_within_box(self) -> Result>, cxx::Exception>; +} + +impl TryWithinBoxTrivial for Result +where + T: ExternType + Sized + Unpin, +{ + type Inner = T; + fn try_within_box(self) -> Result>, cxx::Exception> { + self.map(Box::pin) + } +} + /// Emulates the [`WithinUniquePtr`] trait, but for trivial (plain old data) types. /// This allows such types to behave identically if a type is changed from /// `generate!` to `generate_pod!`. @@ -673,6 +746,7 @@ where use cxx::memory::UniquePtrTarget; use cxx::UniquePtr; use moveit::New; +use moveit::TryNew; pub use rvalue_param::RValueParam; pub use rvalue_param::RValueParamHandler; pub use value_param::as_copy; @@ -706,6 +780,10 @@ pub mod prelude { pub use crate::CppUniquePtrPin; pub use crate::PinMut; pub use crate::RValueParam; + pub use crate::TryWithinBox; + pub use crate::TryWithinBoxTrivial; + pub use crate::TryWithinUniquePtr; + pub use crate::TryWithinUniquePtrTrivial; pub use crate::ValueParam; pub use crate::WithinBox; pub use crate::WithinBoxTrivial;