Preface
This is the reference manual of MoonTypes, a library that provides means to define C-like structured types in Lua. [1]
It is assumed that the reader is familiar with the Lua programming language.
Getting and installing
For installation intructions, refer to the README file in the MoonTypes official repository on GitHub.
Module organization
The MoonTypes module is loaded using Lua’s require() and returns a table containing the functions it provides (as usual with Lua modules). This manual assumes that such table is named types, i.e. that it is loaded with:
types = require("moontypes")
but nothing forbids the use of a different name.
Examples
Complete examples can be found in the examples/ directory of the release package.
License
MoonTypes is released under the MIT/X11 license (same as Lua, and with the same only requirement to give proper credits to the original author). The copyright notice is in the LICENSE file in the base directory of the official repository on GitHub.
See also
MoonTypes is part of MoonLibs, a collection of Lua libraries for graphics and audio programming.
Introduction
MoonTypes is a library that provides means to define C-like structured types in Lua, to instantiate them, and set/get fields from type instances.
It has been designed for the definition of message formats - specifically, for signals exchanged by MoonAgents' agents - but it is a standalone library and can be as well used as a general purpose type system.
Types in MoonTypes are defined in one or more groups. This allows, for example, to define different message interfaces and keep their type definitions separate.
The definition of types is done according to a syntax that is very similar to that for type declaration in the C programming language, and that supports structs, arrays, and unions.
Once a group is populated with type definitions, instances of its type can be created and their fields can be accessed using a convenient dot notation.
Groups
A group is a database of type definitions, created with the following function:
-
group = group([ np ])
Creates a new group, having only the pre-defined primitive types in it.
The optional parameter np (a string), if supplied, overrides the default NP value ('not present'), which is the question mark character ('?').
Once created, a group can be populated using the following methods:
-
group:typedef(definitions)
group:typedef_from_file(filename)
Add to group the types defined in the string definitions or in the file named filename, according to the type definition syntax.
group:typedef([[
-- some aliases of primitive types:
typedef boolean myboolean
typedef string mystring
typedef void myvoid
-- a derived structured type:
typedef struct {
boolean b
string s
number arr[5]
} mytype
]])
Instances
In MoonTypes, an instance of a type is a Lua sequence whose first array-element is a string denoting the type (the type name), and the elements that follow are the values of the terminal fields.
Essentially, an instance is a flattened-out concrete representation of an object of the given type, and the type definition allows to interpret it and access its fields (at any level) by name, using the get/set methods described in the next session.
A special NP value ('not present'), is used for terminal fields that are absent. The NP value can be configured on a group basis, and by default is '?' (a question mark character).
Instances can be created with the following methods:
-
instance = group:new(typename)
Creates a new instance of the type named typename.
All the terminal fields are initialized to the NP value (meaning 'not present').
-
instance1 = instance:clone ( )
Creates a new instance by cloning the already existing instance, including its values.
local x = group:new('mytype') -- create an instance of mytype
--> x = { 'mytype', '?', '?', '?', '?', '?', '?', '?' }
Accessing fields
The fields of an instance are referred to with a dot notation which is similar to the one used in C, but with a couple of differences: elements of arrays are named with their index, starting from 1 (e.g.,'arr.1', 'arr.2', …), and the special name '*' can be used to denote the whole instance (that is, the outermost-level field).
In the methods that follow, the fieldname argument must always be given as a string using this notation.
-
instance:set(fieldname, [value1, value2, …])
Writes the given values to the field named fieldname.
Assuming fieldsz is the size of the field, and nargs is the number of supplied arguments, then:
- if nargs < fieldsz, only the first nargs terminal fields are written (in the given order), while the remaining ones are left untouched;
- if nargs >= fieldsz, the entire field is written with the first fieldsz arguments, and any extra value is ignored;
- if nargs = 0 (no values are supplied), all the terminal fields of fieldname are reset to the NP value ('not present').
-
value1, value2, … = instance:get(fieldname)
Returns the terminal values of the field named fieldname.
local x = group:new('mytype') -- create an instance of mytype
--> x = { 'mytype', '?', '?', '?', '?', '?', '?', '?' }
-- set some values:
x:set('b', true) -- set the x.b field
x:set('s', 'this is s') -- set the x.s field
x:set('arr', 12, 25, 41) -- set (part of) the x.arr field
--> x = { 'mytype', true, 'this is s', 12, 25, 41, '?', '?' }
-- get some values:
print(x:get('*')) --> true 'this is s' 12 25 41 ? ? (the whole type)
print(x:get('b')) --> true
print(x:get('s')) --> 'this is s'
print(x:get('arr')) --> 12 25 41 ? ? (the whole array)
print(x:get('arr.2') --> 25 (only the 2nd element)
-- copy a whole array from an instance to another:
local y = group:new('mytype')
y:set('arr', x:get('arr'))
--> y = { 'mytype', '?', '?', 12, 25, 41, '?', '?' }
The above set/get methods do not perform any check on the validity of the values that are written to or read from the fields. A somewhat safer access can be achieved by using the following methods, that however can be used only to access terminal fields (that is, fields of primitive types). When these methods are used, the value first tested for consistency with its type, and a Lua error( ) is raised if the test fails.
-
value = instance:tget(fieldname)
Safely reads the terminal field named fieldname.
Finally, the two methods that follow are provided to conveniently access instances of primitive types, that is, instances that have a single terminal field. These methods are equivalent to the tset/tget methods, but the fieldname is here implicit.
Type definition syntax
MoonTypes allows for the definition of structured types starting from primitive types, using a C-like typedef construct (see the PEG declaration syntax below). The syntax supports the struct and union constructs, arrays, and nesting.
Each type has a size, defined as the number of terminal fields it contains. The size determines the length of any instance of that type.
Primitive types (listed in the next subsection) are the types of terminal fields. They all have size 1, with the notable exception of the void type [2] that has size 0.
Derived types are the structured types obtained by combining already defined types with the typedef construct. The size of a derived type follows from its definition, and may be 0, 1, or greater.
Types are added to a group by supplying their definitions to the group:typedef( ) method in a string (or a text file), and using the syntax described by the following PEG:
typedecl <- 'typedef' sp+ basetype sp+ Name array* sep? basetype <- 'void' / structorunion / Type array <- sp* '[' sp* Size sp* ']' structorunion <- ('struct' / 'union') sp* fieldlist fieldlist <- '{' sp* field (sep field)* sep? '}' field <- basetype sp+ FieldName array? sp <- ' ' / '\t' / '\n' sep <- (sp* (',' / ';') sp*) / sp+
Here Size
is a positive integer, while Name
, FieldName
and Type
are identifiers
composed only of letters, digits and underscores, and not starting with a digit
(Type
must be the name of an already defined type, primitive or derived as well).
The syntax essentially boils down to the syntax for type declarations in C, with few differences.
Unlike in C, typedefs and fields in structs/unions can be separated by just whitespaces
(sequences of spaces, tabs and newlines). The separators ,
and ;
may be optionally
used, but they are not required.
Also, this is not shown in the PEG but the syntax tolerates end-of-line Lua comments (-- …
),
C comments (/* … */
), C++ comments (// …
), and lines starting with a #
(like for example C preprocessor directives).
These are all ignored by MoonTypes when parsing the type definition string or file.
Primitive types
A MoonTypes group, when created, has the following primitive types pre-defined in it:
-
Lua’s native basic types with the exception of nil,
-
the void type (a type having no values),
-
a few constrained numeric types (bit, char, short, int, etc.), and
-
the bitstr and hexstr types (strings representing binary and hexadecimal data).
Beware that fields of the Lua types table, function, thread, and userdata are stored in instances by reference.
The table below contains the full list of pre-defined primitive types, together with a description of the values they admit.
Type | Size | Values |
---|---|---|
void |
0 |
none |
boolean |
1 |
any Lua boolean |
number |
1 |
any Lua number |
string |
1 |
any Lua string |
function |
1 |
any Lua function |
table |
1 |
any Lua table |
thread |
1 |
any Lua thread |
userdata |
1 |
any Lua userdata |
bit |
1 |
a Lua number that can be either 0 or 1 |
char |
1 |
any Lua number representing an 8-bit signed integer (-128 ≤ x ≤ 127) |
uchar |
1 |
any Lua number representing an 8-bit unsigned integer (0 ≤ x ≤ 255) |
short |
1 |
any Lua number representing a 16-bit signed integer (-215 ≤ x ≤ 215-1) |
ushort |
1 |
any Lua number representing a 16-bit unsigned integer (0 ≤ x ≤ 216-1) |
int |
1 |
any Lua number representing a 32-bit signed integer (-231 ≤ x ≤ 231-1) |
uint |
1 |
any Lua number representing a 32-bit unsigned integer (0 ≤ x ≤ 232-1) |
long |
1 |
any Lua number representing a 64-bit signed integer (-263 ≤ x ≤ 263-1) |
ulong |
1 |
any Lua number representing a 64-bit unsigned integer (0 ≤ x ≤ 264-1) |
float |
1 |
any Lua number representing a single precision floating point |
double |
1 |
any Lua number (representing a double precision floating point) |
bitstr |
1 |
any Lua string containing only the characters '0' and '1' |
hexstr |
1 |
any Lua string, of even length, containing only characters from the ranges '0-9', 'a-f' and 'A-F' |
Additional primitive types may be added to a group using the following method, prior to parsing the type definition strings or files that use them:
-
group:primitive(name, checkfunc)
Defines an additional primitive type named name (a string). The checkfunc argument must be a function accepting a value and returning true if it is a valid value for the newly defined type, or false otherwise.
local group = types.group()
-- Define a primitive type named 'mynumber', constrained between 0 and 1 inclusive
group:primitive("mynumber", function(x) return type(x)=="number" and x>=0 and x<=1)
Miscellanea
This section describes some additional methods for group and instance objects.
-
np = group:np()
Returns the NP value ('not present') for the group.
-
size, first, last, descr = group:sizeof (typename, [fieldname])
size, first, last, descr = instance:sizeof ([fieldname])
Returns the properties of the field fieldname in the type typename, or in the instance's type:
- size: the number of terminals the field is composed of,
- first, last: the indices of the first and last terminals in the instance table,
- descr: a string describing the structure of the field.
If fieldname is not supplied, it defaults to '*', meaning the outermost field.
-
string = instance:tostring([fieldname], [sep])
instance:print([fieldname], [sep])
Returns/prints a string with the full name of the field fieldname, followed by its terminal values.
If fieldname is not supplied, it defaults to '*', meaning the outermost field.
The optional sep parameter is the separator to be used when concatenating the terminal fields, and defaults to a single space.