Skip to content

Commit 9480105

Browse files
committed
Support Result<T, E> as return type in #[func]
Adds `ErrorToGodot` trait to customize error mapping strategies. Supports a few common ones out of the box.
1 parent 47f2695 commit 9480105

15 files changed

Lines changed: 833 additions & 18 deletions

File tree

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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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::{GodotConvert, ToGodot};
13+
14+
/// Defines how a Rust error type maps to Godot when used in `Result<T, E>` return types of `#[func]` methods.
15+
///
16+
/// The associated type [`Mapped`][Self::Mapped] determines what GDScript sees as the function's return type. This type
17+
/// can depend on `T` -- the ok-value type of the `Result` -- because the trait is generic over `T`.
18+
///
19+
/// # Usage
20+
/// Users implement the required method [`result_to_godot`][Self::result_to_godot] and set the `Mapped` associated type.
21+
/// Use a blanket impl over `T` to cover all possible ok-types at once.
22+
///
23+
/// For usage examples of built-in strategies, see each type in the [`strat`][crate::meta::error::strat] module.
24+
///
25+
/// ## Custom implementation: typed `Array<T>` with 0 or 1 elements
26+
/// Since the trait is generic over `T`, custom implementations can require tighter bounds (such as
27+
/// [`Element`][crate::meta::Element]) and use a typed `Array<T>` as the via type. This example returns
28+
/// a 1-element array on success and an empty array on error. GDScript sees `Array[T]` as the return type.
29+
///
30+
/// ```no_run
31+
/// # use godot::prelude::*;
32+
/// use godot::builtin::Array;
33+
/// use godot::meta::error::ErrorToGodot;
34+
/// use godot::meta::{Element, GodotConvert, ref_to_arg, ToGodot};
35+
///
36+
/// struct MyError(String);
37+
///
38+
/// impl<T: Element> ErrorToGodot<T> for MyError {
39+
/// // GDScript sees `Array[T]` -- a typed array.
40+
/// type Mapped = Array<T>;
41+
///
42+
/// fn result_to_godot(result: Result<&T, &Self>) -> Result<Array<T>, String> {
43+
/// match result {
44+
/// Ok(val) => Ok(array![ref_to_arg(val)]),
45+
/// Err(_) => Ok(Array::new()),
46+
/// }
47+
/// }
48+
/// }
49+
/// ```
50+
///
51+
/// GDScript usage:
52+
/// ```gdscript
53+
/// var result: Array[int] = node.try_something()
54+
/// if result.is_empty():
55+
/// print("Operation failed")
56+
/// else:
57+
/// var value = result[0]
58+
/// ```
59+
///
60+
/// # Built-in strategies
61+
///
62+
/// See the [`strat`] module for all provided implementations and an overview table.
63+
///
64+
/// [`strat`]: crate::meta::error::strat
65+
///
66+
/// # Fatal errors and calling conventions
67+
///
68+
/// Fatal error types (those where [`result_to_godot`][Self::result_to_godot] returns `Err(msg)` on failure) rely on
69+
/// Godot's varcall mechanism to abort the calling GDScript function. However, Godot internally selects between two
70+
/// calling conventions based on static type information in the script, and this choice is **not controllable from GDExtension**:
71+
///
72+
/// - **Varcall** is used when the caller or arguments are untyped (e.g. `var obj: Variant`).
73+
/// Varcall provides an `r_error` output parameter, which gdext uses to signal call failure. The GDScript VM then
74+
/// aborts the calling function — similar to how a panic would behave.
75+
///
76+
/// - **Ptrcall** is used when both the object and all argument/return types are statically known
77+
/// (e.g. `var obj: MyClass`, `var x: int = obj.my_func()`).
78+
/// Ptrcall has **no** error output parameter, so fatal errors cannot abort the calling function.
79+
/// Instead, the error is logged as a Godot error and a default value is returned.
80+
///
81+
/// In practice, this means that a fatal `Result::Err` will abort the GDScript function in most cases (varcall),
82+
/// but will degrade to a logged error + default return value when GDScript has full type information (ptrcall).
83+
/// This is a limitation of Godot's GDExtension API, not something gdext can work around.
84+
pub trait ErrorToGodot<T: ToGodot>: Sized {
85+
/// The type to which `Result<T, Self>` is mapped on Godot side.
86+
type Mapped: GodotConvert;
87+
88+
/// Map a `Result<T, Self>` to a Godot representation or a fatal error message.
89+
///
90+
/// Return `Ok(via)` to pass the value back to GDScript (the call succeeds), or `Err(message)` to abort the
91+
/// GDScript function call and print an error. The latter is the "fatal" mode.
92+
///
93+
// TODO: replace Result<Via, String> with a dedicated ErrorHandling<Via> enum to communicate intent more clearly:
94+
// Return(Via) — pass this value to GDScript; the call succeeds.
95+
// Abort(String) — abort the GDScript call with this error message.
96+
fn result_to_godot(
97+
result: Result<&T, &Self>,
98+
) -> Result<<Self::Mapped as GodotConvert>::Via, String>;
99+
}

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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.to_godot()` | 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::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>) -> Result<crate::builtin::Variant, String> {
74+
match result {
75+
Ok(val) => Ok(val.to_variant()),
76+
Err(()) => Ok(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(
90+
result: Result<&T, &Self>,
91+
) -> Result<<global::Error as crate::meta::GodotConvert>::Via, String> {
92+
match result {
93+
Ok(_) => Ok(global::Error::OK.to_godot_owned()),
94+
Err(e) => Ok(e.to_godot_owned()),
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)