TypedStatus

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 DefaultEnumValue() { 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>;

Using an existing 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) &;

Quick usage guide

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 DefaultEnumValue() { 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;
    ...
  }
}

Constructing a TypedStatus

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
|DefaultEnumValue| 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};

TypedStatus::Or

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>::DefaultEnumValue 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());
}

Testing

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.

Additional setup for mojo

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"
},

UKM & data-recording

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 DefaultEnumValue() { 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);
  }
}

Design decisions

See go/typedstatus for design decisions.