Description
In order to support Result, we should first define a GdError
enum that implements std::error::Error
. It could replace engine::global::Error
but I think it would probably make more sense to have our own enum so it's extensible. Ideally this would contain engine error as one option:
#[non_exhaustive]
pub enum GdError {
Engine(engine::global::Error), // explicitly returned errors from Godot
ArgMarshaling(MarshalingErrorDetails), // variant argument marshaling error
ReturnMarshaling(MarshalingErrorDetails), // variant return value marshaling error
ScriptError(ScriptErrorDetails), // an error occurred within GdScript
}
// impl std::error Error for GdError
// impl Display, etc
// impl From<> for the various internal types
Some of these *Details
structs may just be unit types if there's no details available right now (Godot apparently doesn't have a GetLastScriptError API for instance, but would leave the door open for improvement in the future). It may also make sense to make multiple error enums for functions where only some of these are possible (perhaps GdCallError should differ for instance, YMMV) but the idea is the same.
Once this is defined, it would be ideal to have a policy of having a function which returns Result whenever an operation is failable. In particular, anything that triggers GDScript (inline script only, deferred script calls need not offer this guarantee) may potentially fail due to a script error and surfacing that failure to calling code is important to be able to enforce invariants. Only the calling code knows whether that error should be propagated or if it's safe to eat it, or perhaps handle it in some custom manner (possibly custom to that particular call site).
To that end, the following calls are proposed to extend the existing interface:
Object::try_call
,Object::try_get
,Object::try_set
Callable::try_callv
PackedScene::try_instantiate(&self)
- If
Object::emit_signal
is inline, it should have a try, but I think it's deferred (not sure). - There's probably more candidates but these seem like the majority of the ways Rust invokes gdscript and would represent a great start IMHO.
These should all return the same value as the regular non-try function in the Ok
case and GdError
in the Err
case. To make sure the code paths get exercise, the non-try variant could be implemented in terms of the try_* variant with .unwrap()
.
The above changes would allow Rust code to determine when scripts fail. To compliment this, it would also be good to accept Rust functions exposed to Godot that return values of the form Result<T : ToGodot, E : std::error::Error>
. When called via Godot, this would simply return the Ok
type or behave in the same was as though there was a trapped panic (in today's behavior) in the event of an Err
.
#[godot_api]
impl MyStruct {
#[func]
pub fn foo(&self) -> Result<(),GdError> {
self.base.try_call(StringName::from("bar"), &[])?; // propagate any script errors to the calling script
Ok(())
}
}
This seems like trivial behavior, but it has a few advantages:
- Not all callsites for marshaled functions necessarily come from Godot. Rust callsites can still freely handle the Result so enabling that function signature would have ergonomic value.
- It means that in the fairly common case where Godot calls into Rust which then calls back into Godot (via call/set/get/etc), the Rust function can propagate the
try_*
error results with the try operator (?
) if it doesn't want to do specific handling. - The generated glue no longer needs to trap panics in Rust functions that return Result since there's already a mechanism for propagating failures. panic trapping can remain the default for other function signatures but this is consistent with Rust's pay-for-what-you-use paradigm.