Parasol In Depth

In this document we get to grips with Parasol's object oriented design and how it differs to other systems and frameworks that exist today. We'll start with the basics, but the latter parts of this document may assume that you have installed the Parasol Framework, have successfully run some examples and perhaps experimented with some of the code.


Table of Contents

  1. Design Overview
  2. Modules
    1. Module Initialisation
    2. Exporting Functions
  3. Classes
    1. The MetaClass
    2. Class Specifications
    3. Fields
      1. Field Types
      2. Field Access Management
      3. Virtual Fields
    4. Local Objects and Inheritance
    5. Actions (Contract Interfaces)
    6. Methods
    7. Sub-Classes
    8. Spec Design

Design Overview

Parasol follows a modular design. It has a Core API that must be initialised when a program starts, and provides essential API calls that are needed for a functional development environment. Non-essential features are broken out into API modules (libraries) that manage a specific feature-set, such as audio, graphics and data management. Access to the core and modules is achieved via language header files that are auto-generated during the build process.

The exact manner in which the system is boot-strapped is a choice for client, as the Parasol build process is highly configurable. Generally a build will either function as an all-purpose distribution (such as the official release) or a static, custom build that is compiled for a specific program.

Parasol's base-level design objectives are:

  • Provide a modern object oriented API that does not alienate programming languages that need to interoperate with our services.
  • Provide multimedia support and a first-class UI that uses vector graphics exclusively.
  • Describe our APIs with an IDL (Interface Definition Language) so that headers can be generated for any programming language.
  • The build process must be fully configurable, with the option to leave out features that are not required by the client.
  • The Parasol environment must be fully self-contained so that programs are portable and easily compiled for new targets. Calls to the host system, such as Windows or Linux should not be necessary for the majority of client programs.

Framework Initialisation

The src/link folder contains platform-specific code for initialising and shutting down the Parasol Framework. The init-unix.cpp and init-win.cpp are such examples for Linux and Windows, both of which export init_parasol() and close_parasol() functions. Calling these from any executable is sufficient for opening and closing the framework. To see a working project example, please refer to the src/launcher folder and read the CMakeLists.txt and parasol.cpp files.

If a build isn't static, the initialisation process will start by finding the Parasol core.so or core.dll binary first. Priority is given to the local folder from which the executable is running. If no installation is found then a check will be made for a global installation, such as in /usr/local/ before reporting a failure.

Closing the framework is a clean process, which is to say that all resources will be terminated so that the process is returned to its original state. This means that opening and closing the Parasol Framework on an ad-hoc basis is viable if desired.

Modules

Parasol libraries are referred to as modules and depending on the desired target, can either be built as dynamic shared libraries or statically compiled into an executable. There is no restriction on the choice of programming language when creating a module, but native modules in the framework are written in C++ for consistency.

Modules can export a library of API functions, one or more classes, or a mixture of both. Exports are managed in the initialisation of the module. There is also a provision for a destructor function that is called during Parasol's 'expunge sequence', which allows modules to unload their resources safely. This is a useful way of detecting resource leaks that might otherwise go unnoticed.

Before we discuss the code, here's a look at the CMake file for the XML module:

set (MOD xml)
set (INC_MOD_XML TRUE PARENT_SCOPE)

idl_gen ("${MOD}.fdl" NAME ${MOD}_defs
   OUTPUT "${INCLUDE_OUTPUT}/modules/${MOD}.h"
   APPEND_IDL "${MOD}_def.c")

add_library (${MOD})
set_module_defaults (${MOD})
target_sources (${MOD} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/${MOD}.cpp")

flute_test (${MOD}_tests "${CMAKE_CURRENT_SOURCE_DIR}/tests/test.fluid")

The idl_gen() call will generate automated headers and documents from the xml.fdl file, which contains the public specifications of the module. This process is further discussed in the FDL manuals in this Wiki.

The add_library() call registers the module with the build process and set_module_defaults() pre-configures the build for the named module. The target_sources() call associates the cpp source files with the module.

The flute_test() references a Fluid script that will test the module's functionality; this is further discussed in the Unit Testing manual in this Wiki.

Module Initialisation

When the Core API loads a module for the first time, it needs an accessible way to initialise the module. In C++ this is achieved with the PARASOL_MOD() macro, which creates a publicly accessible table that the Core can interact with. The parameters that can be passed to the macro are:

Parameter Description
Init The initialisation routine, called when the module is loaded for the first time. Declared as static ERR Init(OBJECTPTR, struct CoreBase *)
Close The close routine (optional). Called on each occasion that a client closes the module. Declared as static void Close(OBJECTPTR)
Open The open routine (optional). Called on each occasion that a client opens the module. Declared as static ERR Open(void)
Expunge The expunge routine. Global resources allocated by the module must be deallocated during the expunge. Declared as static ERR Expunge(void)
IDL Reference to a compiled IDL, as generated from the module's .fdl file
Structures A list of STRUCTS, which declares public struct types that are available to clients of the module.
Example: static STRUCTS glStructures = { { "XMLTag", sizeof(XMLTag) } };

Note that it is compulsory that referenced globals use the static keyword, as it prevents naming conflicts in a static build.

Here's the XML module's declaration of PARASOL_MOD():

PARASOL_MOD(MODInit, NULL, NULL, MODExpunge, MOD_IDL, &glStructures)

The Init and Expunge functionality deserves a closer look. The XML module takes a copy of the CoreBase reference, which it needs in order to make function calls to the Core API. It then creates the XML class by declaring its specifications, which is detailed elsewhere in this manual.

The Expunge process must undo all allocations made by the Init process. In this case the XML module will only need to remove the XML class using FreeResource().

static ERR MODInit(OBJECTPTR pModule, struct CoreBase *pCore)
{
   CoreBase = pCore;

   clXML = objMetaClass::create::global(
      fl::BaseClassID(ID_XML),
      fl::ClassVersion(VER_XML),
      fl::Name("XML"),
      fl::FileExtension("*.xml"),
      fl::FileDescription("XML File"),
      fl::Category(CCF::DATA),
      fl::Actions(clXMLActions),
      fl::Methods(clXMLMethods),
      fl::Fields(clFields),
      fl::Size(sizeof(extXML)),
      fl::Path(MOD_PATH));

   return clXML ? ERR::Okay : ERR::AddClass;
}

static ERR MODExpunge(void)
{
   if (clXML) { FreeResource(clXML); clXML = NULL; }
   return ERR::Okay;
}

Exporting Functions

A module can opt to export function interfaces for client programs. This is achieved by adding a hook to the module's Open routine. The following example is from the Network module:

static ERR MODOpen(OBJECTPTR Module)
{
   Module->set(FID_FunctionList, glFunctions);
   return ERR::Okay;
}

The FunctionList declares the list of functions that will be returned to the client. In theory a module can have more than one function table for different clients. Multiple function lists are rarely used, but can help in offering backwards compatibility by checking the requested module version number.

The glFunctions array, excerpt below, is auto-generated from the Network module's network.fdl file and names the available functions, with links to function code. Parameter names and type information is also included. Taking advantage of the available FDL tools to generate this information eliminates the potential for mistakes as well as reducing development time. Critically it also ensures that run-time introspection is available and this enables usage from scripting languages like Fluid.

FDEF argsAddressToStr[] = { { "Result", FD_STR|FD_ALLOC }, { "IPAddress:IPAddress", FD_PTR|FD_STRUCT }, { 0, 0 } };
FDEF argsHostToLong[] = { { "Result", FD_LONG|FD_UNSIGNED }, { "Value", FD_LONG|FD_UNSIGNED }, { 0, 0 } };
FDEF argsHostToShort[] = { { "Result", FD_LONG|FD_UNSIGNED }, { "Value", FD_LONG|FD_UNSIGNED }, { 0, 0 } };
FDEF argsLongToHost[] = { { "Result", FD_LONG|FD_UNSIGNED }, { "Value", FD_LONG|FD_UNSIGNED }, { 0, 0 } };
FDEF argsSetSSL[] = { { "Error", FD_LONG|FD_ERROR }, { "NetSocket", FD_OBJECTPTR }, { "Tags", FD_TAGS }, { 0, 0 } };
FDEF argsShortToHost[] = { { "Result", FD_LONG|FD_UNSIGNED }, { "Value", FD_LONG|FD_UNSIGNED }, { 0, 0 } };
FDEF argsStrToAddress[] = { { "Error", FD_LONG|FD_ERROR }, { "String", FD_STR }, { "IPAddress:Address", FD_PTR|FD_STRUCT }, { 0, 0 } };

const struct Function glFunctions[] = {
   { (APTR)netStrToAddress, "StrToAddress", argsStrToAddress },
   { (APTR)netAddressToStr, "AddressToStr", argsAddressToStr },
   { (APTR)netHostToShort, "HostToShort", argsHostToShort },
   { (APTR)netHostToLong, "HostToLong", argsHostToLong },
   { (APTR)netShortToHost, "ShortToHost", argsShortToHost },
   { (APTR)netLongToHost, "LongToHost", argsLongToHost },
   { (APTR)netSetSSL, "SetSSL", argsSetSSL },
   { NULL, NULL, NULL }
};

For information on how the function tables are generated, refer to Embedded Document Formatting. Manually writing function tables is strongly discouraged as it can lead to the documentation being out-of-sync with the declarations in the module.

Writing a Function

A new module function should always be prefaced with a commented -FUNCTION- declaration as defined in the EDF standard. This must then be immediately followed with the function itself. If the module accepts parameters, include an -INPUT- declaration and likewise for a return code, specify a -RESULT-. Here's a working example for HostToLong() from the Network module:

/***

-FUNCTION-
HostToLong: Converts a 32 bit (unsigned) long from host to network byte order.

Converts a 32 bit (unsigned) long from host to network byte order.

-INPUT-
uint Value: Data in host byte order to be converted to network byte order

-RESULT-
uint: The long in network byte order

***/

ULONG HostToLong(ULONG Value)
{
   return htonl(Value);
}

You might have noticed that a call to HostToLong() from a C++ program would be netHostToLong(). We leave out the net prefix in our module declaration as this is an implementation concern that is specific to the client's chosen language.

The last requirement is to ensure that the function is named in the functionNames() declaration in the module's FDL file. If this is not done then the function will not be exported by our automated tools.

Classes

Parasol classes are defined at run-time, not during the build process as is the case for many OO languages. This is a key design feature that maximises the potential for class introspection and allows dissimilar languages to talk over common OO protocols. Advanced features also include support for design-by-contract, virtual fields, permission based field access and resource tracking.

The MetaClass

New classes are created by initialising MetaClass interfaces that declare new class specifications. While reading this section we advise keeping the MetaClass Manual close by, in case you would like further detail on the fields that are referenced.

The code snippet below from the XML module initialises the XML class:

clXML = objMetaClass::create::global(
    fl::BaseClassID(ID_XML),
    fl::ClassVersion(VER_XML),
    fl::Name("XML"),
    fl::FileExtension("*.xml"),
    fl::FileDescription("XML File"),
    fl::Category(CCF::DATA),
    fl::Actions(clXMLActions),
    fl::Methods(clXMLMethods),
    fl::Fields(clFields),
    fl::Size(sizeof(extXML)),
    fl::Path(MOD_PATH));

Each fl::Field() reference in this list is defining the value of a field in the MetaClass. In C++ we use the fl namespace to define known field names and their types for type-safety reasons. It is not possible to use fl::Name() with a non-string value for instance.

As the XML class is a data oriented class, it sets the FileExtension and FileDescription fields with appropriate metadata values. It also defines its category as DATA. This information can have value for applications such as file managers that attempt to match files to class interfaces. If a class doesn't save or load a specific file type then this information can be omitted.

The Actions, Methods and Fields values define the specifications of the class structure and its functional interfaces. We'll look at these in more detail in the following sections.

Take note of the Size value and its reference to extXML. A class specification must always declare the size of its objects so that they can be correctly allocated on the heap. What is notable here is that the official C++ object for the XML class is declared as objXML, so why extXML? The difference is that objXML is the publicly accessible interface for clients, but we want to be able to store and hide additional information from the client. We can achieve this by leveraging C++ class inheritance as seen in this example from the XML module:

class extXML : public objXML {
   public:
      // Everything that follows will be hidden from the client.
      bool   ReadOnly;
      bool   StaleMap;
      ...
      extXML() : ReadOnly(false), StaleMap(true) { }
};

Consequently, all XML object references in the XML module use the extXML type only.

Class Specifications

Building a class specification that will be accepted by the MetaClass starts with the creation of an FDL file before writing any code. The XML FDL file is too large to insert here, but we can look at the specification for the class itself:

methods("xml", "XML", {
  { id=1,  name="SetAttrib" },
  { id=2,  name="Serialise" },
  { id=3,  name="InsertXML" },
  { id=4,  name="GetContent" },
  { id=5,  name="Sort" },
  ...
})

class("XML", { src="xml.cpp", output="xml_def.c" }, [[
  str Path         # Location of the XML data file
  obj Source       # Alternative data source to specifying a `Path`
  int(XMF) Flags   # Optional user flags
  int Start        # Starting cursor position (tag index) for some operations
  int Modified     # Modification timestamp
  error ParseError # Private
  int LineNo       # Private
]])

The call to class() makes a reference to the xml.cpp file for the purpose of reading additional information from the class' embedded documentation (see Embedded Document Formatting). The generated output file, xml_def.c looks like this:

// Auto-generated by idl-c.fluid

static const struct FieldDef clXMLFlags[] = {
   { "WellFormed", 0x00000001 },
   { "IncludeComments", 0x00000002 },
   ...
   { NULL, 0 }
};

FDEF maSetAttrib[] = { { "Index", FD_LONG }, { "Attrib", FD_LONG }, { "Name", FD_STR }, { "Value", FD_STR }, { 0, 0 } };
FDEF maSerialise[] = { { "Index", FD_LONG }, { "Flags", FD_LONG }, { "Result", FD_STR|FD_ALLOC|FD_RESULT }, { 0, 0 } };
FDEF maInsertXML[] = { { "Index", FD_LONG }, { "Where", FD_LONG }, { "XML", FD_STR }, { "Result", FD_LONG|FD_RESULT }, { 0, 0 } };
FDEF maGetContent[] = { { "Index", FD_LONG }, { "Buffer", FD_BUFFER|FD_STR }, { "Length", FD_LONG|FD_BUFSIZE }, { 0, 0 } };

static const struct MethodEntry clXMLMethods[] = {
   { -1, (APTR)XML_SetAttrib, "SetAttrib", maSetAttrib, sizeof(struct xmlSetAttrib) },
   { -2, (APTR)XML_Serialise, "Serialise", maSerialise, sizeof(struct xmlSerialise) },
   { -3, (APTR)XML_InsertXML, "InsertXML", maInsertXML, sizeof(struct xmlInsertXML) },
   { -4, (APTR)XML_GetContent, "GetContent", maGetContent, sizeof(struct xmlGetContent) },
   ...
   { 0, 0, 0, 0, 0 }
};

static const struct ActionArray clXMLActions[] = {
   { AC::Clear, XML_Clear },
   { AC::DataFeed, XML_DataFeed },
   { AC::Free, XML_Free },
   { AC::GetKey, XML_GetKey },
   { AC::Init, XML_Init },
   ...
   { 0, NULL }
};

#undef MOD_IDL
#define MOD_IDL "s.XMLAttrib:zsName,zsValue\ns.XMLTag:lID,lParentID,lLineNo,lFlags,zeAttribs..."

The generator covers many of our needs and in this case has output relevant constants, methods, actions and the IDL for hooking into languages. The list of field specifications for the MetaClass however is not generated. These need to be defined manually because the level of customisation exceeds the capabilities of FDL. We'll look at how this is done in the next section.

Fields

The structure of a new class is declared by with an array of FieldArray values. Here's the relevant clip from the XML module:

static const FieldArray clFields[] = {
   { "Path",       FDF_STRING|FDF_RW, NULL, SET_Path },
   { "Source",     FDF_OBJECT|FDF_RI },
   { "Flags",      FDF_LONGFLAGS|FDF_RW, NULL, NULL, &clXMLFlags },
   { "Start",      FDF_LONG|FDF_RW },
   { "Modified",   FDF_LONG|FDF_R },
   { "ParseError", FDF_LONG|FD_PRIVATE|FDF_R },
   { "LineNo",     FDF_LONG|FD_PRIVATE|FDF_R },
   // Virtual fields
   { "ReadOnly",   FDF_LONG|FDF_RI, GET_ReadOnly, SET_ReadOnly },
   { "Src",        FDF_STRING|FDF_SYNONYM|FDF_RW, GET_Path, SET_Path },
   { "Statement",  FDF_STRING|FDF_ALLOC|FDF_RW, GET_Statement, SET_Statement },
   { "Tags",       FDF_ARRAY|FDF_STRUCT|FDF_R, GET_Tags, NULL, "XMLTag" },
   END_FIELD
};

Note that the order of the fields and the declared types must match to that of the class() call in the FDL file we referenced earlier. Each entry starts with the field name, type flags and optional get and set functions. A type arg can also be specified as the final value - this only applies for certain type specifiers, such as the FLAGS option.

Field Types

A field must always be associated with a type so that its size can be determined, and ensures that the Core can read and write the value correctly. Primitive types are defined using the FDF bit flags as seen in the following table:

FDF Flag Description
BYTE Field is an 8-bit integer.
WORD Field is a 16-bit integer.
LONG Field is a 32-bit integer.
DOUBLE Field is a 64-bit float.
LARGE Field is a 64-bit integer.
POINTER Field is an address pointer.
OBJECT Field points to an object.
OBJECTID Field refers to an object UID.
LOCAL Field points to a local object.
STRING Field points to a string.
ERROR Field is an ERR code.
RGB Field is a four-byte array representing R,G,B,A values.
FUNCTION Field is an embedded FUNCTION value.
FUNCTIONPTR Field points to a FUNCTION value.
STRUCT The field refers to a C struct as declared in the module's FDL, and the type arg must refer to the name of that struct. Use in conjunction with POINTER.
ARRAY Field is a pointer to an array (the base type being declared by other FDF flags).
UNIT The field type is a display unit, supported by the Unit type. Values are fixed by default, but can be described as scaled with the FD_SCALED type option.

Integer fields are signed by default, but can be made unsigned by adding the UNSIGNED flag.

Primitive field types can be augmented with the following additional flags, many of which will affect run-time behaviour. The FLAGS and LOOKUP options allow Fluid programs to use string values instead of plain constants for instance.

FDF Flag Description
CPP The field is a C++ variant of the declared type. STRING becomes std::string; ARRAY becomes pf::vector<>.
RESOURCE When applied in conjunction with the STRUCT flag, the field value will be treated as a trackable system resource that can be passed to module functions.
SCALED The field supports scaled values, typically represented as a multiplier ranging from 0 to 1.0. For use with the UNIT indicator only.
ALLOC Field is allocated on demand, and terminated with FreeResource(). Must be used in conjunction with a get function.
FLAGS The field represents bit-flags. The type arg must refer to an array of FieldDef structs that define the flags.
LOOKUP The field represents an integer value that corresponds to a name in a lookup table. The type arg must refer to an array of FieldDef structs that define the available lookup names.
SYNONYM Declares the field as a synonym (an alternative name for another field). Synonyms are accessible to the client but will not appear in documentation in order to limit repetition. Can be used to maintain backwards compatibility when a field name is changed.
SYSTEM The field exists for system use only, and will not be documented for client use. If possible, clients should be prevented from accessing the field.

Field Access Management

Access to field values can be controlled by specifying read/write permission flags. These permissions are included in auto-generated documentation to communicate the level of access to developers.

Compiled languages like C++ are not able to warn the developer of permission breaches if the developer has direct access to the class declaration. Integrated languages like Fluid however can throw an error, making the inclusion of access controls particularly worthwhile.

The available permission flags are as follows:

FDF Flag Description
READ/R Field is readable.
WRITE/W Field is writeable.
INIT/I Field is writeable, but only prior to initialisation.
RW Synonym for `READ
RI Synonym for `READ

Virtual Fields

A virtual field is one that is accessible to the client via software controlled interfaces. Virtual fields helpfully eliminate the need for 'get' and 'set' methods that often pollute the namespace of OO classes, which makes for a cleaner interface.

There are two ways to declare a virtual field, the first being to explicitly associate the field type with the FDF_VIRTUAL flag. The second is to match the R/W permission flags with get and set functions. For example if the FDF_RW flag is used and both of the get and set functions are defined, the field is automatically considered virtual.

Some fields are partially virtual, for instance the XML Path uses a set function to allocate and store the path string, yet direct reading of the stored value is permitted. This is done when there is no advantage from hiding a field behind an accessor.

When writing a get function, the general function prototype is ERR GET_FieldName(*Object, T *Value) where T is the C++ equivalent of the field's type. Set is similar, being ERR SET_FieldName(*Object, T Value). When getting a field, the Value pointer must be populated on return unless an error is being reported.

If a field is declared with the UNIT option then T is Unit and contains both type and value information. When writing a get function for a unit, the Type must be checked to verify what the client is requesting and the value then written to the Unit.Value field. The following example illustrates:

static ERR GET_Dimension(extMyClass *Self, Unit &Unit)
{
   DOUBLE return_value = ...calculated value...;
   if (Unit.scaled()) return_value /= Self->ParentHeight;
   Unit.Unit = return_value;
   return ERR::Okay;
}

Note that if the requested Type is not supported then ERR::FieldTypeMismatch is customarily returned.

For setting a Unit:

static ERR SET_Dimension(extMyClass *Self, Unit &Unit)
{
   if (Unit.scaled()) Self->TargetField = Unit.Value * Self->RelativeValue;
   else Self->TargetField = Unit.Value;
   return ERR::Okay;
}

Local Objects and Inheritance

Sometimes a class may need to allocate an object within its own scope, meaning that the object is not included in the object tree managed by the client. Such objects are designated as local.

If a local object is accessible through a class field, it must be defined with the FDF_LOCAL option. A working example can be seen in the Picture class, which will always allocate a Bitmap object to contain and represent the rendered image. The Bitmap must be defined as local because the client did not explicitly request its creation.

Local objects bring us to the topic of inheritance. Inheriting the attributes of a local object can be highly beneficial, saving us from having to otherwise duplicate the features already proffered by another class. The Picture class for instance, is essentially a higher order interface for the Bitmap class. It needs to provide all the features of a Bitmap, but with the addition of some extra metadata and the ability to decode and encode an image file like a PNG.

A Parasol class can inherit the features of its local objects by using the CLF::INHERIT_LOCAL flag during the MetaClass creation process. In the Picture class this looks as follows:

static const FieldArray clFields[] = {
   { "Bitmap", FDF_LOCAL|FDF_R, NULL, NULL, ID_BITMAP },
   { "Mask",   FDF_LOCAL|FDF_R, NULL, NULL, ID_BITMAP },
   ...
};

clPicture = objMetaClass::create::global(
   fl::Fields(clFields),
   fl::Flags(CLF::INHERIT_LOCAL),
   ...);

Parasol's inheritance is applied on a run-time basis and uses a top-down approach in terms of priority. For instance, reading the width of a Picture object would normally fail because no width is declared in the Picture class spec. Use of local inheritance means that the Bitmap object will be checked for the width field, and the corresponding value will be returned to client just as if the Picture class had defined width for itself. This all happens seamlessly for the client. If the Picture did declare its own width, that would take precedence and the Bitmap width would be ignored.

Inheritance applies to fields only. Actions and methods are not propagated from local objects.

Actions (Contract Interfaces)

Actions are a type of method that utilise a design-by-contract approach to object orientation. Supporting actions in a class is a great way of building a functional interface, as the developer will already know about the constraints and behaviours defined by the named contracts. The class interface will be seen as consistent and predictable, just like the other classes that support action interfaces.

A complete and detailed guide on action interfaces is available in the Action Reference Manual. In this section we will discuss how actions are supported in practice.

Actions are declared during MetaClass creation and utilise the ActionArray, as in this XML class example:

static const struct ActionArray clXMLActions[] = {
   { AC::Clear,        XML_Clear },
   { AC::DataFeed,     XML_DataFeed },
   { AC::Free,         XML_Free },
   { AC::GetKey,       XML_GetKey },
   { AC::Init,         XML_Init },
   { AC::NewObject,    XML_NewObject },
   { AC::Reset,        XML_Reset },
   { AC::SaveToObject, XML_SaveToObject },
   { AC::SetKey,       XML_SetKey },
   { 0, NULL }
};

clXML = objMetaClass::create::global(
    fl::Actions(clXMLActions),
    ...);

The ActionArray pairs the AC action identifiers with the routines that you will provide. Writing an ActionArray list manually isn't recommended - leverage the IDL compilers to do that for you. The template for action functions is ERR CLASS_ActionName(*Object, *Args) although Args can be dropped if the action does not take parameters. Here are some examples of the prototypes declared by the XML class:

static ERR XML_Clear(extXML *Self);
static ERR XML_DataFeed(extXML *Self, struct acDataFeed *Args);
static ERR XML_Reset(extXML *Self);
static ERR XML_SaveToObject(extXML *Self, struct acSaveToObject *Args);

Notice how action parameters use a consistent template of ac[Name] structs, such as acDataFeed and acSaveToObject. Parameter names and their types are discussed in the Action Reference Manual.

When writing the code for each action, always precede the code with an -ACTION- document tag and include a short description at minimum. This information will appear in the documentation for the class, and also ensures that the action is recognised by IDL compilers so that the ActionArray is generated accurately.

/***
-ACTION-
Clear: Clears all of the data held in an XML object.

An optional long description can be written here.
-END-
***/

static ERR XML_Clear(extXML *Self)
{
   ...
   return ERR::Okay;
}

Methods

Parasol supports methods in the traditional OO sense, with the caveat that the return type is always an ERR code and the output(s) are declared in the parameters. This ensures that there is a consistent design approach that works across programming languages, and also allows for multiple return values to be supported.

Declaring methods is largely similar to that of actions. A key difference is that more documentation is required, and the method will need to declare its parameters. Here's a shortened excerpt from the XML class that declares its methods:

FDEF maSetAttrib[] = { { "Index", FD_LONG }, { "Attrib", FD_LONG }, { "Name", FD_STR }, { "Value", FD_STR }, { 0, 0 } };
FDEF maSerialise[] = { { "Index", FD_LONG }, { "Flags", FD_LONG }, { "Result", FD_STR|FD_ALLOC|FD_RESULT }, { 0, 0 } };
FDEF maInsertXML[] = { { "Index", FD_LONG }, { "Where", FD_LONG }, { "XML", FD_STR }, { "Result", FD_LONG|FD_RESULT }, { 0, 0 } };
...

static const struct MethodEntry clXMLMethods[] = {
   { -1, (APTR)XML_SetAttrib, "SetAttrib", maSetAttrib, sizeof(struct xmlSetAttrib) },
   { -2, (APTR)XML_Serialise, "Serialise", maSerialise, sizeof(struct xmlSerialise) },
   { -3, (APTR)XML_InsertXML, "InsertXML", maInsertXML, sizeof(struct xmlInsertXML) },
   ...
   { 0, 0, 0, 0, 0 }
};

clXML = objMetaClass::create::global(
    fl::Methods(clXMLMethods),
    ...);

As-is the case for action definitions, we don't recommend that you manually write the MethodEntry list or the parameters. The IDL compilers will handle that for you. When writing the code for each method, always precede the code with a -METHOD- document tag and include a short description, long description, -INPUT- and -ERRORS- tags. This information will appear in the documentation for the class, and also ensures that the method is recognised by IDL compilers so that the MethodEntry list is generated accurately. Here's an example for the SetAttrib() method of the XML class:

-METHOD-
SetAttrib: Adds, updates and removes XML attributes.

This method is used to update and add attributes to existing XML tags, as well as adding or modifying content.

...

-INPUT-
int Index: Identifies the tag that is to be updated.
int(XMS) Attrib: Either the index number of the attribute that is to be updated, or set to `NEW`.
cstr Name: String containing the new name for the attribute.  If `NULL`, the name will not be changed.
cstr Value: String containing the new value for the attribute.  If `NULL`, the attribute is removed.

-ERRORS-
Okay
NullArgs
Args
OutOfRange: The `Index` or `Attrib` value is out of range.
Search: The attribute, identified by `Name`, could not be found.
ReadOnly: The XML object is read-only.
-END-

For further information on formatting guidelines, see Embedded Document Formatting.

Method prototypes follow the template ERR CLASS_MethodName(*Object, struct abvMethodName *Args). The abv code is defined in the appropriate methods() call in the module's FDL file. Here's the prototype for SetAttrib():

static ERR XML_SetAttrib(extXML *Self, struct xmlSetAttrib *Args);

Sub-Classes

Parasol supports a unique inheritance concept whereby a class can inherit all aspects of another class and extend it with additional features. From the perspective of the client, the objects will look no different to that of the original class. This feature is known as sub-classing, whereby the dominant class is referred to as the base-class and the additional features are provided by a sub-class.

In this model, the following points are note-worthy:

  • The base-class is dominant, and therefore defines the interface contract for the client. A sub-class cannot change the contract.
  • The purpose of the sub-class should be to augment and improve the capabilities of the base-class.
  • Inheritance is one-to-one. It isn't possible for two sub-classes to augment an object at the same time.
  • Client code that targets the base-class will be compatible with all its sub-classes. From the client's perspective, there is no difference between the two types because the interface contract remains consistent.
  • Sub-classes can declare new fields, methods and actions in their spec if necessary. This can be done without impacting on compatibility with the base-class or the client code that solely targets the base-class.

There are a number of existing classes in the default Parasol build that have active sub-classes, such as:

  • Picture: Supports PNG by default, extended by the JPEG sub-class.
  • Sound: Supports WAV by default, extended by the MP3 sub-class.
  • Vector: Defines a contract interface for supporting common vector graphics features. Extended by VectorEllipse, VectorRectangle, VectorText and many more.
  • FilterEffect: Defines a contract interface for building graphics filter effects. Extended by ImageFX, FloodFX, CompositeFX, LightingFX and many more.

Notice that sub-classing is commonly used for two different design approaches; a) Supporting data files; b) Defining interface contracts that can be extended with additional code.

Sub-classes tend to be very easy to write, and no additional documentation needs to be written if the base-class is not being extended. Initialisation of the JPEG sub-class looks as follows:

static ActionArray clActions[] = {
   { AC::Activate,  JPEG_Activate },
   { AC::Init,      JPEG_Init },
   { AC::Query,     JPEG_Query },
   { AC::SaveImage, JPEG_SaveImage },
   { 0, NULL }
};

...

objModule::create pic = { fl::Name("picture") }; // Load our dependency ahead of class registration

clJPEG = objMetaClass::create::global(
   fl::BaseClassID(ID_PICTURE),
   fl::ClassID(ID_JPEG),
   fl::Name("JPEG"),
   fl::Category(CCF::GRAPHICS),
   fl::FileExtension("*.jpeg|*.jpeg|*.jfif"),
   fl::FileDescription("JPEG Picture"),
   fl::FileHeader("[0:$ffd8ffe0]|[0:$ffd8ffe1]|[0:$ffd8fffe]"),
   fl::Actions(clActions),
   fl::Path(MOD_PATH));

Notice that the JPEG class does not need to define a field spec as it will inherit this from the Picture base-class. It does however define a list of custom actions so that it can hook into the Picture interface and support JPEG encoding.

A question remains, how is it determined that a file source is supported by a sub-class? The onus is not placed on the client to figure out what a JPEG is; this would be too onerous and at a loss of run-time dynamism. The answer lies in the initialisation process, which in the event that the base-class fails with ERR::NoSupport will lead to a query of the registered sub-classes to determine if support is available. Here's a look at the JPEG initialisation process, which supports the creation of new JPEG files and the loading of existing files:

static ERR JPEG_Init(extPicture *Self)
{
   ...

   Self->get(FID_Location, &path);

   if ((!path) or ((Self->Flags & PCF::NEW) != PCF::NIL)) {
      // A JPEG image is being created from scratch (e.g. to save an image to disk).
      ...
      return ERR::Okay;
   }
   else if (Self->getPtr(FID_Header, &buffer) IS ERR::Okay) {
      if ((buffer[0] IS 0xff) and (buffer[1] IS 0xd8) and (buffer[2] IS 0xff) and
          ((buffer[3] IS 0xe0) or (buffer[3] IS 0xe1) or (buffer[3] IS 0xfe))) {
         log.msg("The file is a JPEG picture.");
         if ((Self->Flags & PCF::LAZY) IS PCF::NIL) acActivate(Self);
         return ERR::Okay;
      }
      else log.msg("The file is not a JPEG picture.");
   }

   return ERR::NoSupport;
}

Notice how the file header is checked for the byte sequence that would identify the data as JPEG. If this is not matched, ERR::NoSupport is returned so that the initialisation sequence can query the next available sub-class. Also of note is the Header field; this feature is particular to the Picture class design and is intended to save each sub-class from reading the file header independently.

Spec Design

Before we leave this chapter on classes, it is worth considering Parasol's contractural approach to OO and how that should impact on your class designs. It is extremely common for classes to use concepts that are generic by nature, for instance a UI widget will typically express an X, Y, Width and Height. A class that loads data may specify a Path. Bit-options may be expressed as Flags. As you spend more time using Parasol classes, you will discover that not only does the naming schedule for fields tend to be consistent and predictable, the code rarely differs in these generic areas too.

We do not enforce generic constraints at a field level as this is seen as impractical and can cause issues when deviations are warranted. We do however strongly encourage observing the terminology and methodologies that are already in place across Parasol's interfaces. Leveraging existing concepts if you do any work at an API level can save time, with less documentation needing to be written, and also for the client in being able to predict behaviour patterns. If in doubt, check the source code of something that shares some commonality with what you are trying to achieve.