Fluid Reference Manual

Fluid is a Lua-based scripting language, specially customised for integration with the Parasol Framework. It supports garbage collection, dynamic typing and a bytecode interpreter for compiled code. Lua was chosen as our preferred programming language due to its extensive popularity amongst game developers, a testament to its low overhead, speed and lightweight processing when compared to common scripting languages.

Full compatibility with Parasol's C++ APIs is included. This includes support for all common constants found in the C headers (e.g. error codes, action ID's and special flags). Object classes are self-describing, so the need for external libraries or header files to communicate with a given type of object is unnecessary under normal circumstances. Module functions are also self-describing, offering the same level of access to function calls as one would expect when programming in C.

This manual expands on the information in the existing Lua Reference Manual, and covers features that are exclusive to Fluid. The official Lua 5.1 Reference Manual is required reading before you continue with this document, as a working knowledge of the Lua language is assumed.

Further Reading

For more information on the usage of available classes and modules, please refer to the Parasol API at www.parasol.ws.

For general information on the syntax provided by Lua, please read the following online manuals:


Usage

To run a Fluid script, use the parasol executable:

parasol myfile.fluid

Named arguments can be passed to the Fluid script by following the script location with a series of variable values:

parasol myfile.fluid name="John" surname=Bloggs

To debug a Fluid script, use the --log-api parameter.

For further information on available options, execute parasol with the --help parameter.

File Recognition

Fluid files can use the file extension .lua or .fluid for identification. Ideally, scripts should start with the comment -- $FLUID near the start of the document so that they can be correctly identified by the Fluid parser.


Lua Extensions

A number of extensions have been added (and some features removed) to Lua 5.1 in order to add value to the Fluid language. This section examines the major changes in regards to the published Lua Reference Manual.

Removed Features

The Package, IO and OS libraries normally found in Lua are removed as they duplicate features already found in Parasol's Script, File and Time classes. The introspective debug library is also removed for the purpose of reducing size.

Bitwise Operations

Bitwise operations are supported via the [https://bitop.luajit.org/api.html](bitop API).

Comments

Single-line C++ style comments are supported in addition to Lua's commenting standard. For example:

// This is a C++ style, single line comment.

--[[ This is a Lua comment --]]

Script Parameters

Arguments passed to the Fluid script can be accessed via the arg() function. In the following example either 'width' is returned or 1024 otherwise:

width = arg('width', 1024)

All arguments are managed as strings regardless of the type of the value.

Non-Zero Value Detection

A non-zero value detection function, nz(), is included to simplify the handling of nil and zero values (such as a zero length string). Using nz() simplifies program flow where variables must have a value, for example:

myvalue = nz(var.shoppinglist, 'Empty Basket')

The first parameter is checked if it is empty. If so, the function's second parameter is returned if provided. Otherwise a value of true is returned.


Script Management

include

The include statement is used to load the definitions for an API's functions and classes. It accepts a series of API names as input, and will load each in sequence, for example:

include 'core','xml','display'

API interfaces are protected from being loaded more than once. Calls to mod.load() result in API definitions being loaded automatically, and may make a call to include being unnecessary in that case.

require

The require statement will load a Fluid language file from the scripts: volume and then executes it. It differs from loadFile() in that the script will be registered so as to prevent multiple executions, and the volume restriction improves security.

loadFile()

Use loadFile() to load, parse and execute a Fluid script. This feature is useful for re-using code, or breaking a large project into multiple script files. Example:

loadfile('programs:tools/project/example.fluid')

If the path is not fully qualified, the current path of the process will be used to determine the location of the source file.

If the executed script ends with a return statement, the value(s) will be returned from loadFile() in their original form.

exec()

Use exec() to parse a Fluid statement and execute it. Can raise an exception if the statement is unparseable or fails during execution. Example:

exec('print("Hello World")')


Exception Handling

Calls to the Parasol API do not raise exceptions by default, instead opting to return ERR codes that can be managed by the client (or ignored). Promoting ERR codes to exceptions can be a useful at times, so some functionality is provided to enable this feature as seen in the functions described below.

check()

check() is the equivalent of an assert() for ERR codes. Any non-safe error code passed to this function will be converted to an exception with a readable message that matches the ERR code. It is most powerful when used in conjunction with the catch() or pcall() functions, which will apply the line number of the exception to the result. The code will also be propagated to the Script object's Error field if you are calling the script from C++.

raise()

raise() will raise an exception immediately from an ERR code; for example raise(ERR_Failed). Unlike check(), all codes have coverage, including minor codes. The error code will also be propagated to the Script object's Error field if called from a C/C++ program.

error()

error() is the standard Lua function for raising exceptions and this is also the case in Fluid. It should be used when raising an exception with a custom error message, e.g. error('I have failed.').

catch()

Use catch() to switch on exception handling for functions that return ERR codes, as well as regular exceptions that would otherwise be caught by pcall(). Affected code includes: obj.new(); any module function that returns an ERR type; and any method or action call.

catch() is typically called with two parameters - the first being a custom function that will be executed, and the second a function handler that will be activated if an exception is raised. The handler will receive an Exception parameter that contains fields that describe what happened. The fields are code for the ERR code (if any); message describes the exception; line is the line number that raised the fault.

catch(function()
    -- Code to execute
end,
function(Exception)
    print('Code: ' .. nz(Exception.code,'LUA') .. ', Message: ' .. Exception.message)
end)

Sometimes it can be convenient to catch only specific error codes of interest, which can be achieved by listing them just prior to the handler reference:

catch(function()
    -- Code to execute
end,
{ ERR_Failed, ERR_Terminate }, -- Errors to catch
fuction(Exception)
end)

Calling catch() without an exception handler will ignore raised exceptions, instead returning the Exception table for further inspection:

local exception = catch(function()
    -- Code to execute
end)

In all other cases the ERR code is returned by default.

Be aware that the scope of catch runs deep, and extends into any functions that are called. Using catch() over a broad section of code is considered mis-use, and can lead to unexpected confusion. Consider using xpcall() instead when broad exception handling is desired.

The following ERR codes are not treated as exceptions:

Okay, False, LimitedSuccess, Cancelled, NothingDone, Continue, Skip, Retry, DirEmpty.


Module Interface

The module interface provides a means for Fluid programs to communicate with Parasol's APIs. They are loaded using the mod.load() function, for instance:

mGfx = mod.load('display')

Calling mod.load() returns the function table for the requested module, allowing calls to be made to its functions. For instance:

local err, x, y = mGfx.getCursorPos()

Notice that Fluid is capable of returning multiple results if a function has declared more than one return value. Typically the first value will be an ERR code.

For consistency, we use a set of commonly named global variables to store module function tables. This is because loading an API more than once is undesirable. The following table illustrates the global names that we use for the default set of APIs.

Module Global Name
audio mAudio
code mSys
display mGfx
font mFont
network mNet
vector mVec
xml mXML

Taking the above into account, our first example should now be written as follows:

if not mGfx then mGfx = mod.load('display') end

Note that the Core module, identified by the mSys global variable is considered an essential API and is always available by default.

Type Conversion

When calling an API function, Fluid uses best efforts to convert function values to the types declared in the function definition. For instance, if a string value is used for a numeric argument, Fluid will automatically convert the string to an integer. If the type cannot be converted (for instance, an abstract pointer cannot be converted to a number) then a type mismatch occurs and an exception will be raised.

While type conversion is convenient, we recommend using the correct type whenever possible as this will ensure that your calls are being made efficiently.

Buffer Handling

When calling functions that copy results to a user-supplied buffer, pass an array interface. The following example illustrates reading content from a file into an array buffer:

buffer = array.new(file.size, 'byte')
err, result = file.acRead(buffer)
print(tostring(buffer))

Notice that the acRead() call hasn't been given its second parameter (the amount of data to read). This is possible because Fluid knows the size of the buffer and will use it for the second parameter if not already defined.

Multiple Result Values

Some functions may return multiple result values. Here is an example of a module function that returns two results:

ERR ListMemory(LONG Flags, struct ListMemory **Memory)

The first result is an ERR code. The second result is stored in the variable pointed to by the Memory argument. In a C program, this function would be used as follows:

if (!ListMemory(0, &memory)) { ... }

In Fluid, result-pointer arguments are excluded from the function specification, as the ability to write to variable addresses is a risk to stability. Instead, such arguments are handled internally and their values are returned as part of the result set. The following example shows how to call ListMemory() in Fluid:

local err, list = mSys.ListMemory(0)

Some functions like the above can return allocated memory or some other form of temporary resource. These are normally marked as such in the function definition, which allows the garbage collector to automatically remove it once its references have been reduced to zero. Remember to use local wherever possible to clean up resources that have gone out of scope.


Object Interface

The object interface provides the necessary functionality to create and manage objects. It allows you to read and manipulate object fields, call methods and actions, manage subscriptions and connect to existing named objects.

Object Creation

To create a new object, use the object interface's new() method with the name of the class that will be instantiated. Here's an example that creates an object, sets necessary field values and initialises it:

file, err  = obj.new('file')
file.path  = 'readme.md'
file.flags = '!READ'
err = file.acInit()

The first result from new() is the created object's interface and the second result is an ERR code if creation fails. Take note that an object must always be initialised before any real interaction occurs (anything beyond setting field values).

The above example can be greatly simplified by providing a field list immediately after the class name:

file = obj.new('File', { path='readme.md', flags='!READ' } )

In this case the path and flags values will be set on our new object and then initialised automatically (it is assumed all field settings are being defined up-front). The field values may be of any type and Fluid will use best efforts to convert them to the correct field type.

In the event of an error, new() will return nil as the first result and does not throw an exception. We advise guarding against problems by testing the result or by using catch().

Object Relationships

Parasol includes an integrated OO feature that allows objects to be attached to each other so that they form parent, child and sibling relationships. By default, all objects created by obj.new() are owned by the Fluid script (which exists as a runtime object).

Sometimes a new object will need to be declared as the child of an existing object. Imagine for instance, that we have a window open and we want to draw a rectangle to it. This can be achieved with something like:

viewport = glWindow:clientViewport({ aspectRatio = ARF_MEET })
viewport.new('VectorRectangle', { x=0, y=0, width='100%', height='100%', fill='rgb(255,0,0)' })

Notice that the call to viewport.new() looks suspiciously like an obj.new() call. Functionally they are identical, but the key difference is that the rectangle is now going to be a child of the viewport instead of our script. When the rectangle is initialised, it will determine that it is owned by the viewport and will appear in that context when displayed.

It is the case that all objects support the new() method for the purpose of supporting these complex hierarchies.

Be aware that object relationships have absolute priority over other factors in determining their lifetime. In the above example, if the viewport is destroyed (e.g. the user closes the window) then the rectangle will be destroyed with it because its state is linked to the parent. If an object is loosely coupled to a script due to such circumstances, you can call the exists() method at any time on the object's interface to confirm it is not destroyed.

object.children()

You can get a list of the children that have been attached to an object by calling the children() method. It returns an array of UID's that could then be converted to interfaces with obj.find(). The children() method also supports a class filter in the first parameter, e.g. children('VectorRectangle') would return a list of all rectangles.

Accessing Objects

It is possible to access the interface of an existing object by searching for its name (if given one on initialisation) or UID:

window = obj.find('my_window')
winsurface = obj.find(window.surface)

If failure occurs, nil is returned by the find() method. If multiple objects exist with the same name, the most recently created object is returned first.

obj:lock()

If an object is being shared in a threaded program, locking may be necessary to prevent data corruption. This can be achieved by calling lock() and passing a function that will gain an exclusive lock on the object. The lock will remain until the function returns.

obj:lock(function()
   --Code--
end)

Garbage Collection

The Lua garbage collector will automatically terminate an object once all references to that object are released or the script is terminated. If an object has a single reference, you can manually terminate it by setting the reference to nil, for example:

file = obj.new('File')
...
file = nil

In some cases you may wish to create an object that remains after the script has executed and is not removed by scope rules. To do this, use the detach() method to unlink the object from the garbage collector. The object can still be destroyed manually via a call to its free() method.

Action and Method Calls

After successfully initialising an object interface, interaction with it is possible through actions, methods and fields. The following example illustrates how we could change the size of the VectorRectangle we created earlier:

rect.acRedimension(20, 20, 0, 100, 100, 0)

Action calls are identified by their ac prefix. Method calls are prefixed by mt. Field references can be directly accessed without a prefix. The naming scheme for methods, actions and fields is case insensitive.

For information about the actions and methods supported by an object class, refer to its published API documentation on our website.

Field Interface

Object fields are accessible by conventional means, i.e. thing = my_object.field_name and my_object.field_name = thing for get and set respectively. There are get() and set() methods that are equivalent to these, and they can be useful in special circumstances such as retrieving a value from an array-based field without first retrieving the entire array:

item = view.get('item(10)')

Some classes support the concept of variable fields, which are unrestricted custom key values that can be added to an object by the client. The getVar() and setVar() methods have been provided to support this feature. For instance:

script.setVar('apple', 'banana')
banana = script.getVar('apple')

Bit Fields

To test for the presence of a bit in a flag-based field, the following template supported by get() can be used:

state = object.get('flagfield.flagname')

For example:

state = surface.get('flags.visible')

If the named flag is set, a value of 1 is returned, otherwise 0.

To get the value of a flag or lookup field in string format, prefix the name of the field with a $ symbol when using the get() method as follows:

name = object.get('$flags')


Action & Method Subscriptions

Parasol allows clients to monitor the functional execution of an object by subscribing to its actions and methods. One commonly used strategy involves subscribing to the free() action of an object so that the client is notified of object termination.

Subscription is enabled via the subscribe() method. The following example illustrates the creation of an HTTP object that performs a download and calls my_function() on completion of the download process. Note that Args is a table that contains named arguments for the action that was subscribed to. The CustomRef is an optional value that will be passed to the subscriber.

function my_function(UID, Args, CustomRef)
   print('Download complete.')
end

http = obj.new('http', { ... } )
http.acActivate()
handle = http.subscribe('deactivate', my_function, customref)

To unsubscribe from an action, call unsubscribe() with the action/method name. Doing so will allow the garbage collector to remove associated resources that are no longer required.


Processing Interface

The processing interface assists with the management of your program's idle state and reaction to signals. Its most basic feature is the commonly used sleep() function, as follows:

processing.sleep()

Calling sleep() in this way will put the script to sleep until a message is received to awaken it. Typically this means that the function won't return until a QUIT message is received, which would be delivered if the user opts to close the main window.

sleep() can also return early if a timeout in seconds is specified in the first parameter.

Signals

If a Fluid script is utilising threads, synchronisation can be an important issue that requires careful management. The processing interface makes it possible to put the main thread to sleep and wake it once a series of signals has been triggered. In the following working example, we create a basic thread that prints a message and then signals two XML objects in order to wake the main thread. This might be plausible if we were asking the thread to process XML data for instance:

function testSignals()
   local signal_a = obj.new('xml', { flags='NEW' })
   local signal_b = obj.new('xml', { flags='NEW' })

   -- Note that the thread code is parsed as a string and can't see any variables without a call to obj.find().
   -- The termination handler on the other hand has access to signal the objects directly.

   thread.script([[
      msg('Thread is now in session.')
   ]],
   function()
      msg('Thread has been executed.')
      signal_a.acSignal()
      signal_b.acSignal()
   end)

   local proc = processing.new({ timeout=1.0, signals = { signal_a, signal_b } })
   msg('Sleeping....')
   local err = proc.sleep()
   assert(err == ERR_Okay, "Unexpected error: " .. mSys.GetErrorMsg(err))
end

Notice that we called processing.new() to create a dedicated interface for signal management. The two objects that will be signalled are passed in via the signals field. When the thread completes, it calls acSignal() on the objects to change their signal state. It is necessary for both objects to change state in order to wake the main thread.

signal()

If no signal objects are listed in a call to processing.new(), it is possible to manually call the signal() method to wake the sleeping thread instead. This simple technique is sometimes used in passive event systems, e.g. the user clicking a mouse button could result in a signal() call that wakes the main thread.


Thread Interface

Fluid supports a simplified threading model so as to minimise the potential problems occurring from their use. The functionality is as follows:

thread.script(Statement, Callback)

The script() method compiles a statement string and executes it in a separate script state. The code may not directly share variables with its creator, but it can find existing objects and interact with them by calling obj.find().

The Callback parameter is a function that will be executed once the threaded script has returned. It executes in the space of the caller and not the thread itself, which means it can access local variables safely.

thread.script([[
   print('Thread is now in session.')
]],
function()
   print('Thread has finished executing.')
end)

thread.action|method(Object, Action, Callback, Key, Args...)

The thread.action() and thread.method() interfaces provide a convenient means of executing an object function in a separate thread. There is some overhead in executing a new thread, but this strategy becomes highly effective when used to perform lengthy processes such as the loading and parsing of data.

Here's an example that reads the first 1Kb of data from a file and prints the content:

function on_complete(Action, File, Error, Buffer)
   print('Read string: ' .. Buffer)
   File.free()
end

str_buffer = string.rep(1024)
thread.action(file, AC_Read, on_complete, str_buffer, str_buffer)
processing.sleep()

The Action parameter is accepted as an action ID (fast) or a string (slower). The Key makes it possible to pass a custom parameter to the callback, and can be used to pass multiple parameters if a Lua table is used.

The Callback routine receives notification of the thread's completion, and in this example uses this as an opportunity to free the file object. This is a recommended pattern, and ensures that the target object is not destroyed while the thread is executing. Never declare the object value as local.

Internally, callbacks are executed on the next message processing cycle and this is why a call to processing.sleep() is used in this example.

The Error parameter in the callback reflects the ERR code returned by the action - but bear in mind the possibility that if thread preparation fails, the callback will never be executed and an exception will be thrown instead.


Structure Interface

The structure interface is provided so that Fluid can use C/C++ structures declared in the Parasol API. You will find that many class and module API's use structures for returning condensed information.

Creating a Structure

Structures are created using the struct interface's new() method. The prototype is as follows:

newstruct = struct.new(struct_def, [address])

The struct_def is a reference to a named structure definition declared in the Parasol API, or one returned from MAKESTRUCT(). The address is an optional setting for linking a struct to an existing pointer rather than allocating an empty memory block for the structure. Example:

xmltag = struct.new('XMLTag', address)

It is possible to manually change the address pointer that is assigned to a struct - this saves on having to create a struct object for every address to be processed. Example:

oldaddress = xmltag.ptr(NewAddress)

To retrieve the current address:

address = xmltag.ptr()

Note that nil is returned if the address is set to zero.

Additional Functionality

The byte size of a structure can be retrieved with the structsize() method, for example:

print('The size of the structure is ' .. xmltag.structsize())

To get the total number of fields in the structure, use #, e.g. #xmltag.

Custom Structures

The MAKESTRUCT() function is used to build structure definitions. A structure definition is a single string that defines all fields in a structure, matched in the order in which they appear in the structure. Consider the following C structure:

struct XMLTag {
   LONG   Index;
   LONG   ID;
   struct XMLTag *Child;
   struct XMLTag *Prev;
   struct XMLTag *Next;
   APTR   Private;
   struct XMLAttrib *Attrib;
   WORD TotalAttrib;
   UWORD Nest;
};

To define this structure in Fluid we would use the following code:

MAKESTRUCT('XMLTag', 'lIndex,lID,pChild,pPrev:XMLTag,pNext:XMLTag,pPrivate,pAttrib,wTotalAttrib,uwNest')

You will notice that each field name is defined using the same order and names as identified in the structure. Each name is prefixed with a lower-case character that indicates the field type. Using the correct field types is the most crucial part of the structure definition and each must be chosen from the following table:

Character Field Type
l Long
d Double
x Large
f Float
w Word
c Char
p Pointer. You can use ':StructName' as a suffix to reference other structures.
s String
o Object (Pointer)
u Unsigned (Use in conjunction with a type)

Fixed arrays are also permitted in the structure definition if a field name is followed with enclosed square brackets that contain the array size, e.g. [12].


Array Interface

The array interface is used to manage the arrays that are returned by object fields, actions, methods and certain functions. Although Lua includes native support for arrays, their functionality is particular to the Lua environment. Arrays for use within the Parasol API must use the interface that we provide.

To create an array, use the new() method with the following prototype:

myarray = array.new(size, type)

The size indicates the total number of entries in the array. The type is a string that declares the type to use for each element in the array. Supported types are byte, word, long, large, string, ptr, float and double.

Strings can also be converted into an array of bytes using the special bytestring type, as in the following template:

myarray = array.new('enter your string here', 'bytestring')

Array Indexes

Array elements can be retrived using the Lua syntax for array referencing:

var = myarray[20]

The total number of elements in the array, as defined on the array's creation, can be retrieved by using the '#' prefix:

total = #array

array.getstring()

string = myarray.getstring(Start, [Length])

The getstring() method is available for byte arrays only. It converts part of the array into a Lua string. If the Length is unspecified then the full length of the array is retrieved as a string.

array.copy()

array.copy(Source, [DestIndex], [TotalElements])

The copy() method is used to copy the data from Source to the array, targeting DestIndex. The Source can be either a string type or another array.


Logging

The following functions are included as standard so that messages can be logged to the console.

msg()

msg() accepts a single string parameter and prints it to the debug log. The log level must be set to api or higher in order to be visible.

print()

print() accepts a single string parameter and prints it to stderr. If stderr is unavailable on a system like Android, the message is printed to the debug log instead.


Event Subsystem

Fluid programs can receive system-wide events whenever they are broadcast. Two functions are provided for the purpose of event management. The first is subscribeEvent(), which will connect a Fluid function to a specific event:

error, handle = subscribeEvent(eventname, function)

The eventname is a string that must follow the format group.subgroup.name, for example system.task.created. Valid event strings are described in full detail in the Events Manual of the Parasol SDK. A single asterisk wildcard is allowed in the subgroup and/or name if listening to multiple events is desirable, for example system.*.* would listen for all system events. The referenced function will receive two arguments if the event is signalled - EventID and Args. The Args parameter is a table containing named parameters - if the event does not include any parameters then the table will be empty.

To unsubscribe from an event, call unsubscribeEvent() with the event handle that was returned by the initial subscribeEvent() call:

unsubscribeEvent(handle)


Credits

Fluid is developed by Paul Manias and runs on Mike Pall's LuaJIT framework, which in turn is based on the Lua programming language.

Lua is designed and implemented by a team at PUC-Rio, the Pontifical Catholic University of Rio de Janeiro in Brazil. Lua was born and raised at Tecgraf, the Computer Graphics Technology Group of PUC-Rio, and is now housed at Lua.org. Both Tecgraf and Lua.org are laboratories of the Department of Computer Science.