This library will encode and decode data as per the specification documented here: https://github.com/fluffy/draft-jennings-game-state-over-rtp. (This code is against commit 1955e14ab4557045608efd7564ea08c3984ca86a, dated 2021-12-23.)
cmake -S . -B build ; cmake --build build --parallel
The following is an example CMakeLists.txt file that can be used to include this library in other software package builds.
# Enable fetching content
include(FetchContent)
# Fetch the Game State Encoder Library
FetchContent_Declare(gse
GIT_REPOSITORY https://github.com/cisco/gse.git
GIT_TAG main)
# Make the library available
FetchContent_MakeAvailable(gse)
The core of the library is written in C++ and a C interface exists (discussed below) to facilitate using the library with other languages.
The two main C++ objects are:
- gs::Encoder
- gs::Decoder
These objects only need to be instantiated once. They are stateless and
thread-safe, as long as no two threads write or read from the same DataBuffer
simultaneously.
The objects to encode are defined in the header file gs_types.h
and
should align with the Internet Draft referenced above. For example, there is
a structure called gs::Hand1
. To encode a gs::Hand1
object, one would
populate the structure with the desired values and then call gs::Encoder
's
Encode()
function, passing it a DataBuffer
object into which the
object should be serialized. The DataBuffer
can allocate a buffer
or accept a user-provided buffer (and length) when it is instantiated.
The total length of the encoded object(s) can be determined by calling
DataBuffer
's GetDataLength()
function.
Multiple objects can be encoded into the same DataBuffer
. Each call
to Encode()
will result in the next object being appended to previously
serialized objects in the DataBuffer
.
Likewise, multiple objects may be deserialized from the same DataBuffer
.
To decode a buffer full of objects received over a network, for example,
one would create a DataBuffer
object having a pointer to the start of the
encoded data. Then, gs::Decoder
's Decode()
function would be called.
The Decode()
function accepts a variant gs::GSObject
(and called
repetitively until there are no further objects) or a vector of variants
gs::GSObjects
(requiring a single call to decode the entire buffer) as the
second parameter into which the decoded object(s) will be written.
Note that the objects gs::Serializer
and gs::Deserializer
exist to
facilitate serialization and deserialization of various simpler data types
into and out of the DataBuffer
. One does not use those object directly.
Rather, they are used indirectly by gs::Encoder
and gs::Decoder
.
In the event of an error, the gs::Encoder
may throw an exception of
type gs::EncoderException
if an attempt is made to encode an invalid object
or DataBufferException
if there is a problem with the DataBuffer
, though
an effort was made to detect such issues to avoid such exceptions from being
thrown. Likewise, gs::Decoder
may throw an exception of the type
DataBufferException
or gs::DecoderException
if there is an error reading
from the data before or decoding the data buffer.
For examples of how to use these objects, see the unit test code in test/test_gs_encoder and test/test_gs_decoder or the C API code.
The C interface has the following encoder (serialization) functions:
- GSEncoderInit()
- GSEncoderSetBuffer()
- GSEncoderResetBuffer()
- GSEncoderDataLength()
- GSEncodeObject()
- GetEncoderError()
- GSEncoderDestroy()
One would call GSEncoderInit()
with, optionally, a pointer to the raw buffer
and a buffer length. That will create a context that is used with other calls.
A buffer may also be passed in via GSEncoderSetBuffer()
, allowing re-use of
the context created via GSEncoderInit()
. It's also possible to reset the
existing buffer with a call to GSEncoderResetBuffer()
, which effectively
clears whatever is in the buffer previously assigned to the context.
A GS_Object
is populated and passed into GSEncodeObject()
, which will
return 1 on success, 0 if the buffer cannot hold more data, or -1 on error.
On error, one can check the error string. Calling the GSEncoderDataLength()
will return the number of octets serialized into the buffer. GSEncodeObject()
may be called repeatedly until there is no more room in the buffer.
GSEEncoderDestroy()
will destroy the encoder context and any associated data,
so be sure to call GSEncoderDataLength()
before GSEEncoderDestroy()
.
The C interface has the following decoder (deserialization) functions:
- GSDecoderInit()
- GSDecoderSetBuffer()
- GSDecoderResetBuffer()
- GSDecodeObject()
- GetDecoderError()
- GSDecoderDestroy()
One would call GSDecoderInit()
with, optionally, a pointer to the buffer
holding the object(s) to deserialize and a buffer length. That will create a
context that is used with other calls. A buffer may also be passed in via
GSDeccoderSetBuffer()
, allowing re-use of the context created via
GSEncoderInit()
. It's also possible to reset the existing buffer with a call
to GSDecoderResetBuffer()
, which effectively sets the reading position back
to the start of the buffer that was previously provided and assigns a new
buffer length value.
One then calls GSDecodeObject()
with, a pointer to a GS_Object
type. The
structured will be zero-initialized by this API call, so the caller need not
initialize it. The result result will be 1 if successful, 0 if there are no
more objects to deserialize from the buffer, or -1 if there is an error.
On error, one can check the error string. GSDecodeObject()
may be called
repeatedly until there are no more objects to decode.
As a part of the decode process, some data may be dynamically allocated on the
heap with pointers assigned inside the GS_Object
. Pointers to any such data
are stored with the decoder context and will be freed when GSEDecoderDestroy()
is called. One may assume pointers remain valid until GSEDecoderDestroy()
is
called.
GSEDecoderDestroy()
will destroy the decoder context and any associated data.