Storyboard events contain string based names and a variable data field. For
this reason the event data must be serialized into a buffer for communication.
The Storyboard IO API provides the functions needed to both serialize your data
and send the event. The event you wish to send must first be serialized via a
call to gre_io_serialize()
. This will allocated a serialized data
buffer for your event. The event can then be sent via the
gre_io_send()
function. Once the event has been sent the
buffer can be reused or freed via a call to
gre_io_free_buffer()
.
Serialized buffers can be reused multiple times. The
gre_io_serialize_buffer()
function will resize or
reallocate the buffer if the data being serialized is larger than the
existing buffer. This is designed to cut down on repetitive memory
allocation and deallocation churn.
Data parameters must be sent in order of descending alignment requirements. Example: 4u1 4u1 2u1 1s0 is good, 2u1 4u1 4u1 1s0 is not
gre_io_t *send_handle; gre_io_serialized_data_t *nbuffer = NULL; const char *event_data = "my event data" /* * Connect to a channel to send messages. */ send_handle = gre_io_open("my_channel", GRE_IO_TYPE_WRONLY); if(send_handle == NULL) { printf("Can't open send handle [%s]\n", argv[1]); return 0; } /* * Send a named event containing no data payload */ nbuffer = gre_io_serialize(nbuffer, NULL, "my_event_name", NULL, NULL, 0); gre_io_send(send_handle, nbuffer); /* * Send a named event with an additional string payload */ nbuffer = gre_io_serialize(nbuffer, NULL, "my_event_name", "1s0 data", event_data, strlen(event_data)+1); gre_io_send(send_handle, nbuffer);
New events can be readily defined and are not required to contain
a data payload. In this case their format string and data payload
will be empty values. When creating new events, it is appropriate
to namespace the event definitions so that the names of events do
not collide. For example, the Storyboard framework reserves the
name prefix of gre.
for user interface events, and the
timer functions all generate events that are prefixed with
timer.
The use of events is closely coupled with the declaration and operation of actions. An action can only be invoked when an event matching the action definition is received. This results in a common design pattern where an action will perform sophisticated logic in an external script or program and then signal a completion action to run once the script work is complete.
Event >Action (script) >Work >Trigger Event >Action (completion)
Any data associated with an event as its payload must be a linearly contiguous block of memory. In order for clients receiving the event to decode the event data memory block back into structured content a key must be provided. The format string that is provided as part of the event is this decoding key.
The format string describes how the individual bytes of event data are to be grouped together as specific data types. For example the Lua script plugin can use the format string to convert the event data memory block into Lua variables that conform to Lua's type system. Once converted, the symbolic name for the data, provided as part of the format string, can be used to reference that particular information. Other clients, such as C or C++ programs, may not need to interpret the data symbolically but may use a language specific mechanism to convert the memory block.
The format string is relatively straightforward to create and is a series of
entries formatted as [numbytes][signed/unsigned][numelements][ ][name]
.
For the standard C data types the number formatting would look like:
C/C++ Type | Format String | Data Size |
---|---|---|
int8_t | 1s1 | 1 byte |
uint8_t | 1u1 | 1 byte |
int16_t | 2s1 | 2 bytes |
uint16_t | 2u1 | 2 bytes |
int32_t | 4s1 | 4 bytes |
uint32_t | 4u1 | 4 bytes |
int64_t | 8s1 | 8 bytes |
uint64_t | 8u1 | 8 bytes |
float (IEEE754 float) | 4f1 | 4 bytes |
char * | 1s0 | Length of string including nul terminator |
So, if you were transmitting the following C/C++ structure you would presume that the bytewise memory layout would be:
You would use a format string of 4s1 a 2u1 b
to describe the event.
The symbolic field descriptions a
and b
are optional but
highly recommended. They are used to give the data symbolic representation for
clients that can't access the memory bytes directly (such as Lua). These symbolic
field descriptions do not need to match the names of the structure member variables
so an equally valid format string for the above structure might have been
4s1 angle 2u1 magnitude
if angle
and magnitude
were better symbolic names for what the data represents.
The format string provided describes the linear memory layout of the event data. Consequently it is important that the format string take into consideration any alignment or padding inserted when the memory block is created. Consider changing the order of the members in the sample structure:
without any additional guidance to tell it otherwise the C/C++ compiler is going to create storage for the structure such that members are aligned to boundaries that match their data types (ie 4 byte types are aligned on 4 byte boundaries). This can create holes in the memory layout.
Here the 32 bit/4 byte
member of the structure a
comes after b
but there are
two additional bytes of padding inserted to ensure a
starts on a 4
byte memory boundary. Since the format string must describe the linear memory
layout for clients, we would have to change the format string to accomodate the
extra padding inserted for alignment and the format string would be 2u1 b
2u1 pad 4s1 a
. It is always good practice to avoid wasting extra
bytes on padding alignment, but Storyboard does not perform any sort of
interpretation. In fact providing a format string that mis-aligns data can
result in unpredictable behaviour.
Event data frequently will contain string information. Strings are
simply an array of one byte values with a nul terminating character, often
represented as a pointer to this memory (i.echar *
). All text in
Storyboard is encoded using UTF-8 so this statement applies regardless of the
text values being represented. If an event's data payload is composed of a
single string, then the bytes of that string can be used directly as the block
of memory:
char * event_data = "Crank";
The
event_data
variable, as a pointer to memory, can be used
directly and the format string used to represent it would be 1s0
msg
, where msg
can be whatever symbolic name makes
sense.
It is not possible to send C/C++ structures that contain strings as members if those variables are declared as pointers because the memory of the structure (including the string) is not linear and the event data must be a linearly contiguous block of memory.
However it is possible to include strings within structures by
either fixing their size which will force their storage to be included as part
of a structure block, i.e char msg[20]
, or if only a single string
is being sent then the C/C++ idom of overallocating the size of a structure can
be used to force a linear memory layout:
The C/C++ code technique for using this would look something like:
struct event_data { int a; //Assume 32 bit integers char b[1]; }; struct event_data *ed; //Allocate the memory for the base structure and the string to follow it ed = malloc(sizeof(*ed) + strlen("Crank")); //Assign the values to the allocated structure ed->a = 2018; strcpy(ed->b, "Crank"); //nul character is accounted for by b[1]
In this case the data can now be described with the format string 4s1 a
1s0 b
where the 1s0
is shorthand for nul terminated
strings and would be equivalent to saying 1s6
where 6 is the number
of bytes in the string "Crank" plus the nul terminating character.