Lua logo

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

MoonTypes requires Lua version 5.3 or greater, and LPeg.

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.

Example: defining types
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.

Example: instantiating a type
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.

Example: reading and writing the fields of an instance
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.

  • instance:tset(fieldname, [value])
    Safely writes value to the terminal field named fieldname..
    The supplied value must be a valid value for the terminal field’s type, or the NP value ('not present').
    If value is not supplied, the field is reset to the NP value.

  • 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.

  • instance:pset([value])
    value = instance:pget( )
    Same as tset( ) and tget( ) called with fieldname='*'.
    The instance must have a single field, and that field must be of a primitive type (or an alias).

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.

Table 1. Primitive types
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 (-215x ≤ 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 (-231x ≤ 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 (-263x ≤ 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.

Example: a custom primitive type
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.


1. This manual is written in AsciiDoc, rendered with AsciiDoctor and a CSS from the AsciiDoctor Stylesheet Factory.
2. The void type is convenient for messages, or signals, carrying no other information content beside their arrival.