Error Handling
Luna SDK does not use the exception mechanism provided by C++. Instead, it adopts a more light-weight error-handling mechanism by returning error codes. Compared to other error-code based solutions, Luna SDK manages error codes and the relationship between error codes, so the user can extend the error handling mechanism easily.
Error code
#include <Luna/Runtime/Error.hpp>
ErrCode
represents one error code, which is a machine-sized unsigned integer (usize
). ErrCode
is defined as a dedicated structure type to distinguish from normal return values, the actual error code value can be fetched by code
property of ErrCode
. We use error code 0
to represent a successful operation (no error), and any non-zero error code value represents one error.
The error code value is not defined directly. Instead, the user should call get_error_code_by_name
to fetch the error code for one specific error. The error code is generated by the system on the first call to get_error_code_by_name
, and is cached and returned directly on succeeding calls to get_error_code_by_name
with the same arguments. The error code for the same error will change in different processes, so do not store the error code directly, store its name and category (which will be explained in the following section) instead.
Error name and category
#include <Luna/Runtime/Error.hpp>
Every ErrCode
is described by two properties: error name and error category, which is required when calling get_error_code_by_name
, and can be fetched by get_error_code_name
and get_error_code_category
. Error name is a UTF-8 string that briefly describes the error, while error category is used to hold one set of error codes in the same domain. For example, the Runtime
module of Luna SDK defines one error category called BasicError
, which contains error codes like bad_arguments
, out_of_memory
, not_supported
, etc.
The error category is represented by errcat_t
, and is identified by one UTF-8 name string. You can get errcat_t
from its name by calling get_error_category_by_name
, and get the name of one errcat_t
by calling get_error_category_name
. Error categories can also contain sub-categories, for example, BasicError
may contains one IOError
sub-category that contains all error codes related to IO errors. In such case, the error category name and sub-category name should both be specified for sub-categories, separated by double colons (::
), like BasicError::IOError
.
You can call get_all_error_codes_of_category
to get all error codes of one specific error category, and get_all_error_subcategories_of_category
to get all error sub-categories of one specific error category.
Declaring error codes
Error codes can be declared by specifying error categories as namespaces, and error codes as functions that return the corresponding ErrCode
instances. All error categories should be declared directly in Luna
namespace. Every error category should have one errtype
function that returns the errcat_t
instance of the specified error category.
namespace Luna
{
namespace MyError
{
//! Gets the error category object of `MyError`.
LUNA_MYMODULE_API errcat_t errtype();
LUNA_MYMODULE_API ErrCode my_error_1();
LUNA_MYMODULE_API ErrCode my_error_2();
LUNA_MYMODULE_API ErrCode my_error_3();
//...
namespace MySubError
{
//! Gets the error category object of `MySubError`.
LUNA_MYMODULE_API errcat_t errtype();
LUNA_MYMODULE_API ErrCode my_error_4();
LUNA_MYMODULE_API ErrCode my_error_5();
//...
}
}
}
When implementing such functions, you may use static local variables to prevent fetching the error code every time it is called:
namespace Luna
{
namespace MyError
{
LUNA_MYMODULE_API errcat_t errtype()
{
static errcat_t e = get_error_category_by_name("MyError");
return e;
}
LUNA_MYMODULE_API ErrCode my_error_1();
{
static ErrCode e = get_error_code_by_name("MyError", "my_error_1");
return e;
}
//...
namespace MySubError
{
LUNA_MYMODULE_API errcat_t errtype()
{
static errcat_t e = get_error_category_by_name("MyError::MySubError");
return e;
}
LUNA_MYMODULE_API ErrCode my_error_4()
{
static ErrCode e = get_error_code_by_name("MyError::MySubError", "my_error_4");
return e;
}
//...
}
}
}
Built-in errors
Runtime/Error.hpp
contains a list of error codes that covers most common error types, like bad_arguments
, bad_platform_call
, out_of_memory
, not_found
, already_exists
, etc. All these error codes are declared in BasicError
error category, and can be used directly.
Besides error codes in BasicError
, some built-in modules of Luna SDK declare their own error codes. For example, RHI
module declares device_lost
in RHIError
error category to indicate one graphic device removal error. You can check module documentations and interface files for error codes defined by such modules.
Result object
#include <Luna/Runtime/Result.hpp>
To represent one function that may throw errors, you should wrap the return type of the function with the result object typeR<T>
, which encapsulates the returned value of the function as well as one error code. The result object can be constructed by passing normal return values (which indicates a successful function call) or error codes (which indicates one error). If the result object is constructed by error, its result object will not be initialized.
The following example shows how to declare and implement one function that may throw errors:
R<u64> get_file_size(File* file)
{
u64 size;
BOOL succeeded = system_get_file_size(file, &size);
if(succeeded) return size; // Return the return value means success.
else return BasicError::bad_platform_call(); // Return the error code means failure.
}
If the return type of the function is R<void>
, you can return ok
to indicate one successful function call. Note that using ok
is allowed only if the function return value is R<void>
. You can also use RV
to replace of R<void>
for convenience.
RV reset_file_cursor(File* file)
{
BOOL succeeded = system_reset_file_cursor(file);
if(succeeded) return ok;
else return BasicError::bad_platform_call();
}
On the caller side, we can use succeeded
and failed
to test whether one result object represents one valid return value or one error code:
auto res = reset_file_cursor(file);
if(failed(res))
{
// Gets the error code stored in `R<T>`.
ErrCode err = res.errcode();
// Handle the error.
// ...
}
Error objects
#include <Luna/Runtime/Error.hpp>
Error codes indicate only the type of the error, without any further information, which can be inconvenient for the user to indicating the error. For such purpose, Luna SDK provides error objects that extend error codes to provide more detailed information about the error.
One error object is represented by Error
and contains three members:
code
: The error code.message
: One UTF-8 short description of the error.info
: OneVariant
that may contain any additional error information provided.
To return one error object instead of one error code, first set the error object by calling get_error
, then returns BasicError::error_object
as the returned error code of the function:
Error& err = get_error();
err = Error(BasicError::not_found(), "The specified file %s is not found.", file_name);
return BasicError::error_object();
The error object fetched by get_error
is a thread-local object attached to the current thread, so error objects in different threads are independent to each other. If you want to pass error objects between different threads, you can always store one Error
instance down and pass it using your own methods.
You can also use set_error
to simplify the process of creating and returning error objects. The above code can be rewritten by:
return set_error(BasicError::not_found(), "The specified file %s is not found.", file_name);
set_error
always returns BasicError::error_object
, so we can return it directly.
On the caller side, if we find the error code of one function is BasicError::error_object
, we should retrieve the real error code by checking the same error object set by the calling function:
auto res = do_something();
if(failed(res))
{
ErrCode err = res.errcode();
if(err == BasicError::error_object())
{
err = get_error().code;
}
// Handle the error.
// ...
}
We can use unwrap_errcode
to simplify this process and retrieve the error code directly like so:
auto res = do_something();
if(failed(res))
{
ErrCode err = unwrap_errcode(res);
// Handle the error.
// ...
}
unwrap_errcode
will retrieve the error code from R<T>
result object, and if the error code is BasicError::error_object
, it will then retrieve the real error code automatically from the error object of this thread.
We can also call explain
to fetch the message stored on the error object if the error code is BasicError::error_object
, or the name of the error code if not:
auto res = do_something();
if(failed(res))
{
debug_printf("%s", explain(res.errcode()));
}
Try-catch macros for error handling
#include <Luna/Runtime/Result.hpp>
Correctly handling functions that may throw errors requires a lot of if
statements to judge whether every function call is successful, which takes a lot of effort. In order to ease this, Luna SDK provides macros that can be used to handle throwable functions using a try-catch syntax, much like those in C++.
In order to catch error codes returned by throwable function, we firstly need to declare one pair of try-catch blocks using lutry
and lucatch
like so:
lutry
{
}
lucatch
{
}
lutry
block is the place where throwable functions are called. In this block, throwable functions are wrapped by luexp
, lulet
and luset
macros:
luexp
is used if the return type of the function isR<void>
.lulet
creates a new local variable to hold the return value of the function.luset
assigns the return value of the function to one existing variable.
The user can also use luthrow
to throw one directly. The following code shows the usage of these four macros:
lutry
{
luexp(do_something_that_may_fail());
lulet(size, get_file_size(file)); // Creates one new local variable `size`.
luexp(set_file_size(file, 1024));
u64 new_size;
luset(new_size, get_file_size(file)); // Assigns to one existing variable `new_size`.
if(new_size != 1024)
{
luthrow(BasicError::bad_platform_call()); // Throw errors directly.
}
}
lucatch
{
//...
}
For all these four macros, if the calling function or luthrow
throws errors, the execution flow will be interrupted and redirected to lucatch
block by a internal goto
jump. In lucatch
block, the user should handle the error, or just return the error to the caller function. lures
macro is used in this block to represent the error code.
lucatch
{
ErrCode code = unwrap_errcode(lures); // To fetch the real error code if lures is `BasicError::error_object`.
if(code == BasicError::bad_platform_call())
{
// Do something...
}
else if(code == BasicError::bad_arguments())
{
// Do something...
}
else return lures; // Forward the error to caller function if the error cannot be handled here.
}
If the user does not want to handle errors at all, she can use lucatchret
instead of lucatch
block, which will forward all errors caught to the caller function:
lutry
{
//...
}
lucatchret; // Return all errors caught.
In most cases, only one lutry
-lucatch
pair is needed for one function. If you need multiple lutry
-lucatch
pairs in the same function, add suffix numbers to macros of succeeding lutry
-lucatch
pairs after the first pair like so:
RV func()
{
// First pair.
lutry
{
luexp(...);
lulet(a, ...);
luset(a, ...);
luthrow(...);
}
lucatch
{
return lures;
}
// Another pair.
lutry2
{
luexp2(...);
lulet2(a, ...);
luset2(a, ...);
luthrow2(...);
}
lucatch2
{
return lures2;
}
// Another pair.
lutry3
{
luexp3(...);
lulet3(a, ...);
luset3(a, ...);
luthrow3(...);
}
lucatch3
{
return lures3;
}
}