Understand generated code

All code examples in this section assume you have defined a MyComponents function as described in Providing components, and set up the following preamble:

#include <improbable/standard_library.h>
#include <improbable/worker.h>
#include <example.h>

using namespace improbable;

The C++ schema-generated code consists of two main parts: data classes and component classes.

Data classes

Data classes are used to represent data at rest and correspond to the schemalang type definitions.

A data class is generated for each type defined in the schema, with fields corresponding
to each field in the schema definition.

Data representation

  • Strings are represented as UTF-8 encoded std::string members. Make sure to use this same encoding when updating a component.
  • list<T> fields are represented as a worker::List<T> of the repeated type. This type is very similar to std::vector<T> and has most of the methods you'd expect.
  • map<Key, Value> fields are represented as worker::Map<Key, Value>. This type is very similar to std::unordered_map<Key, Value> and has most of the methods you'd expect. The biggest difference is it has a well-defined iteration order, which is insertion order.
  • option<T> fields are represented as worker::Option<T>.
    First, check if the option contains a value with !option.empty() or if (option) { ... }, then dereference the option to access the contained value. This type is very similar to the std::optional<T> type introduced in C++17.

Component classes

Component classes correspond to schemalang component definitions and contain metadata and classes for sending and receiving component data (updates, command requests and command responses).

For each component, we generate:

  • a metaclass.

    Metaclasses are used when referring to specific components when using the API - every template parameter T expects a metaclass argument.

  • an Update class nested inside the metaclass.

    This has an optional field for each field in the component (since it represents a diff).

  • command metaclasses nested inside the metaclass, as SomeComponent::Commands::SomeCommand.

    These work in a similar way to component metaclasses, but for command-related functions.

Schema bundle files

The schema compiler can generate a C++ header file which contains:

  • A type alias SchemaComponents which is a worker::Components type specialized with all of your components
  • A function GetSchemaComponents() which returns a worker::ComponentRegistry& containing your components
  • A C++ MakeSchemaBundle() helper function that creates a worker::SchemaBundle you can use for JSON (de-)serialization of schema types

The generated worker::Components type eliminates the need to provide this type yourself. You might still want to provide it yourself if you only use a subset of your schema types and you want to limit dependencies on schema types. This can help to keep compilation times low. Alternatively, you could generate this type only for a subset of your schema files.

For JSON (de-)serialization of schema types you need access to a binary schema bundle. You have several options here. Either pass those results to worker::SchemaBundle::Create or use the generated convenience function MakeSchemaBundle which does that for you. See JSON representation of Schema_ objects.

You can generate this code by invoking the schema_compiler with the --cpp_bundle_out=<dir> parameter. This generates both a C++ header and source file in the specified directory which contain a couple of helper types and functions for all the .schema files you gave as input to the schema compiler.

The C++ header file contains these declarations:

namespace worker {
using __BUNDLE__Components = worker::Components<...>;
const worker::ComponentRegistry& Get__BUNDLE__Components();
SchemaBundle Make__BUNDLE__Bundle();
} // ::worker

__BUNDLE__ is replaced with the bundle name you specified using the --cpp_bundle_name=<name> parameter. Note that this has to be a valid C++ identifier. The default name is Schema. Changing the name is useful if you want to generate these helpers for multiple subsets of your schema.

You can control the base name of the generated header and source file with the
--cpp_bundle_file_name=<name> parameter. --cpp_source_extension=<extension> controls the C++ source file's extension.

The generated C++ header file expects the #include files for all supplied .schema files to be available on the #include path. This means that you must run --cpp_out for all the .schema files that you gave as input to the schema compiler (when you used --cpp_bundle_out), and specify the same --schema_path as when you used --cpp_bundle_out.

Note that using this feature prevents you from using the benefits of caching to a certain degree, because you always need to regenerate the bundle when one of the .schema files changes. In some use cases it could make sense to split up your C++ schema bundle into two or more bundles, for example to separate schema types that change often from those that are stable, to keep compilation times low.

Example

Given the following simple schema (in package example):

type StatusEffect {
  string name = 1;
  int32 multiplier = 2;
}
 
component Creature {
  id = 12345;
  int32 health = 1;
  list<StatusEffect> effects = 2;
}

The generated code will contain a data class StatusEffect corresponding to type defined in the schema. For the creature component, a Creature metaclass containing a Creature::Update class will be generated, alongside a CreatureData data class.

The Creature metaclass can be passed as a template parameter to any templated API function, for example to send an update for the creature component. Note that the Creature metaclass is also used with the worker::Entity class (which the View uses to represent entities) to inspect component data "at rest".

Here is an example of how these classes can be used with the API:

void GeneratedCodeExamples(worker::Connection& connection) {
  worker::Dispatcher dispatcher{MyComponents()};
 
  dispatcher.OnAuthorityChange<example::Creature>(
      [&connection](const worker::AuthorityChangeOp& op) {
        if (op.Authority == worker::Authority::kAuthoritative) {
          // We were granted authority over the creature component. Send an update.
          example::Creature::Update update;
          update.set_health(10);
          connection.SendComponentUpdate<example::Creature>(op.EntityId, update);
        } else {
          // Authority was revoked.
        }
      });
 
  dispatcher.OnComponentUpdate<example::Creature>(
      [](const worker::ComponentUpdateOp<example::Creature>& op) {
        const example::Creature::Update& update = op.Update;
        if (update.effects()) {
          auto effects = update.effects().data();
          for (const auto& effect : *effects) {
            std::cout << op.EntityId << " has effect " << effect.name() << " with multiplier "
                      << effect.multiplier() << std::endl;
          }
        }
      });
}

Updated about a year ago


Understand generated code


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.