13 KiB
Ox Docs
Systems
Error Handling
The GBA build has exceptions disabled.
Instead of throwing exceptions, all engine code must return ox::Error
s.
For the sake of consistency, try to stick to ox::Error
in non-engine code
as well, but non-engine code is free to use exceptions when they make sense.
Nostalgia and Ox both use ox::Error
to report errors. ox::Error
is
a struct that has overloaded operators to behave like an integer error code,
plus some extra fields to enhance debuggability.
If instantiated through the OxError(x)
macro, it will also include the
file and line of the error.
The OxError(x)
macro should only be used for the initial instantiation of
an ox::Error
.
In addition to ox::Error
there is also the template ox::Result<T>
.
ox::Result
simply wraps the type T value in a struct that also includes
error information, which allows the returning of a value and an error without
resorting to output parameters.
If a function returns an ox::Error
or ox::Result
it should be
declared as noexcept
and all exceptions should be translated to an
ox::Error
.
ox::Result
can be used as follows:
ox::Result<int> foo(int i) noexcept {
if (i < 10) {
return i + 1; // implicitly calls ox::Result<T>::Result(T)
}
return OxError(1); // implicitly calls ox::Result<T>::Result(ox::Error)
}
int caller1() {
auto v = foo(argc);
if (v.error) {
return 1;
}
std::cout << v.value << '\n';
return 0;
}
int caller2() {
// it is also possible to capture the value and error in their own variables
auto [val, err] = foo(argc);
if (err) {
return 1;
}
std::cout << val << '\n';
return 0;
}
ox::Error caller3(int &i) {
return foo(i).moveTo(i);
}
ox::Error caller4(int &i) {
return foo(i).copyTo(i);
}
int caller5(int i) {
return foo(i).unwrap(); // unwrap will kill the program if there is an error
}
int caller6(int i) {
return foo(i).unwrapThrow(); // unwrap will throw if there is an error
}
int caller7(int i) {
return foo(i).or_value(0); // will return 0 if foo returned an error
}
ox::Result<uint64_t> caller8(int i) {
return foo(i).to<uint64_t>(); // will convert the result of foo to uint64_t
}
Lastly, there are a few macros available to help in passing ox::Error
s
back up the call stack, oxReturnError
, oxThrowError
, and
oxRequire
.
oxReturnError
is by far the more helpful of the two.
oxReturnError
will return an ox::Error
if it is not 0 and
oxThrowError
will throw an ox::Error
if it is not 0.
Because exceptions are disabled for GBA builds and thus cannot be used in the
engine, oxThrowError
is only really useful at the boundary between
engine libraries and Nostalgia Studio.
Since ox::Error
is always nodiscard, you must do something with them.
In rare cases, you may not have anything you can do with them or you may know
the code will never fail in that particular instance.
This should be used sparingly.
void studioCode() {
auto [val, err] = foo(1);
oxThrowError(err);
doStuff(val);
}
ox::Error engineCode() noexcept {
auto [val, err] = foo(1);
oxReturnError(err);
doStuff(val);
return {};
}
void anyCode() {
auto [val, err] = foo(1);
std::ignore = err;
doStuff(val);
}
Both macros will also take the ox::Result
directly:
void studioCode() {
auto valerr = foo(1);
oxThrowError(valerr);
doStuff(valerr.value);
}
ox::Error engineCode() noexcept {
auto valerr = foo(1);
oxReturnError(valerr);
doStuff(valerr.value);
return {};
}
Ox also has the oxRequire
macro, which will initialize a value if there is no error, and return if there is.
It aims to somewhat emulate the ?
operator in Rust and Swift.
Rust ?
operator:
fn f() -> Result<i32, i32> {
// do stuff
}
fn f2() -> Result<i32, i32> {
let i = f()?;
Ok(i + 4)
}
oxRequire
:
ox::Result<int> f() noexcept {
// do stuff
}
ox::Result<int> f2() noexcept {
oxRequire(i, f()); // const auto [out, oxConcat(oxRequire_err_, __LINE__)] = x; oxReturnError(oxConcat(oxRequire_err_, __LINE__))
return i + 4;
}
oxRequire
is not quite as versatile, but it should still cleanup a lot of otherwise less ideal code.
oxRequire
also has variants for throwing the error and for making to value non-const:
oxRequireM
- oxRequire MutableoxRequireT
- oxRequire ThrowoxRequireMT
- oxRequire Mutable Throw
The throw variants of oxRequire
are generally legacy code.
ox::Result::unwrapThrow
is generally preferred now.
Logging and Output
Ox provides for logging and debug prints via the oxTrace
, oxDebug
, and oxError
macros.
Each of these also provides a format variation.
Ox also provide oxOut
and oxErr
for printing to stdout and stderr.
These are intended for permanent messages and always go to stdout and stderr.
Tracing functions do not go to stdout unless the OXTRACE environment variable is set. They also print with the channel that they are on, along with file and line.
Debug statements go to stdout and go to the logger on the "debug" channel.
Where trace statements are intended to be written with thoughtfulness,
debug statements are intended to be quick and temporary insertions.
Debug statements trigger compilation failures if OX_NODEBUG is enabled when CMake is run,
as it is on Jenkins builds, so oxDebug
statements should never be checked in.
This makes oxDebug
preferable to other forms of logging, as temporary prints should
never be checked in.
oxError
always prints.
It includes file and line, and is prefixed with a red "ERROR:".
It should generally be used conservatively.
It shuld be used only when there is an error that is not technically fatal, but
the user almost certainly wants to know about it.
oxTrace
and oxTracef
:
void f(int x, int y) { // x = 9, y = 4
oxTrace("nostalgia.core.sdl.gfx") << "f:" << x << y; // Output: "f: 9 4"
oxTracef("nostalgia.core.sdl.gfx", "f: {}, {}", x, y); // Output: "f: 9, 4"
}
oxDebug
and oxDebugf
:
void f(int x, int y) { // x = 9, y = 4
oxDebug() << "f:" << x << y; // Output: "f: 9 4"
oxDebugf("f: {}, {}", x, y); // Output: "f: 9, 4"
}
oxError
and oxErrorf
:
void f(int x, int y) { // x = 9, y = 4
oxError() << "f:" << x << y; // Output: "ERROR: (<file>:<line>): f: 9 4"
oxErrorf("f: {}, {}", x, y); // Output: "ERROR: (<file>:<line>): f: 9, 4"
}
Model System
Ox has a model system that provides a sort of manual reflection mechanism.
Models require a model function for the type that you want to model. It is also good to provide a type name and type version number, though that is not required.
The model function takes an instance of the type it is modelling and a template parameter type. The template parameter type must implement the API used in the models, but it can do anything with the data provided to it.
Here is an example from the Nostalgia/Core package:
struct NostalgiaPalette {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaPalette";
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors;
};
struct NostalgiaGraphic {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
static constexpr auto TypeVersion = 1;
int8_t bpp = 0;
// rows and columns are really only used by TileSheetEditor
int rows = 1;
int columns = 1;
ox::FileAddress defaultPalette;
NostalgiaPalette pal;
ox::Vector<uint8_t> pixels;
};
template<typename T>
constexpr ox::Error model(T *h, ox::CommonPtrWith<NostalgiaPalette> auto *pal) noexcept {
h->template setTypeInfo<NostalgiaPalette>();
// it is also possible to provide the type name and type version as function arguments
//h->setTypeInfo("net.drinkingtea.nostalgia.core.NostalgiaPalette", 1);
oxReturnError(h->field("colors", &pal->colors));
return {};
}
template<typename T>
constexpr ox::Error model(T *h, ox::CommonPtrWith<NostalgiaGraphic> auto *ng) noexcept {
h->template setTypeInfo<NostalgiaGraphic>();
oxReturnError(h->field("bpp", &ng->bpp));
oxReturnError(h->field("rows", &ng->rows));
oxReturnError(h->field("columns", &ng->columns));
oxReturnError(h->field("defaultPalette", &ng->defaultPalette));
oxReturnError(h->field("pal", &ng->pal));
oxReturnError(h->field("pixels", &ng->pixels));
return {};
}
The model system also provides for unions:
#include <ox/model/types.hpp>
class FileAddress {
template<typename T>
friend constexpr Error model(T*, ox::CommonPtrWith<FileAddress> auto*) noexcept;
public:
static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress";
union Data {
static constexpr auto TypeName = "net.drinkingtea.ox.FileAddress.Data";
char *path;
const char *constPath;
uint64_t inode;
};
protected:
FileAddressType m_type = FileAddressType::None;
Data m_data;
};
template<typename T>
constexpr Error model(T *h, ox::CommonPtrWith<FileAddress::Data> auto *obj) noexcept {
h->template setTypeInfo<FileAddress::Data>();
oxReturnError(h->fieldCString("path", &obj->path));
oxReturnError(h->fieldCString("constPath", &obj->path));
oxReturnError(h->field("inode", &obj->inode));
return {};
}
template<typename T>
constexpr Error model(T *io, ox::CommonPtrWith<FileAddress> auto *fa) noexcept {
io->template setTypeInfo<FileAddress>();
// cannot read from object in Reflect operation
if constexpr(ox_strcmp(T::opType(), OpType::Reflect) == 0) {
int8_t type = 0;
oxReturnError(io->field("type", &type));
oxReturnError(io->field("data", UnionView(&fa->m_data, 0)));
} else {
auto type = static_cast<int8_t>(fa->m_type);
oxReturnError(io->field("type", &type));
fa->m_type = static_cast<FileAddressType>(type);
oxReturnError(io->field("data", UnionView(&fa->m_data, static_cast<int>(fa->m_type))));
}
return {};
}
There are also macros in <ox/model/def.hpp>
for simplifying the declaration of models:
oxModelBegin(NostalgiaGraphic)
oxModelField(bpp)
oxModelField(rows)
oxModelField(columns)
oxModelField(defaultPalette)
oxModelField(pal)
oxModelField(pixels)
oxModelEnd()
Serialization
Using the model system, Ox provides for serialization. Ox has MetalClaw and OrganicClaw as its serialization format options. MetalClaw is a custom binary format designed for minimal size. OrganicClaw is a wrapper around JsonCpp, chosen because it technically implements a superset of JSON. OrganicClaw requires support for 64 bit integers, whereas normal JSON technically does not.
These formats do not currently support floats.
There is also a wrapper format called Claw that provides a header at the beginning of the file and can dynamically switch between the two depending on what the header says is present. The Claw header also includes information about the type and type version of the data.
Claw header: M1;net.drinkingtea.nostalgia.core.NostalgiaPalette;1;
That reads:
- Format is Metal Claw, version 1
- Type ID is net.drinkingtea.nostalgia.core.NostalgiaPalette
- Type version is 1
Except when the data is exported for loading on the GBA, Claw is always used as a wrapper around the bare formats.
Metal Claw Example
Read
#include <ox/mc/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(ox::BufferView const&buff) noexcept {
return ox::readMC<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(ox::BufferView const&buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readMC(buff, pal));
return pal;
}
Write
#include <ox/mc/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette1(NostalgiaPalette const&pal) noexcept {
ox::Buffer buffer(ox::units::MB);
std::size_t sz = 0;
oxReturnError(ox::writeMC(buffer.data(), buffer.size(), pal, &sz));
buffer.resize(sz);
return buffer;
}
ox::Result<ox::Buffer> writeSpritePalette2(NostalgiaPalette const&pal) noexcept {
return ox::writeMC(pal);
}
Organic Claw Example
Read
#include <ox/oc/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(ox::BufferView const&buff) noexcept {
return ox::readOC<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(ox::BufferView const&buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readOC(buff, &pal));
return pal;
}
Write
#include <ox/oc/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette1(NostalgiaPalette const&pal) noexcept {
ox::Buffer buffer(ox::units::MB);
oxReturnError(ox::writeOC(buffer.data(), buffer.size(), pal));
return buffer;
}
ox::Result<ox::Buffer> writeSpritePalette2(NostalgiaPalette const&pal) noexcept {
return ox::writeOC(pal);
}
Claw Example
Read
#include <ox/claw/read.hpp>
ox::Result<NostalgiaPalette> loadPalette1(ox::BufferView const&buff) noexcept {
return ox::readClaw<NostalgiaPalette>(buff);
}
ox::Result<NostalgiaPalette> loadPalette2(ox::BufferView const&buff) noexcept {
NostalgiaPalette pal;
oxReturnError(ox::readClaw(buff, pal));
return pal;
}
Write
#include <ox/claw/write.hpp>
ox::Result<ox::Buffer> writeSpritePalette(NostalgiaPalette const&pal) noexcept {
return ox::writeClaw(pal);
}