The purpose of TypedStatus is to provide a thin wrapper around return-value enums that support causality tracking, data attachment, and general assistance with debugging, without adding slowdowns due to returning large structs, pointers, or more complicated types.
A use-every-feature example:
struct MyExampleStatusTraits { // [REQUIRED] Declare your enum enum class Codes : StatusCodeType { kSomething = 9090, kAnotherThing = 92, kAThirdThing = 458, kAFinalThing = 438, }; // [REQUIRED] Declare your group name static constexpr StatusGroupType Group() { return "MyExampleStatus"; } // [OPTIONAL] Declare your "default" code. If this method is defined, // then the function OkStatus() can be used to return a status with this // code. Statuses created with this default code can not have any data, // causes, or a message attached. static constexpr Codes OkEnumValue() { return Codes::kSomething; } // [OPTIONAL] If |OnCreateFrom| is declared, then TypedStatus<T> can be // created with {T::Codes, SomeOtherType} or {T::Codes, string, SomeOtherType} // The pre-created TypedStatus is passed into this method for additional // manipulation. static void OnCreateFrom(TypedStatus<MyExampleStatusTraits>* impl, const SomeOtherType& t) { impl->WithData("key", SomeOtherTypeToString(t)); } // [OPTIONAL] If you'd like to be able to send your status to UKM, declare // this method in your traits. This allows you to pack any part of the // status internal data into a single ukm-ready uint32. static uint32_t PackExtraData(const internal::StatusData& data) { return 0; } }; // Typically, you'd want to redefine your template instantiation, like this. using MyExampleStatus = TypedStatus<MyExampleStatusTraits>;
TypedStatus<T>
All TypedStatus specializations have the following common API:
// The underlying code value. T::Codes code() const; // The underlying message. std::string& message() const; // Adds the current file & line number to the trace. TypedStatus<T>&& AddHere() &&; // Adds some named data to the status, such as a platform // specific error value, ie: HRESULT. This data is for human consumption only // in a developer setting, and can't be extracted from the TypedStatus // normally. The code value should be sufficiently informative between sender // and reciever of the TypedStatus. template<typename D> TypedStatus<T>&& WithData(const char *key, const D& value) &&; template<typename D> void WithData(const char *key, const D& value) &; // Adds a "causal" status to this one. // The type `R` will not be retained, and similarly with the data methods, // `cause` will only be used for human consumption, and cannot be extracted // under normal circumstances. template<typename R> TypedStatus<T>&& AddCause(TypedStatus<R>&& cause) &&; template<typename R> void AddCause(TypedStatus<R>&& cause) &;
If you have an existing enum, and would like to wrap it:
enum class MyExampleEnum : StatusCodeType { kDefaultValue = 1, kThisIsAnExample = 2, kDontArgueInTheCommentSection = 3, };
Define an |TypedStatusTraits|, picking a name for the group of codes: (copying the descriptive comments is not suggested)
struct MyExampleStatusTraits { using Codes = MyExampleEnum; static constexpr StatusGroupType Group() { return "MyExampleStatus"; } static constexpr Codes OkEnumValue() { return Codes::kDefaultValue; } }
Bind your typename:
using MyExampleStatus = media::TypedStatus<MyExampleStatusTraits>;
Use your new type:
MyExampleStatus Foo() { return MyExampleStatus::Codes::kThisIsAnExample; } int main() { auto result = Foo(); switch(result.code()) { case MyExampleStatus::Codes::...: break; ... } }
There are several ways to create a typed status, depending on what data you'd like to encapsulate:
// To create an status with the default OK type, there's a helper function that // creates any type you want, so long as it actually has a kOk value or |OkEnumValue| implementation. TypedStatus<MyType> ok = OkStatus(); // A status can be implicitly created from a code TypedStatus<MyType> status = MyType::Codes::kMyCode; // A status can be explicitly created from a code and message, or implicitly // created from a brace initializer list of code and message TypedStatus<MyType> status(MyType::Codes::kMyCode, "MyMessage"); TypedStatus<MyType> status = {MyType::Codes::kMyCode, "MyMessage"}; // If |MyType::OnCreateFrom<T>| is implemented, then a status can be created // from a {code, T} pack, or a {code, message, T} pack: TypedStatus<MyType> status = {MyType::Codes::kMyCode, 667}; TypedStatus<MyType> status = {MyType::Codes::kMyCode, "MyMessage", 667}; // A status can be created from packs of either {code, TypedStatus<Any>} or // {code, message, TypedStatus<Any>} where TypedStatus<Any> will become the // status that causes the return. Note that in this example, // OtherType::Codes::kOther is itself being implicitly converted from a code // to a TypedStatus<OtherType>. TypedStatus<MyType> status = {MyType::Codes::kCode, OtherType::Codes::kOther}; TypedStatus<MyType> status = {MyType::Codes::kCode, "M", OtherType::Codes::kOther};
For the common case where you‘d like to return some constructed thing OR an error type, we’ve also created TypedStatus<T>::Or<D>
.
The TypedStatus<T>::Or<D>
type can be constructed implicitly with either a TypedStatus<T>
, a T
, or a D
.
This type has methods:
bool has_value() const; // Return the error, if we have one. // Callers should ensure that this `!has_value()`. TypedStatus<T> error() &&; // Return the value, if we have one. // Callers should ensure that this `has_value()`. OtherType value() &&; // It is invalid to call `code()` on an `Or<D>` type when // has_value() is true and TypedStatusTraits<T>::OkEnumValue is nullopt. T::Codes code();
Example usage:
MyExampleStatus::Or<std::unique_ptr<VideoDecoder>> CreateAndInitializeDecoder() { std::unique_ptr<VideoDecoder> decoder = decoder_factory_->GiveMeYourBestDecoder(); auto init_status = decoder->Initialize(init_args_); // If the decoder initialized successfully, then just return it. if (init_status == InitStatusCodes::kOk) return std::move(decoder); // Otherwise, return a MediaExampleStatus caused by the init status. return MyExampleStatus(MyExampleEnum::kDontArgueInTheCommentSection).AddCause( std::move(init_status)); } int main() { auto result = CreateAndInitializeDecoder(); if (result.has_value()) decoder_loop_->SetDecoder(std::move(result).value()); else logger_->SendError(std::move(result).error()); }
There are some helper matchers defined in test_helpers.h that can help convert some of the trickier method expectations. For example, this:
EXPECT_CALL(object_, Foo(kExpectedCode));
becomes:
EXPECT_CALL(object_, Foo(HasStatusCode(kExpectedCode)));
The EXPECT_CALL macro won't test for overloaded operator== equality here, so |HasStatusCode| is a matcher macro that allows checking if the expected status has the matching error code.
If you want to send a specialization of TypedStatus over mojo, add the following to media_types.mojom:
struct MyExampleEnum { StatusBase? internal; };
And add the following to media/mojo/mojom/BUILD.gn near the StatusData
type binding.
{ mojom = "media.mojom.MyExampleEnum", cpp = "::media::MyExampleEnum" },
TypedStatus is designed to be easily reported to UKM. A status is represented by 16-bit hash of the group name, the 16-bit code, and 32 bits of extra data. Any implementation of TypedStatus can define a |PackExtraData| method in the traits struct which can operate on internal data and pack it into 32 bits. For example, a TypedStatus which might often have wrapped HRESULTs might look like this:
struct MyExampleStatusTraits { // If you do not have an existing enum, you can `enum class Codes { ... };` // here, instead of `using`. using Codes = MyExampleEnum; static constexpr StatusGroupType Group() { return "MyExampleStatus"; } static constexpr Codes OkEnumValue() { return Codes::kDefaultValue; } static uint32_t PackExtraData(const StatusData& info) { absl::optional<int> hresult = info.data.GetIntValue("HRESULT"); return static_cast<uint32_t>(hresult.has_value() ? *hresult : 0); } }
See go/typedstatus for design decisions.