Skip to content

Commit f8a28c7

Browse files
authored
Merge pull request #1544 from godot-rust/feature/func-result
Support `Result<T, E>` as return type in `#[func]`
2 parents ed460cd + 3e39db8 commit f8a28c7

15 files changed

Lines changed: 883 additions & 27 deletions

File tree

godot-core/src/builtin/callable.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -667,12 +667,12 @@ mod custom_callable {
667667
let ctx = meta::CallContext::custom_callable(name.as_ref());
668668

669669
let err = unsafe { &mut *r_error };
670-
crate::private::handle_fallible_varcall(&ctx, err, move || {
670+
crate::private::handle_fallible_varcall(&ctx, err, || {
671671
// Re-borrow inside closure so C doesn't have to be UnwindSafe.
672672
let c: &mut C = unsafe { CallableUserdata::inner_from_raw(callable_userdata) };
673673
let result = c.invoke(arg_refs);
674674

675-
unsafe { meta::varcall_return_checked(Ok(result), r_return, r_error) };
675+
unsafe { meta::varcall_return_checked(Ok(result), r_return, r_error, &ctx)? };
676676
Ok(())
677677
});
678678
}
@@ -697,7 +697,7 @@ mod custom_callable {
697697
let ctx = meta::CallContext::custom_callable(&w.name);
698698

699699
let err = unsafe { &mut *r_error };
700-
crate::private::handle_fallible_varcall(&ctx, err, move || {
700+
crate::private::handle_fallible_varcall(&ctx, err, || {
701701
// Re-borrow inside closure so FnMut doesn't have to be UnwindSafe.
702702
let w: &mut FnWrapper<F> =
703703
unsafe { CallableUserdata::inner_from_raw(callable_userdata) };
@@ -712,7 +712,7 @@ mod custom_callable {
712712
);
713713

714714
let result = (w.rust_function)(arg_refs).to_variant();
715-
unsafe { meta::varcall_return_checked(Ok(result), r_return, r_error) };
715+
unsafe { meta::varcall_return_checked(Ok(result), r_return, r_error, &ctx)? };
716716
Ok(())
717717
});
718718
}

godot-core/src/meta/error/call_error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ impl CallError {
326326
err
327327
}
328328

329+
/// `#[func]` returning `Result<T, E>` which hits the `Err(E)` case *and* intends to fail the Godot call.
330+
pub(crate) fn failed_by_user_result(call_ctx: &CallContext, message: String) -> Self {
331+
Self::new(call_ctx, message, None)
332+
}
333+
329334
/// Whether this error was caused by a Rust panic (as opposed to a Godot or godot-rust error).
330335
///
331336
/// If true, the panic hook has already printed the error; callers can avoid printing it again.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
//! The [`ErrorToGodot`] trait for mapping `Result<T, E>` to Godot return types.
9+
//!
10+
//! Built-in strategies are in the [`strat`][super::strat] module.
11+
12+
use crate::meta::ToGodot;
13+
14+
/// Defines how `Result<T, E>` returned by `#[func]` is mapped to Godot.
15+
///
16+
/// When implemented for a type `E`, this trait enables `Result<T, E>` return types
17+
/// [through a blanket impl](../trait.ToGodot.html#impl-ToGodot-for-Result%3CT,+E%3E).
18+
///
19+
/// # Implementing the trait
20+
/// The associated type [`Mapped`][Self::Mapped] determines what GDScript sees as the function's return type. This type
21+
/// can depend on `T` -- the ok-value type of the `Result` -- because the trait is generic over `T`.
22+
///
23+
/// Users then override [`result_to_godot()`][Self::result_to_godot], returning a [`CallOutcome`]:
24+
/// - [`CallOutcome::Return(mapped)`][CallOutcome::Return] -- the call succeeds; pass `mapped` back to GDScript.
25+
/// - [`CallOutcome::CallFailed(msg)`][CallOutcome::CallFailed] -- an unexpected error occurred; log `msg` and fail the call.
26+
///
27+
/// # Built-in strategies
28+
/// See the [`strat`][crate::meta::error::strat] module for all provided implementations, or for inspirations for custom error handling.
29+
///
30+
/// # Example: typed `Array<T>` with 0 or 1 elements
31+
/// Since the trait is generic over `T`, custom implementations can require tighter bounds (such as [`Element`][crate::meta::Element]) and use
32+
/// a typed `Array<T>` as the mapped type.
33+
///
34+
/// This example returns a 1-element array on success, or a 0-element one on error -- a poor man's `Option<T>` in GDScript.
35+
///
36+
/// ```no_run
37+
/// # use godot::prelude::*;
38+
/// use godot::builtin::Array;
39+
/// use godot::meta::error::{CallOutcome, ErrorToGodot};
40+
/// use godot::meta::{Element, ref_to_arg};
41+
///
42+
/// struct MyError(String);
43+
///
44+
/// impl<T: Element> ErrorToGodot<T> for MyError {
45+
/// // GDScript sees Array[T] as the #[func]'s return type.
46+
/// type Mapped = Array<T>;
47+
///
48+
/// fn result_to_godot(result: Result<&T, &Self>) -> CallOutcome<Array<T>> {
49+
/// // Construct [elem] or [].
50+
/// let array = match result {
51+
/// Ok(elem) => array![ref_to_arg(elem)],
52+
/// Err(_) => Array::new(),
53+
/// };
54+
///
55+
/// // We always return a value, never fail the call -> only use CallOutcome::Return.
56+
/// CallOutcome::Return(array)
57+
/// }
58+
/// }
59+
/// ```
60+
///
61+
/// GDScript usage:
62+
/// ```gdscript
63+
/// var result := node.some_fn() # typed Array[...]
64+
/// if result.is_empty():
65+
/// print("Operation failed")
66+
/// else:
67+
/// var value := result.front() # typed!
68+
/// ```
69+
pub trait ErrorToGodot<T: ToGodot>: Sized {
70+
/// The type to which `Result<T, Self>` is mapped on Godot side.
71+
type Mapped: ToGodot;
72+
73+
/// Map a `Result<T, Self>` to a Godot return value or an unexpected-error message.
74+
fn result_to_godot(result: Result<&T, &Self>) -> CallOutcome<Self::Mapped>;
75+
}
76+
77+
/// Outcome of mapping a `Result<T, E>` for a `#[func]` return value.
78+
///
79+
/// Returned by [`ErrorToGodot::result_to_godot()`]. Decides how Godot handles the result of a user-defined `#[func]`.
80+
pub enum CallOutcome<R> {
81+
/// Pass this value back to GDScript; the call succeeds.
82+
Return(R),
83+
84+
/// The call encounters an unexpected error; log provided message and perform best-effort failure handling.
85+
///
86+
/// This either stops the calling GDScript function or results in a default value of `R` on Godot side. Rust callers using
87+
/// `Object::try_call()` always receive `Err`. For detailed Godot-side semantics and an example, see
88+
/// [`strat::Unexpected`][crate::meta::error::strat::Unexpected].
89+
CallFailed(String),
90+
}
91+
92+
// ----------------------------------------------------------------------------------------------------------------------------------------------
93+
// Macro for immediately exiting function.
94+
95+
/// Return early from a `#[func]`, creating an error value from a format string (including string literals).
96+
///
97+
/// Same principle as [`eyre::bail!`](https://docs.rs/eyre/latest/eyre/macro.bail.html),
98+
/// [`miette::bail!`](https://docs.rs/miette/latest/miette/macro.bail.html), and
99+
/// [`anyhow::bail!`](https://docs.rs/anyhow/latest/anyhow/macro.bail.html).
100+
///
101+
/// This macro expands to `return Err(E::from(format!(...)))`, where `E` is inferred from the function's return type.
102+
/// Accepts a string literal or a `format!`-style format string with arguments.
103+
///
104+
/// Works with any error type `E` that implements `From<String>`, e.g. [`strat::Unexpected`][crate::meta::error::strat::Unexpected].
105+
#[macro_export]
106+
macro_rules! func_bail {
107+
($($arg:tt)*) => {
108+
return ::std::result::Result::Err(::std::convert::From::from(
109+
::std::format!($($arg)*)
110+
))
111+
};
112+
}

godot-core/src/meta/error/io_error.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ use crate::classes::FileAccess;
1212
use crate::global::Error as GodotError;
1313
use crate::obj::Gd;
1414

15-
/// Error that can occur while using `gdext` IO utilities.
15+
/// Error that can occur while using godot-rust I/O utilities.
16+
///
17+
/// Some APIs using this:
18+
/// - [`tools::try_load()`][crate::tools::try_load]
19+
/// - [`tools::try_save()`][crate::tools::try_save]
20+
/// - [`tools::GFile::try_from_unique()`][crate::tools::GFile::try_from_unique]
1621
#[derive(Debug)]
1722
pub struct IoError {
1823
data: ErrorData,

godot-core/src/meta/error/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@
1010
mod call_error;
1111
mod call_error_type;
1212
mod convert_error;
13+
mod error_to_godot;
1314
mod io_error;
1415
mod string_error;
1516

17+
pub mod strat;
18+
1619
pub use call_error::*;
1720
pub use call_error_type::*;
1821
pub use convert_error::*;
22+
pub use error_to_godot::*;
1923
pub use io_error::*;
2024
pub use string_error::*;
25+
26+
pub use crate::func_bail;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
//! Built-in [`ErrorToGodot`] strategies for mapping `Result<T, E>` return types of `#[func]` methods.
9+
//!
10+
//! This module is intended to be used qualified with `strat::` module: `strat::Unexpected` etc.
11+
//! It is re-exported in the prelude to enable this.
12+
//!
13+
//! # Overview
14+
//! Each type in this module implements [`ErrorToGodot`] with a different mapping strategy. Some strategies are not provided out-of-the-box
15+
//! but could serve as inspiration to build your own. If you find any of those useful, let us know, and we may consider adding them.
16+
//!
17+
//! | Strategy | `Mapped` type | Ok path | Err path | GDScript ergonomics |
18+
//! |------------------------------------------------|-------------------------------------|--------------------|---------------------------|-----------------------------------------|
19+
//! | [`strat::Unexpected`]<br>Unexpected errors | `T` | `val.clone()` | Default or<br>failed call | Sees `T`; `?` works with any `Error` |
20+
//! | `()`<br>Nil on error | `Variant` | `val.to_variant()` | `null` | `if val == null` |
21+
//! | [`global::Error`]<br>Godot error enum | `global::Error` | `OK` constant | `ERR_*` constant | `val == OK` |
22+
//! | _(not provided)_<br>`Variant` | `Variant` | `val.to_variant()` | `err.to_variant()` | Must check type/value |
23+
//! | _(not provided)_<br>Dictionary `ok`/`err` | `Dictionary`<br>`<GString,Variant>` | `{"ok" => val}` | `{"err" => msg}` | `d.has("ok")`<br>`d["ok"]` |
24+
//! | _(not provided)_<br>Array 0/1 elems | `Array<T>` | `[val]` | `[]` | `a.is_empty()`<br>`a.front()` -- typed! |
25+
//! | _(not provided)_<br>Custom class | `Gd<RustResult>` | wrap in class | wrap in class | `r.is_ok()`<br>`r.unwrap()` |
26+
27+
mod unexpected;
28+
29+
pub use unexpected::*;
30+
31+
use super::{CallOutcome, ErrorToGodot};
32+
use crate::global;
33+
use crate::meta::ToGodot;
34+
#[expect(unused)] // for docs.
35+
use crate::meta::error::strat;
36+
37+
// ----------------------------------------------------------------------------------------------------------------------------------------------
38+
// () impl: GDScript sees Variant -- nil on error, val.to_variant() on success.
39+
40+
/// Error strategy that returns `null` on error, instead of making the call fail.
41+
///
42+
/// Use this when an absent value is a normal outcome that GDScript should handle, for example a missing save file
43+
/// for a new player. GDScript receives a `Variant` containing either the value or `null`.
44+
///
45+
/// Since `()` discards all error information, use `.map_err(|_| ())?` to propagate any error into it.
46+
///
47+
/// # Example
48+
/// ```no_run
49+
/// use godot::prelude::*;
50+
/// # #[derive(GodotClass)] #[class(init, base=Node)] struct PlayerData;
51+
///
52+
/// #[godot_api]
53+
/// impl PlayerData {
54+
/// // Returns the high score from a save file, or null if absent or unreadable.
55+
/// // A missing file is normal for new players -- GDScript handles null gracefully.
56+
/// #[func]
57+
/// fn load_high_score(&self, save_path: GString) -> Result<i64, ()> {
58+
/// let text = std::fs::read_to_string(save_path.to_string()).map_err(|_| ())?;
59+
/// text.trim().parse::<i64>().map_err(|_| ())
60+
/// }
61+
/// }
62+
/// ```
63+
///
64+
/// GDScript usage:
65+
/// ```gdscript
66+
/// var score = player.load_high_score("user://save.dat")
67+
/// if score == null:
68+
/// score = 0 # New player, no save file yet.
69+
/// ```
70+
impl<T: ToGodot> ErrorToGodot<T> for () {
71+
type Mapped = crate::builtin::Variant;
72+
73+
fn result_to_godot(result: Result<&T, &Self>) -> CallOutcome<crate::builtin::Variant> {
74+
match result {
75+
Ok(val) => CallOutcome::Return(val.to_variant()),
76+
Err(()) => CallOutcome::Return(crate::builtin::Variant::nil()),
77+
}
78+
}
79+
}
80+
81+
// ----------------------------------------------------------------------------------------------------------------------------------------------
82+
// global::Error impl: GDScript sees the Error enum.
83+
//
84+
// Note: ok_to_mapped discards the Ok value and returns Error::OK. The typical use case is Result<(), global::Error>.
85+
86+
impl<T: ToGodot> ErrorToGodot<T> for global::Error {
87+
type Mapped = global::Error;
88+
89+
fn result_to_godot(result: Result<&T, &Self>) -> CallOutcome<global::Error> {
90+
match result {
91+
Ok(_) => CallOutcome::Return(global::Error::OK),
92+
Err(&e) => CallOutcome::Return(e),
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)