Sites

Menu

Serialization reference

Unlike the Worker SDK in C++, C# and Java, which provides generated classes that you can make use of to interact with schema in your workers, the Worker SDK C# bindings leave code generation completely up to you. To let you do this, they expose a serialization library. This page explains the two different methods of providing serialization code, and provides a reference of how to serialize various schema types.

Handle types

For each component known to a particular worker, the Worker SDK C# bindings make use of 4 different data handle types:

  • ComponentData - Represents a component at a single point in time. All fields (except for options, lists, and maps) need to be explicitly set.
  • ComponentUpdate - Represents a component update. Can contain any number of fields from the component, as it represents a diff of some component data between two points of time.
  • CommandRequest - Represents some command request data. Should contain all fields of the command request type.
  • CommandResponse - Represents some command response data. Should contain all fields of the command response type.

In the higher level languages of the Worker SDK, these handle types are hidden (because they’re dealt with by the schema compiler); for the C# bindings, they’re fully exposed. It’s up to you to serialize the above data (either directly when triggering an operation or by providing vtable functions, as described in the next section).

Serialized handles

The four handle types are serialized into objects of another type following the Schema* naming scheme, i.e. ComponentUpdate is serialized into a SchemaComponentUpdate object which also contains methods for creating shallow copies, clearing fields etc. All Schema* objects have at least one method returning a SchemaObject which represents the lowest level of the serialization library and contains the actual serialized data and the corresponding methods to read and write that data.

Specifying serialization code

All 4 handle types have the following structure (using ComponentUpdate as an example):

public struct ComponentUpdate
{
  public uint ComponentId;
  public SchemaComponentUpdate? SchemaData;
  public UIntPtr UserHandle;
}

When using any handle type for any reason (for example, sending a component update), you are given the choice between either directly creating a serialized object (SchemaData), or passing in your own custom handle (UserHandle), which will later get serialized by a vtable function.

Direct serialization

Using this method, you perform the serialization directly when performing an operation (for example, sending a component update). This is the simplest way to serialize and deserialize data. To enable this, you need to specify a default vtable. The default vtable applies to all components and simply passes the serialized objects through:

  ConnectionParameters parameters = new ConnectionParameters();
  parameters.ComponentVtables = null;
  parameters.DefaultComponentVtable = new ComponentVtable();

Serializing a ComponentUpdate is required to, e.g., call Connection.SendComponentUpdate. A ComponentUpdate has two constructors. To serialize directly, use the constructor taking a Schema* object as described above. This will set the SchemaData member but leave the UserHandle as null, and thus instruct the SDK not to use vtables. The SDK will take ownership of the SchemaData member and you don’t have to free it manually. See below for more information on how to perform the actual serialization on a SchemaComponentUpdate.

For example:

  var schemaUpdate = SchemaComponentUpdate.Create();
  // serialize into schemaUpdate
  const uint ComponentId = 54;
  var update = new ComponentUpdate(ComponentId, schemaUpdate);

  connection.SendComponentUpdate(entityId, update);

Deserialization is similar. For example, when handling a ComponentUpdateOp (while processing operations), a ComponentUpdate will be provided with SchemaData assigned to some serialized data received from the network (and UserHandle set to null). You can then read this component update using the schema functions, as described below. For example:

  SchemaComponentUpdate? schemaUpdate = op.Update.SchemaData;
  if (schemaUpdate.HasValue)
  {
    SchemaComponentUpdate schemaObject = schemaUpdate.Value;
    // deserialize update
  }

Vtables

The other option for serializing data is using “vtables” (named after the concept used to implement polymorphism). A vtable is a struct containing a component ID, and a set of 16 different function pointers (4 for each handle type). Instead of providing a serialized object in the SchemaData field, you specify a custom handle in the UserHandle field, leaving SchemaData as null. This custom handle is of type UIntPtr. You can put in any value you like, you just need to find a way to map it to the object containing your data (see example below for one way to achieve it). For each handle type, you must provide the following functions:

  • Serialize - converts a custom handle into a serialized Schema* object.
  • Deserialize - converts a serialized Schema* object into a custom handle.
  • Copy- creates a shallow copy of a custom handle object.
  • Free - frees a custom handle allocated using Deserialize or Copy.

Note that these functions must be thread-safe, as they are called from within a connection thread in the SDK

These functions have the following signature (using component update as an example):

public unsafe delegate void ComponentUpdateFree(ComponentId componentId, UIntPtr userData, UIntPtr handle);
public unsafe delegate UIntPtr ComponentUpdateCopy(ComponentId componentId, UIntPtr userData, UIntPtr handle);
public unsafe delegate bool ComponentUpdateDeserialize(ComponentId componentId, UIntPtr userData, SchemaComponentUpdate source, out UIntPtr handleOut);
public unsafe delegate SchemaComponentUpdate? ComponentUpdateSerialize(ComponentId componentId, UIntPtr userData, UIntPtr handle);

Note that each function signature contains a userData parameter. This is unrelated to the UserHandle specified in the handle type. Instead, this is an arbitrary UIntPtr that is set inside the ComponentVtable object and also controlled by you. You can leave this as null if it’s not needed.

One possible implementation of these methods would be to have a registry class that contains a dictionary mapping from a UIntPtr key to deserialized objects. The keys can be picked arbitrarily, e.g. with a unique counter. This is similar to the approach used by the Worker SDK in C# and Java.

Keep in mind that the SDK calls the *Serialize function from an internal thread, so the Schema* objects need to be kept alive until they have been processed. Likewise, *Deserialize must store the deserialized objects in a location where they can later be retrieved by the game thread. Whatever approach you use, you need to ensure that object lifetime is properly handled.

The reason why we use UIntPtr here and not a numeric type is because the Worker SDK in C (which the C# bindings are based on) uses the void* type which directly maps to UIntPtr

A simplified example could look like this:

// An example of a generated struct for "Position".
struct Position
{
  public double x;
  public double y;
  public double z;
};

// A class which manages a dictionary from an integer to a blob of managed memory.
class Serialization
{
  internal static System.Collections.Generic.Dictionary<UIntPtr, Object> ObjectHandles;

  internal static UIntPtr AddObject(object o) 
  {  
    UIntPtr id = (UIntPtr)nextHandle++;
    ObjectHandles.Add(id, o);
    return id;
  }

  internal static void RemoveObject(UIntPtr handle) 
  {
    ObjectHandles.Remove(handle);
  }

  private static Int32 nextHandle = 1;
}

// An example of a class which implements the vtable functions for "Position".
class PositionSerialization
{
  static unsafe void ComponentUpdateFree(
    uint componentId,
    UIntPtr userData,
    UIntPtr handle)
  {
    // Removes the object with index 'handle' from the dictionary. The object will get garbage
    // collected later.
    Serialization.RemoveObject(handle);
  }

  static unsafe UIntPtr ComponentUpdateCopy(
    uint componentId,
    UIntPtr userData,
    UIntPtr handle)
  {
    // This function will not copy the data, but create a second handle which points to the same
    // data.
    object o;
    Serialization.ObjectHandles.TryGetValue(handle, out o);
    if (o != null)
    {
      // Allocate a second handle to the same data.
      UIntPtr newHandle = Serialization.AddObject(o);
      return newHandle;
    }
    return (UIntPtr)0;
  }

  static unsafe bool ComponentUpdateDeserialize(
    uint componentId,
    UIntPtr userData,
    SchemaComponentUpdate source,
    out UIntPtr targetOut)
  {
    var position = new Position();
    // read `source` using schema APIs and write data to position struct.
    // return 0 (false) if serialization fails.
    targetOut = Serialization.AddObject(position);
    return true;
  }

  static unsafe SchemaComponentUpdate? ComponentUpdateSerialize(
    uint componentId,
    UIntPtr userData,
    UIntPtr handle)
  {
    object o;
    Serialization.ObjectHandles.TryGetValue(handle, out o);
    if (o == null)
    {
      return null;
    }

    var position = (Position) o;
    var target = SchemaComponentUpdate.Create();
    // Read position struct and write data using schema APIs to target
    return target;
  }
}

Choosing between direct serialization and vtables

Here are some things to keep in mind when choosing between direct serialization and vtables depending on your needs:

Direct serialization

  • (De-)serialization takes place on the game thread. Depending on how much you (de-)serialize each frame, you might use up a sizeable chunk of your update time.
  • Since you control the point in time when data is serialized, serialization is easy to reason about, e.g. you know when serialization is complete.
  • If you have multiple locations where data is serialized, you have to ensure that serialization code is correct in all places.

Vtables

  • Serialization is performed on an internal connection thread in the SDK. This frees up the game thread to do more work on the actual game update.
  • You don’t know when data has been serialized since vtables are asynchronous in nature. So you might need to copy user data to keep it alive for serialization only to be thrown away afterwards while the game thread continues modifying its own copy in later frames.
  • All serialization code for a specific component lives in a single place.
  • Since it’s asynchronous, errors in your serialization code can be harder to debug

The biggest drawback of direct serialization is using up time on the game thread. To work around that, you could also have your own multi-threading solution that ensures serialization is synchronized at the end of your frame. This way your game thread is free to modify objects the next frame again and you might get around extraneous copies of your data.

Performance considerations

In order to ensure best performance when reading from and writing to SchemaObjects, keep these guidelines in mind:

  • Access fields in order of their IDs (specified in your schema)
  • Read or write repeated fields using the SchemaObject.*List functions if possible
  • If you cannot process repeated fields as a whole, at least access them in index order

Serialization reference

We currently don’t provide an example project for the C# bindings, but we provide a C example project which serializes components with both approaches described above. When using those as an example, keep in mind that objects in C# are garbage collected, so you have to ensure that your objects live long enough to survive API calls.

The remainder of this page is a reference which describes in detail how to use the schema API to serialize different schema types, such as commands, lists, options, etc.

In the code examples, we assume that SchemaObject fieldsObject; has been defined already and is set to the schema object which contains the fields of the Example schema type at the beginning of each section. More about schema objects can be found below.

Primitive types

enum MyEnum {
  FOO = 1;
  BAR = 2;
  BAZ = 3;
}

type Example {
  int32 value = 3;
  EntityId entity_id = 4;
  MyEnum my_enum = 5;
}

To write a primitive type to a schema object, you can use any of the SchemaObject.Add* functions, depending on the primitive type. For example, to write an integer 1234 to the value field above, you can write the following code:

  fieldsObject.AddInt32(3, 1234);

To read a primitive type from a schema object, you can use any of the SchemaObject.Get* functions, depending on the primitive type. For example, to read the integer in the value field above into a variable, you can write the following code:

  System.Int32 value = fieldsObject.GetInt32(3);

The mapping from schema type to function family is given below:

.schema type function family
int32 Int32
int64 Int64
uint32 Uint32
uint64 Uint64
sint32 Sint32
sint64 Sint64
fixed32 Fixed32
fixed64 Fixed64
sfixed32 Sfixed32
sfixed64 Sfixed64
bool Bool
float Float
double Double
string Bytes
EntityId EntityId
Entity Object
bytes Bytes
user-defined enum Enum
user-defined type Object

Note that some primitive types, such as a string or Entity, require more work to be serialized and deserialized correctly. More information on these types can be found in later sections.

An EntityId is stored as an int64 under the hood, but for clarity, we expose separate functions for serializing and deserializing entity IDs. For example:

  System.Int64 entityId = fieldsObject.GetEntityId(4);

An enum is stored as an uint32 under the hood, but similarly to EntityId, we expose separate functions for serializing and deserializing enums. For example:

  enum MyEnum
  {
    Foo = 1,
    Bar = 2,
    Baz = 3,
  }

  private static void ReadEnumExample(SchemaObject fieldsObject)
  {
    System.Int32 value = fieldsObject.GetInt32(5);
    MyEnum enumValue = (MyEnum)value;
  }

Lists

type Example {
  list<float> value = 6;
}

A list field is a field whose ID occurs 0 or more times. In generated code, you add values to a list field any number of times to populate your list (whilst for singular primitive types mentioned above, you must add a value exactly once). An example of writing a list in this way can be the following (using the above list field as an example):

  float[] list = new float[]{ 1, 2, 3, 4 };
  fieldsObject.AddFloat(6, list[0]);
  fieldsObject.AddFloat(6, list[1]);
  fieldsObject.AddFloat(6, list[2]);
  fieldsObject.AddFloat(6, list[3]);

If you can, it’s faster to provide the whole list in a single call by using the SchemaObject.Add*List family of functions. The first overload takes native C# arrays and copies them into the SchemaObject in one go. The other overload takes pointer types without copying any data. As a result, the above code can be simplified to:

  float[] list = new float[]{ 1, 2, 3, 4 };
  fieldsObject.AddFloatList(6, list);

  // or

  fixed(float* listValues = list)
  {
    fieldsObject.AddFloatList(6, listValues, list.Length);
    // Don't leave fixed block as long as the SchemaObject lives!
  }

Note that when using the overload taking a pointer, no copy of the data is made, it is your responsibility to ensure that the lifetime of the source data is greater than the schema object.

Sometimes, it’s difficult to ensure that a list of data lives long enough. In this case, you can reserve a buffer to a specified length using SchemaObject.AllocateBuffer, whose lifetime is tied to the schema object. You can modify the above example to use this function in the following way:

  uint bufferLength = (UInt32) (list.Length * Marshal.SizeOf(typeof(float)));
  System.Byte* buffer = fieldsObject.AllocateBuffer(bufferLength);
  // Fill buffer with data.
  fieldsObject.AddFloatList(6, (float*) buffer, (int) bufferLength);

When reading a list field from a schema object, you can:

  • obtain the count with the SchemaObject.Get*Count family of functions.
  • retrieve individual elements with the SchemaObject.Index* family of functions.
  • copy the entire list into a buffer managed by yourself using the SchemaObject.Get*List family of functions.

For example:

  // access the 3rd element (index 2)
  float singleElement = fieldsObject.IndexFloat(6, 2);

  // obtaining the complete list
  float[] resultList = fieldsObject.GetFloatList(6);

  // or, if you want to read into a pinned list
  float[] values = new float[fieldsObject.GetFloatCount(6)];
  fixed (float* buffer = values)
  {
    fieldsObject.GetFloatList(6, buffer);
  }

Strings and bytes

type Example {
  string value_string = 1;
  bytes value_bytes = 2;
}

String and bytes fields are both treated as bytes in the Worker SDK C# bindings, because strings have to be UTF-8 encoded. In SpatialOS, the only difference is how code is generated for them in the Worker SDK in C++, C#, and Java, and how they’re visualized in the Inspector.

In order to serialize byte buffers, you can use the SchemaObject.AddBytes functions. However, converting a string to a byte[] requires additional conversion code, which is why the Worker SDK C# bindings offer additional overloads on SchemaObject which perform that conversion internally and allow you to work directly with strings.

To write a string or byte buffer to a schema object, you can do this with the following code:

  // Write a string.
  fieldsObject.AddString(1, "Hello World.");

  // Write a byte buffer.
  byte[] bytes = CreateBytes(42);
  // Fill the buffer with data.
  fieldsObject.AddBytes(2, bytes);

If you want to avoid internally allocating a byte[] for each string, you can use the SchemaObject.AddBytes overloads directly. In this case you have to take care of properly converting your strings into UTF-8 strings yourself. You also need to ensure that the lifetime of your byte buffer is greater than that of the schema object.

As with lists, you can use SchemaObject.AllocateBuffer to obtain a buffer into which you can copy your data for easier lifetime management:

  // Write a byte buffer.
  uint bufferLength = 42;
  byte[] bytes = CreateBytes(bufferLength);
  System.Byte* buffer = fieldsObject.AllocateBuffer(bufferLength);
  // Fill buffer with data.
  fieldsObject.AddBytes(2, buffer, bufferLength);

To read bytes from a schema object, you can make use of SchemaObject.GetBytes which allocates a byte[], or manage the memory yourself by using SchemaObject.GetBytesLength to obtain the length of the byte buffer, and SchemaObject.GetBytesBuffer to get a pointer to the byte buffer itself. For example:

  // Read and allocate.
  byte[] read = fieldsObject.GetBytes(2);

  // read without allocating
  int bufferLength = fieldsObject.GetBytesLength(2);
  System.Byte* outBytes = fieldsObject.GetBytesBuffer(2);
  // Process outBytes, probably copy to some other data structure.

Objects

type MyType {
  int32 some_value = 1;
  float another_value = 2;
}

type Example {
  MyType data = 4;
}

Objects in schema are containers that contain fields, including potentially more object fields. You can manipulate object fields in a similar way to how you manipulate primitive types, but can also manipulate fields within those object fields.

To write an object following the above schema, you could use the following code:

  SchemaObject type = fieldsObject.AddObject(4);
  type.AddInt32(1, 1234);
  type.AddFloat(2, 1234.0f);

Similarly, we can read the object using the following code:

class MyType
{
  public System.Int32 SomeValue;
  public float AnotherValue;
}

private static void ReadObjectExample(SchemaObject fieldsObject)
{
  SchemaObject typeObject = fieldsObject.GetObject(4);

  MyType type = new MyType();
  type.SomeValue = typeObject.GetInt32(1);
  type.AnotherValue = typeObject.GetFloat(2);
}

Entities

type Example {
  Entity value = 7;
}

Entities in schema are fields that contain an arbitrary set of components. When serialized, they are represented as an object that contains 0 or more objects that represent individual components. The field ID of the inner component objects are the component IDs of the components stored inside the entity object. Inner component data objects should be serialized and deserialized in the same way as ComponentData objects, as described below.

To write an entity following the above schema, you could use the following code:

  ComponentData[] components = GetComponentsFromSomewhere();
  SchemaObject entity = fieldsObject.AddObject(7);
  foreach (ComponentData component in components)
  {
    SchemaObject componentObject = entity.AddObject(component.ComponentId);
    SerializeComponent(component, componentObject);
  }

Similarly, we can read the entity using the following code:

  SchemaObject entity = fieldsObject.GetObject(7);
  System.UInt32[] componentIds = fieldsObject.GetUniqueFieldIds();

  ComponentData[] components = AllocateComponentArray(componentIds.Length);
  for (var i = 0; i < componentIds.Length; ++i)
  {
    SchemaObject component = entity.GetObject(componentIds[i]);
    DeserializeComponent(component, componentIds[i], components[i]);
  }

Options

type Example {
  option<uint32> value = 2;
}

An option field is a field whose ID occurs either 0 or 1 times. It can be thought of as a list which can have either 0 or 1 elements. Therefore, options are used in a similar way to lists. To write an option value, you can call SchemaObject.Add* to set the option to a value, or you can keep the value empty by omitting the call to SchemaObject.Add*. For example:

  // Places 1234 in a uint32 option field.
  fieldsObject.AddUint32(2, 1234);

When reading an option field from a schema object, you can use the SchemaObject.Get*Count family of functions to determine whether the option field is set. For example:

  System.UInt32? value = null;
  if (fieldsObject.GetUint32Count(2) == 1)
  {
    value = fieldsObject.GetUint32(2);
  }

Maps

type Example {
  map<uint32, float> value = 1;
}

A map field is a field which stores key-value pairs. In terms of serialization, it is functionally equivalent to reading and writing a list of objects, where each object is a key-value pair. The field IDs of the key and value are 1 and 2 respectively, which are also defined with the SchemaObject.SchemaMapKeyFieldId and SchemaObject.SchemaMapValueFieldId constants respectively. To write a map, you can use the following code to add a single key value pair [100 -> 25.0f]:

SchemaObject kvpair = fieldsObject.AddObject(1);
kvpair.AddUint32(SchemaObject.SchemaMapKeyFieldId, 100);
kvpair.AddFloat(SchemaObject.SchemaMapValueFieldId, 25.0f);

Similarly, to read a map field, you would make use of Schema_GetObjectCount to obtain the number of key-value pairs, then use Schema_IndexObject to read them. For example:

  System.UInt32 mapSize = fieldsObject.GetObjectCount(1);

  for (System.UInt32 i = 0; i < mapSize; ++i)
  {
    SchemaObject kvpair = fieldsObject.IndexObject(1, i);
    System.UInt32 key = kvpair.GetUint32(SchemaObject.SchemaMapKeyFieldId);
    float value = kvpair.GetFloat(SchemaObject.SchemaMapValueFieldId);
    // Use 'key' and 'value'.
  }

You have to ensure yourself that keys are unique. We recommend always taking the last key that was found. In particular, you have to make sure you don’t crash in this case as this would allow a potential malicious client to take down your worker(s).

Component data

component TestComponent {
  id = 10000;
  int32 foo = 1;
  float bar = 2;
}

Component data is represented as a single object, known as the “fields” object. If you create a SchemaComponentData object to be saved to a snapshot, or you receive some component data when handling an AddComponentOp, you can access the “fields” object using the SchemaComponentData.GetFields function. If the fields object does not exist yet, then this function will automatically create it for you. This “fields” object will contain the fields of your component (defined either inline in the component, or using the data statement in .schema). For example, to create a component data object that corresponds to TestComponent:

SchemaComponentData data = SchemaComponentData.Create();
SchemaObject fields = data.GetFields();
fields.AddInt32(1, 1234);
fields.AddFloat(2, 55.0f);

Similarly, you can read a component data object with the following code:

  // Assume we have a "SchemaComponentData data", possibly contained in a ComponentData.
  SchemaObject fields = data.GetFields();
  System.Int32 foo = fields.GetInt32(1);
  float bar = fields.GetFloat(2);

Component updates

type TestEvent {
  uint32 counter = 1;
  float size = 2;
}

component TestComponent {
  id = 10000;
  int32 foo = 1;
  float bar = 2;

  event TestEvent my_event;
}

A component update can be thought of as a diff between two component snapshots. They contain two objects, a “fields” object and an “events” object.

Fields

In terms of serialization, the “fields” object is equivalent to the “fields” object in a component data. However, all primitive fields are optional. That means that you can choose not to call SchemaObject.Add* for a particular field ID, and that field will not be included in the component update. Note that as a result, special treatment is required to distinguish between an update not changing a collection, or an update clearing a collection (making it empty).

To write a set of fields in a component update:

SchemaComponentUpdate data = SchemaComponentUpdate.Create();
SchemaObject fields = data.GetFields();
fields.AddInt32(1, 1234);

When receiving a component update, there is no guarantee that a field will contain a value. Similar to options, you should use the SchemaObject.Get*Count family of functions to check whether a field is contained within an update. To read an update to TestComponent:

  // Assume we have a "SchemaComponentUpdate data", possibly contained in a ComponentUpdate.
  SchemaObject fields = data.GetFields();
  System.Int32? foo = null;
  float? bar = null;
  if (fields.GetInt32Count(1) > 0)
  {
    foo = fields.GetInt32(1);
  }
  if (fields.GetFloatCount(2) > 0)
  {
    bar = fields.GetFloat(2);
  }

Clearing fields

So far, it’s possible to write an option, list or map which has at least one element in it, otherwise the update is considered to not have a change to that field ID. However, in some cases, you wish to assign an option, list or map field to the empty state rather than leave it unchanged. The SchemaComponentUpdate.*ClearedField* family of functions can be used for this purpose. For example, to clear a list field (by setting the field to an empty list), you can write:

SchemaComponentUpdate data = SchemaComponentUpdate.Create();
data.AddClearedField(3);

When receiving a component update, you can obtain the list of fields to assign to the empty option/list/map with the following code:

  // Assume we have a "SchemaComponentUpdate data", possibly contained in a ComponentUpdate.
  uint clearedFieldCount = data.GetClearedFieldCount();
  System.UInt32 clearedFieldId = 0;
  for (uint i = 0; i < clearedFieldCount; ++i)
  {
    clearedFieldId = data.IndexClearedField(i);
    // Assign an empty option/list/map to the cleared field.
  }

Events

Events are stored as lists of objects which contain the event data, as part of the “events” object. Each field ID in the “events” object corresponds to the “event ID”, which is a 1-based index depending on the order of the events in the component.

To trigger a single ‘my_event’ event as part of a component update, you can use the following code:

SchemaComponentUpdate data = SchemaComponentUpdate.Create();
SchemaObject events = data.GetEvents();
SchemaObject eventData = events.AddObject(1); // event ID 1
eventData.AddUint32(1, 122); // counter
eventData.AddFloat(2, 25.0f); // size

When receiving a component update, you need to ensure that all instances of each event type are processed, as a single component update can contain many copies of the same event being triggered. For example:

  // Assume we have a "SchemaComponentUpdate data", possibly contained in a ComponentUpdate.
  SchemaObject events = data.GetEvents();
  System.UInt32 eventCount = events.GetObjectCount(1); // event ID 1
  for (System.UInt32 i = 0; i < eventCount; ++i)
  {
    SchemaObject eventData = events.IndexObject(1, i); // event ID 1
    System.UInt32 counter = eventData.GetUint32(1);
    float size = eventData.GetFloat(2);
    // Use counter and size.
  }

Command requests and responses

type CmdRequest {
  bytes data = 1;
  float value = 2;
}

type CmdResponse {
  int32 value = 1;
}

component TestComponent2 {
  id = 10001;

  command CmdResponse my_command(CmdRequest);
}

Command requests and response objects are serialized in almost exactly the same way as component data. The only difference is that you need to call SchemaCommandRequest.GetObject to access the command request object from an instance of SchemaCommandRequest, or SchemaCommandResponse.GetObject to access the command response object from an instance of SchemaCommandResponse.

SchemaCommandRequest request = SchemaCommandRequest.Create();
SchemaObject requestObject = request.GetObject();
requestObject.AddBytes(1, CreateBytes(42));
requestObject.AddFloat(2, 55.0f);

Similarly, you can read a command request or response object with the following code (using request as an example):

  // Assume we have a "SchemaCommandRequest request", possibly contained in a CommandRequest.
  SchemaObject requestObject = request.GetObject();
  byte[] data = requestObject.GetBytes(1);
  float value = requestObject.GetFloat(2);

Generic data

Component data, component updates and command requests and responses are targeted towards serializing component data and commands. Sometimes you might want to serialize an arbitrary schema type that is not one of those (although it might be a type that is used inside a component).

type SubType {
  uint32 foo = 1;
  float bar = 2;
}

component NestedComponent {
  id = 10000;
  SubType data = 1;
}

To serialize an object of type SubType in this example, create a SchemaGenericData which will act as a container for the object’s data. This is similar to SchemaComponentData but it can be applied to any type defined in your schema. As with the SchemaComponentData, there is a corresponding SchemaGenericData.GetObject function to access the underlying schema object. All fields of the type are expected to be present in the serialized object.

SchemaGenericData data = SchemaGenericData.Create();
SchemaObject dataObject = data.GetObject();
dataObject.AddUint32(1, 1234);
dataObject.AddFloat(2, 42.0f);

Similarly, you can deserialize your type with the following code:

// Assume we have a "SchemaGenericData data".
SchemaObject dataObject = data.GetObject();
System.UInt32 foo = dataObject.GetUint32(1);
float value = dataObject.GetFloat(2);

Search results

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums