libagents-1.0 Online Reference Manual


Doc_id: 1.0.3_html

This document is the reference manual of the 'libagents-1.0.3' library. The latest version of this document is available in HybridPDF format as part of the libagents library package, at http://libagents.sourceforge.net.

About this document

This document is organized such that it gradually introduces the terminology used in each chapter, all while trying to minimize any forward references to yet-undefined terms and concepts; it is thus strongly recommended to read the information presented in this document progressively, in the order it is presented, from the beginning to the end of this document.

Document versions

The libagents documentation uses a three-number document ID 'x.y.r', which is identical to the version number of the library it documents.

Target audience

The reader of this document is assumed familiar with the C++ programming language and the STL. The libagents library is implemented in ANSI C++11; however, the libagents API is compatible with C++98, such that understanding and using the libagents library features does not require knowledge of the C++11-specific (or later) language features.

Additionally, the reader of his document is assumed to have (at least) basic knowledge about object-oriented, event-driven programming, and about event-driven frameworks and APIs.

Editing this document

If you intend to make changes to this document in view of republishing, please make sure you use the latest version of this document, listed in the "Download" chapter, as the basis for your edit; additionally, please save a copy of your modified version in Open Document Text format (.odt) and send it to itgroup@gmail.com, together with the list of changes that you have made.

License

This document is copyrighted material, all rights reserved.

(c) Information Technology Group - www.itgroup.ro
(c) Virgil Mager

Permission is granted to copy, distribute and/or modify this document
under the terms of the
GNU Free Documentation License, Version 1.3
with no Invariant Sections, no Front-Cover Texts, no Back-Cover Texts

@toc@Table of contents@

About the libagents library

The libagents library provides a C++ implementation of the "Actor Model" paradigm, thus enabling the development of pure C++ multi-threaded applications structured as a collection of asynchronous event-driven execution agents which all run concurrently and communicate with each other via an asynchronous messaging system. The libagents library version 1.0.x implements a reliable message delivery protocol between agents, which provides a failed-delivery notification mechanism for messages that cannot be delivered to their target recipients.

The asynchronous, event-driven, agents-based data processing paradigm as implemented by the libagents library requires approaching the program implementation problem from an agent-oriented perspective. At one end of the spectrum an application can consist of any number of agents that exchange messages with each other and execute specific subroutines exclusively as the result of receiving an incoming message, while at the other end of the spectrum an application can consist of a single agent which executes a standard sequential program flow from start to finish, without implementing any event-driven functionality.

The concept of "agent" is fundamental in a libagents application, and it is implemented as a base class "Agent" in the libagents library. Each libagents user application has to create its own types of agents (tailored to the functionality required by the application) by deriving custom agent classes from the libagents' "Agent" base class.

License

The 'libagents' library is copyrighted software, all rights reserved.

(c) Information Technology Group - www.itgroup.ro
(c) Virgil Mager

The 'libagents' library is free software, and it is distributed "AS IS", with
NO WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, to the maximum extent
permitted by applicable law.


The 'libagents' library license is available in the
LICENSE.TXT file found
in the library distribution package; additionally, the 'libagents' library license
is
also embedded at the beginning of each of the library's source code files.


How to use the libagents library

The libagents-1.0.x library is provided exclusively in source code format, with the library source files organized inside a single top-level folder 'libagents-1.0.x'. The libagents-1.0.x library is distributed as a compressed archive, and it must be be decompressed on the host file system in a location whose path contains exclusively alpha-numeric characters, '_' (underline), '-' (minus), and '.' (dot).

Because the libagents-1.0.x library is provided exclusively in source code format, building a libagents-based application requires making both the application-specific source files, and the libagents library source files, part of the application project, and then all these files have to be compiled and linked together when building the libagents-based application.

The libagents configuration file

The libagents library includes a"library configuration file" 'libagents-config.h' which contains various object declarations, inline functions, constants (declared as enums), and #defines, which are used internally in the libagents library source code, and/or which can be used in a libagents-based user application; additionally, the libagents library has several configuration options which are defined in the library configuration file 'libagents-config.h', and which can be changed according to the requirements of each specific application and/or host operating system:

Creating a libagents application project

As it has been previously described at the beginning of this chapter "How to use the libagents library", in order to build a libagents application both the application-specific source files and the libagents library source files must be present on the host system where the application is to be built; then, the libagents application project must be set up to contain the following files:

Additionally, the following compiler settings must be used when building a libagents-based project:

Example applications

A 'stopwatch' example application is available in the libagents library package. This is a simple GUI application which uses the libagents library for the core logic, and the Qt 5.4.2 framework for its GUI interface. The application can be built by first installing the Qt 5.4.2 framework on the user's host system, then the application project file 'libagents-examples\libagents-example-stopwatch\libagents-example-stopwatch.pro' must be opened in Qt Creator IDE, and finally the project must be built by invoking Qt Creator's "Build project" command from Qt Creator's main menu.

The architecture of a libagents application

The top-level architecture of a libagents-based application consists of two autonomous modules which communicate with each other, and which together "sit on top of" the application's underlying "host framework"; this is illustrated in Fig.1 below:


Fig.1: top-level architecture of a libagents-based application

The following paragraphs in this chapter detail the internal structure and the functionality of the libagents Core and Shell modules.

The Core module

As it has been previously explained in "The architecture of a libagents application" paragraph above, the Core module is responsible for implementing the application's core logic. In terms of internal architecture, the Core module of a libagents application consists of a single top-level "Core" object, which, in turn, contains a hierarchy of "Task", "Thread", and "Agent" objects; this is illustrated in Fig.2 below:


Fig.2: the mandatory top-level architecture of a libagents application Core module

The role and implementation of the constituent components of a libagents application's Core module as illustrated in Fig.2 above are detailed in the following paragraphs.

The Agent objects

A libagents "Agent" object is the elementary data processing units of a libagents application, and it consists of [an instance of] a user-defined class derived from the libagents "Agent" base class.

The entire functionality of an agent object is implemented as a single "onMessageReceived()" method which is the exclusive data processing function of an Agent object, and whose execution is automatically triggered [by libagents's internal mechanisms] each time a new message is received by an agent. The "onMessageReceived()" method is a pure virtual method declared inside libagents's "Agent" base class, and it must be implemented by each user-defined agent class.

In other words, a [user-defined] agent object is a data processing unit that sits idle awaiting for an incoming message, and for each message it receives it executes its "onMessageReceived()" method which contains specific code branches/algorithms associated with the received message and possibly with the value of a [scalar or aggregate] state variable at the time when the message was received (in this latter case the agent implements a state machine); this is illustrated in Fig.3 below:


Fig.3: every data processing sequence performed by an agent is triggered by an incoming message
and it is performed exclusively by the agents "onMessageReceived()" method

Apart from the "onMessageReceived()" method, the libagents "Agent" base class also provides methods for sending a targeted message to another agent which is part of the same Task, for broadcasting a message to all, or part of, the other agents in the same Task, and for sending messages to the application's Shell module (the details on the libagents messaging system are presented in "The messaging system" paragraph later in this document).

The Thread objects

A libagents "Thread" object consists of [an instance of] a user-defined class derived from libagents's "Thread" base class, and it provides methods for creating and destroying agents "inside" a Thread object (these methods are detailed in "The libagents API" chapter later in this document).

A libagents "Thread" object is the elementary execution-scheduling unit of a libagents application, and it logically groups together agents whose message processing algorithms (namely, their "onMessageReceived()" methods) are executed by the same OS thread within a [potentially multi-threaded] application; we will here-forth refer to the OS thread that executes the "onMessageReceived()" method of the Agent objects that are part of a given Thread object as "the underlying OS thread" associated with the Thread object.

The following scheduling rules apply:

  1. for any two agents that belong to two different Thread objects, their "onMessageReceived()" methods are always executed concurrently and independently by the two OS threads that correspond to the two Thread objects that contain the two agents

  2. for any two agents that belong to the same Thread object, their "onMessageReceived()" methods are executed atomically, in succession, by the underlying OS thread of their Thread object container. In other words, once an agent in a given Thread object receives a message and starts executing its "onMessageReceived()" method, all the other agents that belong to the same Thread object will not be receiving, nor will they be processing, any messages until the currently-running "onMessageReceived()" method completes execution

The scheduling mechanism described above is illustrated in Fig.4 below:


Fig.4: each Thread object is associated with a distinct OS thread,
and the "onMessageReceived()" methods of agents belonging to the same Thread object
are executed atomically, in a round-robin scheme, by said Thread object's underlying OS thread

The Task objects

A libagents "Task" object logically groups together one or more "Thread" objects, and it consists of [an instance of] a user-defined class derived from libagent's "Task" base class.

The functional role of a "Task" object is to allow organizing the agents in a libagents application based on how they can exchange messages with each other: namely, all agents that are part of the same Task can exchange direct messages with one-another, while agents that are part of different Tasks can only exchange messages via a special relaying procedure (more details on said relaying procedure are presented in the "Inter-task communication" paragraph later in this document).

The messaging restrictions presented above are illustrated in Fig.5 below, where the red lines represent direct messages that can be exchanged between agents that are part of the same Task object, and the blue lines represent inter-Task messages that can be exchanged between agents only by using the above-mentioned special relaying procedure:


Fig.5: direct messages can only be exchanged between agents in the same Task object,
while inter-Task messaging must use a special relaying procedure

The libagents "Task" base class provides methods for creating and destroying "Thread" objects (these methods are detailed in "The libagents API" chapter later in this document), as well as methods that allow an agent to be subscribed/unsubscribed to/from messages that are broadcasted by another agent (broadcasted messages are presented in the "Broadcasted messages" paragraph later in this document).

The Core object

The "Core" object of a libagents application logically groups together all the application's "Task" objects (and, indirectly, all the application's "Thread" and "Agent" objects), and it consists of a singleton instance of a user-defined class derived from the libagents "Core" base class.

The libagents "Core" base class provides methods for creating and destroying "Task" objects (these methods are detailed in "The libagents API" chapter later in this document), for facilitating the communication among the individual agents in an application, and for exchanging intercom messages between the Core module and the application Shell module (more details on intercom messages are provided in the "Intercom messages" paragraph later in this document).

The Utility objects

The "Utility objects" are optional application-specific, user-defined objects which may, or may not, be implemented by an application (i.e. the libagents library does not provide a base class for the Utility objects), and whose role is to provide various functionalities that are commonly used by the application but which are not related to the integration of the application with its operating environment (e.g. encryption functions, advanced mathematical functions, image processing functions, etc). The Utility objects should be instantiated by (i.e. "contained in") the top-level Core object, thus making them accessible both to the Core object and to all the other objects that are contained in the Core object (i.e. the application's Agent, Thread, and Task objects).

Object local data

As is was previously explained throughout this document, the Agent, Thread, Task, and Core objects of a libagents application's Core module are created by deriving a custom class from a libagents base class, and then creating instances of said derived classes as required by the application (e.g. an Agent object myAgent of type MyAgent is created by first declaring the MyAgent class via derivation from the libagents "Agent" base class, and then instantiating myAgent = new MyAgent, etc); however, none of the libagents Agent, Task, Thread, and Core base classes provide any inbuilt user-accessible data storage elements, such that any user-defined type of object derived from said libagents base classes must implement its own local data structures if needed, tailored for keeping the state information and other associated data as required by said user-defined type of object (the objects' local data, implemented as object properties, can then contain scalar data, embedded or dynamically allocated data structures, arrays, etc).

An example of a libagents application Core module which implements local data in conjunction with each of its Agent, Task, Thread, and Core component objects is illustrated in Fig.6 below:


Fig.6: example of a libagents application
where each component includes a local data store

Initializing the Core module

As it has been previously explained in "The Core module" paragraph earlier in this chapter, the Core module of a libagents application consists of a single top-level "Core" object, which, in turn, contains a hierarchy of "Task", "Thread", and "Agent" objects; in this context, the process of initializing the Core module consists exclusively of creating, and then "starting", the top-level Core object, and it will be the Core object's responsibility to further create, and then "start", its sub-component objects.

Fig.7 below illustrates the succession of events that follow the invocation of the Core object's constructor (step A in Fig.7 below) and its "Start()" method (step B in Fig.7 below):


Fig.7: initialization of a libagents application's Core object

The following notations are used in Fig.7 above:

The following paragraphs detail each of the methods illustrated in Fig.7 above.

The object constructors

As it can be seen in Fig.7 above, the functional role of the Core, Task, Thread, and Agent object constructors is to initialize their corresponding local data structures (if any), and create the sub-component objects (if any) of each particular container object.

The "onStarted()" methods

The "onStarted()" methods illustrated in Fig.7 above are all pure virtual methods provided by the libagents "Core", "Task", "Thread", and "Agent" base classes, and they must be implemented by each Core, Task, Thread, and Agent object used in a libagents application. The mandatory functional role of each object's "onStarted()" method is to start all the sub-component objects (if any) of each container object, e.g. the "onStarted()" method of the Core object must start the application's Task objects, the "onStarted()" method of each Task object must start said Task object's sub-component Thread objects, etc (see Fig.7 above); specifically:

Initialization thread

For most libagents-based applications, the initialization of the application's Core object (and its sub-component objects) can, and should, consist exactly of the orderly execution of the two steps (A) and (B) illustrated in Fig.7, and it should be performed by the application's startup function (i.e. the function illustrated as "main()" in Fig.1) as part of the application's startup procedure; in this case, both the constructors, and the "onStarted()" methods, of all the Core module objects will be executed by the OS thread that runs the application's startup function (because both the constructor, and the "Start()" method, of the Core object are invoked by the application's startup function).

Dynamically changing the Core module at runtime

The internal structure of libagents application's Core module can be changed dynamically at runtime by "killing" any of the Core object's sub-component objects, and/or by creating new sub-component objects of the Core object:

A detailed description of the KillXXX() and StartXXX() methods is provided in "The Core module API" paragraph later in this document.

The messaging system

The application Core module's messaging system is the foundation of the libagents event-driven data processing model, and it provides the means by which an Agent can exchange messages with other Agents in the application, as well as with the application's Shell module.

The libagents library version 1.0.x implements three types of messages, namely "targeted messages", "broadcasted messages", and "intercom messages". The following paragraphs present the essential characteristics of the application Core module's messaging system, while the exact message formats and message-exchange methods are presented in "The libagents API" chapter later in this document.

Targeted messages

The "targeted messages" are messages sent by an agent that is part of a given Task object directly to another agent that is part of the same Task object. Specifically, the libagents "Agent" base class (and thus any user-defined agent derived from the "Agent" base class) implements a "SendMessage()" method which allows a source Agent object to send a targeted message to a specific destination Agent object, where said destination agent must be part of the same Task object as the source agent.

In other words, the "targeted messages" are one-to-one direct messages exchanged between two agents that are part of the same Task object.

Broadcasted messages

The "broadcasted messages" are the foundation mechanism for the subscription-based communication model in a libagents application; specifically:

The libagents "Agent" and "Task" base classes provide a set of methods that control the flow of broadcasted messages from their source agent to their destination agents; specifically:

Fig.8 below exemplifies four agents which have been subscribed to a source agent, with all said agents (source and subscribed) belonging to multiple Threads in the same Task: thus, all the messages that are broadcasted by the source agent towards all the agents in its own Task are automatically received by, and only by, the four subscribed agents:


Fig.8: example of four broadcast subscriptions: each can be established/removed
via the Task class' methods "AddBroadcastSubscription()"/"RemoveBroadcastSubscription()"

Intercom messages

The "intercom messages" are messages that are exchanged between the Core and Shell modules of a libagents application via a dedicated "Intercom" communication port contained in the Core object. The intercom messages routing scheme is illustrated in Fig.9 below:


Fig.9: routing of the libagents intercom messages inside the Core object

Following is a detailed description of each of the intercom messaging-related objects and methods illustrated in Fig.9 above:

Inter-task communication

As it has been previously described in "The Task objects" paragraph earlier in this chapter, the functional role of a Task object is to group together a set of agents that can directly exchange messages with each other by sending/receiving targeted messages and/or broadcasted messages, but which cannot directly exchange messages with agents that are part of a different Task object. However, agents belonging to different Tasks can communicate with each other indirectly via the Core object's Intercom, with the caveat that said messages must be formatted in such a way as to allow the "onIntercomMessageReceived()" method to detect them as "inter-task" messages and properly forward them to their intended destination.

The inter-Task communication procedure described above is illustrated in Fig.10 below:


Fig.10: inter-Task communications via the Core object's Intercom

Scheduled messages

The message sending methods of the Agent base class (and thus of any type of user-defined agent derived from the "Agent" base class) implement a message scheduling function: specifically, when an agent object invokes one of its "SendMessage()" or "BroadcastMessage()" methods, said methods can be instructed to delay sending the message for a specified amount of time, and/or to repeatedly send the message either a specified number of times or until the auto-repeated send operation is explicitly canceled. This message scheduling facility enables the implementation of various kinds of time-dependent functionalities, e.g. messaging protocols that depend on retransmissions until the sent message is acknowledged, or condition monitors, or counter/timer objects, etc (the various message scheduling options are presented in detail in "The libagents API" chapter later in this document).

Message delivery

All the object methods which are sending a message (e.g. the Core object's SendIntercomMessage(), or the Agent objects' SendMessage() method, etc) return a value of '0' or 'false' if the message could not be placed in the destination message buffer, which can only happen if the destination buffer is full (the specific return values of each of these methods are detailed in "The libagents API" chapter later in this document); thus, if a certain piece of code in an application sends a message assuming that the message's destination buffer has enough room to store the sent message, it is highly recommended to cover said message-sending method in an 'assert()' statement (e.g. 'assert(myAgent.SendMessage(...))', etc) in order to force the application to shut down with an 'Assertion failed' error message if the destination buffer does not have enough room to receive the sent message.

Multi-threading in the Core module

Because of the multi-threaded nature of the Core module, any and all methods that may be invoked concurrently from different OS threads must have a thread-safe implementation (e.g. they can be re-entrant, or they can be protected with multi-threading semaphores, etc), and any and all data elements that may be accessed concurrently from different OS threads must be explicitly protected against data races (e.g. they can be implemented as objects that are featured with a thread-safe read-modify-write method).

The Shell module

As it has been explained in "The architecture of a libagents application" chapter earlier in this document, the Shell module of a libagents application serves the role of interfacing the application core logic with the application's operating environment, and the libagents library does not impose any restrictions on the Shell module architecture other than it must contain the application's startup function and that it must provide a proper interface for communicating with the application's Core module (see Fig.1).

The libagents API

This chapter describes the [user-accessible] data types and base classes of the libagents library, and explains how they should be used when creating a libagents application. Each class is presented with a synopsis of its declaration, and then each of its methods is described in detail.

The libagents libary namespace

The libagents library's API is declared inside the 'AGENTS_Lib' namespace, and a convenience alias 'libagents' is #defined in the libagents configuration file 'libagents-config.h' (see "The libagents configuration file" paragraph earlier in this document); thus, in order to use any of the libagents API's objects/functions/constants in a libagents-based application, each such object/function/constant must be prefixed with 'AGENTS_Lib::' or 'libagents::', or the libagents namespace must be made visible in the application source file(s):

The libagents header file

The complete libagents API is "published" in the 'libagents.h' header file, which must be #included in the header file of any file unit {.h+.cpp} which uses the libagents API.

The libagents data types

The libagents data types are classes which are used as arguments and return values to/from various methods of the libagents base classes, and they are meant to be used as-is in a libagents application (i.e. these data types generally need not, and should not, be derived from in a user application).

The alphanum_t data type

The "alphanum_t" data type is a data cell that can store either a string value, or a numeric double-precision value, or a void pointer value, by providing constructors from std::string, from double, and from void*. An alphanum_t data cell can be compared for equality/inequality with another alphanum_t data cell via the '==' and '!= ' operators, and it has three methods 'getNumber()', 'getString()', and 'getPointer()' which return the double, the string, and respectively the void* value contained in the cell, or they throw a runtime exception if trying to extract an undefined value from a data cell (e.g. using the getNumber() method for a cell that has been initialized with a string value will throw a runtime exception).

Synopsis:

class alphanum_t {
public:
inline alphanum_t() = default; // creates an undefined-type data cell
inline alphanum_t(const char* s); // creates a string-type data cell
inline alphanum_t(const std::string& s); // creates a string-type data cell
inline alphanum_t(int i); // creates a numeric-type data cell
inline alphanum_t(unsigned u); // creates a numeric-type data cell
inline alphanum_t(double d); // creates a numeric-type data cell
inline alphanum_t(void* p); // creates a void pointer-type data cell
inline bool isNumber() const; // 'true' iff cell contains numeric value
inline bool isString() const; // 'true' iff cell contains string value
inline bool isPointer() const; // 'true' iff cell contains pointer value
inline double getNumber() const; // return the cell's numeric value
inline const std::string& getString() const;// return the cell's string value
inline void* getPointer() const; // return the cell's void pointer value
inline bool operator==(const alphanum_t& x) const;
inline bool operator!=(const alphanum_t& x) const;
};

Examples:

alphanum_t d1=12.34; // d1 is assigned with alphanum_t(12.34)
alphanum_t d2=d1; // d2 is assigned with d1 (with d1's value and d1's type)
alphanum_t d3=0; // d3 is assigned with alphanum_t(0)
alphanum_t s1="abc"; // s1 is assigned with alphanum_t("abc")
alphanum_t p=(void*) &d1; // p is assigned with alphanum_t(&d1)
bool b1=d1.isNumber();// b1 is true: d1 contains a numeric value
bool b2=d1.isString();// b2 is false: d1 does not contain a string value
bool b3=d1.isPointer();// b3 is false: d1 does not contain a pointer value
double d=d1; // compile-time error: no implicit conversion d1 to numeric value
std::string s=s1; // compile-time error: no implicit conversion s1 to string value
char *c=(char*)p; // compile-time error: no implicit conversion p to pointer value
alphanum_t d4=*(alphanum_t*)p.getPointer(); // valid: d4 gets assigned with d1
d=d1.getNumber(); // valid: double d gets assigned with 12.34
s=s1.getString(); // valid: string s gets assigned with "abc"
s=d1.getString()+"x";// run-time exception: d1 does not hold a string
d1==s1; // false: d1 holds a double while s1 holds a string
d1!=s1; // true: d1 holds a double while s1 holds a string
d1==d2; // true: both the types and the values match
d1==d3; // false: the types match but the values don't
d1=="xyz"; // false: d1 holds a double while alphanum_t("xyz") holds a string
d1==12.34; // true: d1 holds a double and it is equal to alphanum_t(12.34)
d1>=12.34; // compile-time error: alphanum_t::operator>=(alphanum_t) undefined
d1.getNumber()>=12; // true: d1 holds the numeric value 12.34

The id_t data type

The "id_t" data type is an alias of the "alphanum_t" data type, and it is used (mostly) to represent the IDs (read: names) of Agent objects, Thread objects, and Task objects in a libagents application.

Because the Agent, Thread, and Task object names are of type "id_t", they can have have a string value or an enum value (using enum values can significantly enhance the maintainability of the application). Assigning an object name with an id_t data which holds a pointer value is not allowed.

The message_t data type

The "message_t" data type is the data type of all the messages used by the libagents messaging system (i.e. targeted messages, broadcasted messages, and intercom messages, see the "The messaging system" paragraph earlier in this document), and it consists of an array of one or more "alphanum_t" cells, plus a std::map<int, alphanum_t> container which may hold various metadata that might occasionally need to be transferred between objects.

We will here-forth refer to the first "alphanum_t" cell in a "message_t" message as the "message name", and to the remaining "alphanum_t" cells as the "message payload". A message must have at least one cell, i.e. it may have no payload cells, but it must always contain the message name cell.

The "message_t" data type provides several convenient constructors, methods for accessing a cell in the message, methods for appending, inserting, and deleting a cell, for finding the message length (in number of cells), and for serializing and deserializing the message to/from a std::string.

Synopsis:

class message_t {
public:
std::map<int,std::string> meta; // metadata with application-defined semantics
message_t() = default; // new empty message
message_t(const std::string& s);// new one-cell message, cell assigned with a string
message_t(const char* c); // new one-cell message, cell assigned with a string
message_t(int i); // new one-cell message, cell assigned with a number
message_t(unsigned u); // new one-cell message, cell assigned with a number
message_t(double d); // new one-cell message, cell assigned with a number
message_t(void *p); // new one-cell message, cell assigned with a pointer
message_t(const alphanum_t& a); // new one-cell message,cell assigned with alphanum_t
void clear(); // clear message (message will contain zero cells)
const alphanum_t& operator[](unsigned i) const; // const access to a cell
alphanum_t& operator[](unsigned i); // r/w access to a cell
message_t& operator<<(const alphanum_t& p); // append a cell
message_t& insert(unsigned pos, const alphanum_t& p); // insert a cell
bool erase(unsigned pos); // delete a cell if cell exists
unsigned size() const; // message size in # of cells
std::string prettyPrint(std::string separator="", bool decoration=0) const;
bool to_string(std::string& s) const; // serialize message into a std::string
bool from_string(const std::string& s); // restore message from serialized format
};

Examples:

enum {MSGNAME_i1=1, MSGNAME_i2, MSGNAME_i3};        // message names with integer values
std::string MSGNAME_str="message name is a string"; // message name with string value
message_t ms(MSGNAME_str); // ms is {"message name is a string"}
message_t m1=MSGNAME_i1; // m1=message_t(MSGNAME_i1): m1 is {1}
alphanum_t a2=MSGNAME_i2; // a2=alphanum_t(MSGNAME_i2): a2 is assigned integer value 2
message_t ma=a2; // ma=message_t(a2): ma becomes {2}
alphanum_t a3="string cell", a4(4); // a3=alphanum_t("string cell"), alphanum_t a4(4)
message_t m2; // create empty m2
m2<<MSGNAME_i3; // insert m2's name (i.e. cell #0): m2 becomes {3}
m2<<a3<<a4; // append two cells to m2: m2 becomes {3, "string cell", 4}
m2.insert(1,3.3);// insert cell at position 1: m2 beomes {3, 3.3, "string cell", 4}
m2.erase(3); // erase cell #3, return true: m2 becomes {3, 3.3, "string cell"}
int s=m2.size(); // m2.size() is 3
m2.erase(3); // cannot erase cell #3, return false: message only contains cells 0..2
std::string pp=m2.prettyPrint(",",1) // pp becomes (w/o quotes): "#3,#3.2,$string cell"

The Core module API

As it has been described in "The Core module" paragraph earlier in this document, the Core module of a libagents application consists of a singleton Core object whose type is derived from the libagents "Core" base class, and said Core object in turn contains a collection of application-specific objects whose types must be derived from the libagents "Agent", "Thread", and "Task" base classes. Based on the inclusion criteria among the libagents types of objects (i.e. Core object:{Task objects:{Thread objects:{Agent objects}}}), this paragraph is organized as a bottom-up presentation of the above-mentioned base types' APIs, starting with the Agent base class and ending with the Core base class.

The Agent class

The Agent class is the base class from which all user-defined [types of] Agent objects must be derived. The Agent objects are the elementary data processing units in a libagents application (see "The Agent objects" paragraph earlier in this document), and they are logically grouped together into Thread objects (see "The architecture of a libagents application" paragraph earlier in this document). Agent objects can send and receive messages to/from other agents, and to/from the application's Intercom object (see "The messaging system" paragraph earlier in this document).

Synopsis:

class Agent {
protected:
virtual void onStarted()=0;
virtual bool onMessageReceived(const message_t& msg,
const id_t& sourceThreadId,
const id_t& sourceAgentId)=0; // MUST return 'true'!!!
uint64_t SendMessage(const message_t& msg,
const id_t& destThreadId,
const id_t& destAgentId,
float schedule=0,
int repeat=1,
int expire=INT_MAX);
uint64_t BroadcastMessage(const message_t& msg,
const id_t& destThreadId="*",
float schedule=0,
int repeat=1,
int expire=INT_MAX);
bool CancelMessage(uint64_t msgId);
public:
Agent() = delete;
Agent(const id_t& id);
virtual ~Agent();
id_t agentId();
Thread* parentThread();
Core* core();
};

Details:

    • for example, schedule=1000.00 means the message will be sent with a delay of exactly one second from the moment when the method is invoked, while schedule=1000.50 specifies that the send delay will be a random value between 500ms and 1500ms

The Thread class

The Thread class is the base class from which all user-defined [types of] Thread objects must be derived. The Thread objects are the elementary execution-scheduling units of a libagents application, and they group together agents whose "onMessageReceived()" methods are executed by the same OS thread (see "The Thread objects" paragraph earlier in this document).

Synopsis:

class Thread {
protected:
virtual void onStarted()=0;
public:
Thread() = delete;
Thread(const id_t& id);
virtual ~Thread();
bool StartAgent(Agent* a);
bool KillAgent(const id_t& agentId);
Agent* childAgent(const id_t& agentId);
id_t threadId();
Task* parentTask();
Core* core();
};

Details:

The Task class

The Task class is the base class from which all user-defined [types of] Task objects must be derived. The Task objects logically group together Thread objects whose contained agents can communicate with one another by exchanging direct messages (see "The Task objects" paragraph earlier in this document).

Synopsis:

class Task {
protected:
virtual void onStarted()=0;
public:
Task() = delete;
Task(const id_t& id);
virtual ~Task();
bool StartThread(Thread* t);
bool KillThread(const id_t& threadId);
Thread* childThread(const id_t& threadId);
id_t taskId();
Core* core();
bool AddBroadcastSubscription(const alphanum_t& messageName,
const id_t& sourceThreadId,
const id_t& sourceAgentId,
const id_t& destThreadId,
const id_t& destAgentId);
bool RemoveBroadcastSubscription(const alphanum_t& messageName,
const id_t& sourceThreadId,
const id_t& sourceAgentId,
const id_t& destThreadId,
const id_t& destAgentId);
};

Details:

The Core class

The Core class is the base class from which a libagents application's Core [type of] object has to be derived. The Core object of a libagents application logically groups together all the application's component objects, i.e. the application's Task, Thread and Agent objects (see "The Core object" paragraph earlier in this document), and it contains methods for managing the intercom messages that are exchanged between the Core object and the Shell module (see the "Intercom messages" paragraph earlier in this document).

Synopsis:

class Core {
protected:
virtual bool onIntercomMessageReceived(message_t& msg)=0; // MUST return 'true'!!!
bool SendMessage(const message_t& msg,
const id_t& destTaskId,
const id_t& destThreadId,
const id_t& destAgentId);
virtual int onStarted(const message_t& args)=0;
public:
Core(unsigned int messageBufferSize=DEFAULT_MESSAGE_BUFFER_SIZE);
virtual ~Core();
class Intercom {
public:
bool PutMessage(const message_t &msg);
bool GetMessage(message_t &msg);
} intercom;
int Start(message_t args="");
bool SendIntercomMessage(const message_t& msg);
bool StartTask(Task* t);
bool KillTask(const id_t& taskId); // NYI in libagents v1.0.x !!!
Task* childTask(const id_t& taskId);
uint64_t ticker();
static int processors();
};

Details:

Debugging support

The libagents library configuration file 'libagents-config.h' (see "The libagents configuration file" paragraph earlier in this document) #defines a 'force()' macro as an inline funtion 'force_()', and the 'force()' macro is used in various places inside the libagents library to check for the validity of a condition (in a similar way to the standard 'assert()' macro). Most of the current IDEs will allow setting up a breakpoint at the 'exit(1)' statement in the definition of the 'force_()' function in the 'libagents-config.h' file, and the developer can then trace back in the debugger the call path that led to the failed 'force()' test.

Additionally, the 'force()' assertion can also be used in the user applications, thus providing the means to stop the application, when running in debug mode, before the application exits, and the stack call trace can then be examined to determine the position of the failed 'force()' assertion in the program's source code.

Design considerations

This chapter contains several key design considerations and suggestions regarding the architectural details of a libagents-based application.

Design partitioning

The process of designing a libagents application must start with identifying which, if any, of the various processing functions that the application must perform can be separated into different Tasks, based on the functional characteristic of a Task object of grouping together agents that can easily exchange direct messages with each other (by sending/receiving targeted and/or broadcasted messages), but can only exchange inter-Task messages via the special inter-Task communication procedure previously described in the "Inter-task communication" paragraph.

After the Task-level structure of the application has been defined, the second step is to distribute each Task object's functionality among multiple Agent objects by first identifying which actions can be performed in parallel (i.e. by separate agents), and then grouping the resulting agents into Thread objects by taking into consideration the ways in which the agents can best interact with each other (e.g. by thread-safely sharing Thread-local data structures, see the "Multi-threading in the Core module" paragraph earlier in this document, etc) and/or how they may interfere with each others' execution depending on whether they are part of the same, or different, Thread objects (see "The Thread objects" paragraph earlier in this document).

Reference Shell module architecture

As it has been explained in "The architecture of a libagents application" chapter earlier in this document, the Shell module of a libagents application serves the role of interfacing the application core logic with the application's operating environment. In this context, this chapter describes a reference Shell module architecture which allows a simple and well formalized procedure for implementing the Shell module "on top of" most modern host frameworks which are actively maintained at the time of writing this document (e.g. win32/64, Qt, GTK, wxWidgets, etc).

Fig.12 below illustrates the top-level view of the reference Shell module architecture that will be discussed throughout this chapter:


Fig.12: top-level view of the reference Shell module architecture

Fig.14 below illustrates a detailed view of the reference Shell module architecture illustrated in Fig.12 above, together with the interconnections between the application's Core and Shell modules:


Fig.14: detailed view of the reference Shell module architecture,
together with its interconnections with the application's Core module

The implementation details of the Shell module components illustrated in Fig.14 above are detailed in the following paragraphs.

The Shell objects

As it was previously described in the "Reference Shell module architecture" paragraph above, the role of the Shell objects is to provide a libagents application with all the system integration functions that the application requires during its operation (e.g. network communications, file system access, GUI, etc). In this context, Fig.15 below illustrates the generic architecture of the Shell objects that are part of the reference Shell module architecture illustrated in Fig.14:


Fig.15: generic architecture of the reference Shell module's Shell objects

The Shell object's generic architecture as illustrated in Fig.15 above consists of:

The Shell objects' architectural details that depend on the characteristics of the application's host framework are discussed in the "Host framework integration" paragraph later in this chapter.

The Shell Controller object

The Shell Controller object is a mandatory Shell component which must be implemented as part of the reference Shell module architecture illustrated in Fig.14, and its role is to act as the "central coordinator" for all the activities that occur inside the Shell module. In this context, Fig.16 below illustrates the generic architecture of a Shell Controller object that is part of the reference Shell module architecture:


Fig.16: generic architecture of the reference Shell module's Shell Controller object

The Shell Controller's generic architecture as illustrated in Fig.16 above consist of:

The Shell Controller object's architectural details that depend on the characteristics of the application's host framework are discussed in the "Host framework integration" paragraph later in this chapter.

The application startup function

As it was previously described in the "Reference Shell module architecture" paragraph earlier in this chapter, the startup function of a libagents application (illustrated as 'main()' in Fig.14) is the application's initialization function whose execution is automatically launched at application startup, and it is executed by a dedicated OS thread which is allocated to the application by the operating system when the application is started; we will here-forth refer to the OS thread that executes the application's startup function as the application's "main thread".

In terms of functionality, the startup function of a libagents application that uses the reference Shell module architecture must create the startup configuration of all the application's top-level objects, i.e. the Shell module's Shell objects (e.g. windows, network sockets, etc) and Shell Controller object, and the Core module's top-level Core object.

As it is the case with any application that is built "on top of" a host framework, the code template (including the signature) of the startup function of a libagents application is defined by the host framework "upon which" the application is built, and it differs widely from one host framework to another; however, regardless of the host framework-specific details, the startup function of a libagents application that uses the reference Shell module architecture will always have to perform the generic sequence of steps illustrated in Fig.17 below:


Fig.17: sequence of steps that must be performed by the startup function of a libagents application

Following is a detailed presentation of the sequence of steps illustrated in Fig.17 above:

  1. this step is required by some, but not all, host frameworks in order to initialize the application and/or the host framework

    • for example, the "Host framework INIT" step in a Qt 5-based application will consist of:
      QApplication qApp(argc, argv); // create the QApplication object & pass args
  2. this step creates the application's Core object: this is achieved by invoking the Core object's constructor, e.g. "MyApplicationCore *myApplicationCore=new MyApplicationCore", where 'MyApplicationCore' is the class name of the application's Core object, which must be derived from the Core base class provided by the libagents library; this will create the application Core object (see "The Core object" and "Initializing the Core module" paragraphs earlier in this document, step (A) in Fig.7)

    • Note: it is not the responsibility of the startup function to create (and initialize) the sub-component objects of the Core object (i.e. the Task, Thread, Agent, and Core Utility objects); instead, this functionality must be implemented by the Core object itself (see the "Initializing the Core module" paragraph earlier in this document)
  1. after the Core application object has been created (together with any sub-component objects that it may contain), the next step is to create the application's Shell Controller object by invoking e.g. "MyShellController *myShellController=new MyShellController(myApplicationCore)", were 'MyShellController' is the class name of the application's Shell Controller object (see "The Shell Controller object" paragraph earlier in this chapter), and 'myApplicationCore' is a pointer to the application Core object

  2. this step creates [the startup configuration of] the Shell objects (see "The Shell objects" paragraph earlier in this chapter)

  3. after the Shell objects [in their startup configuration] and the Shell Controller object have been created, the next initialization step is to cross-link the Shell Controller object with each of the Shell objects and with the application's Core object: specifically, the Shell Controller object includes a set of pointers which have to be initialized such that they each point to one of the Shell objects, and each Shell object includes a pointer which has to be initialized to point back to the Shell Controller object; additionally, the Shell Controller object also contains a pointer that has to be initialized to point at the application Core object, and the application Core object contains a pointer that has to be initialized to point at the Shell Controller object (see "The Shell Controller object" paragraph earlier in this chapter)

  4. at this point the startup configuration of the libagents application has been completely set up, and the startup function must now start the application's Core object: this is achieved by invoking the Core object's 'Start()' method (see the "Initializing the Core object" paragraph earlier in this document, step (B) in Fig.7)

  5. now the startup function has to initialize and start the Shell Controller object: this is achieved by invoking the Shell Controller object's 'Start()' method (see "The Shell Controller object" paragraph earlier in this chapter)

  6. this step is required by some, but not all, host frameworks as the last step of the application's startup function (e.g. in the case of event-driven host frameworks, this step starts the execution of the application's event loop)

    • for example, the "Host framework RUN" step in a Qt 5-based application will consist of:
      qApp.exec(); // start the QApplication object's event loop

An example application that is built "on top of" the Qt 5.4.2 framework and which illustrates the sequence of steps that must be performed by a libagents application's startup function is available in the libagents-1.0.x distribution package, inside the "libagents-examples/libagents-example-stopwatch-qt" folder (see the function "int main(int argc, char *argv[])" in file "main.cpp").

Multi-threading in the reference Shell module

As it has been described in "The Shell Controller object" paragraph earlier in this chapter, a Shell object that is part of a reference Shell module can send an intercom message to the Core module by invoking the Core module Intercom's "PutMessage()" method indirectly, via the Shell Controller object's "NotifyCore()" relay method (see Fig.14), such that the Intercom's "PutMessage()" method will be executed by the same OS thread that executes the Shell object method which sends the message; in this context, and given the fact that the Intercom's "PutMessage()" method is allowed to be invoked from any OS thread without restrictions (see the description of the Intercom object in the "Intercom messages" paragraph earlier in this document), the libagents library imposes no restrictions regarding which OS thread(s) are executing the Shell objects' methods that are sending intercom messages to the Core module Intercom.

Similarly, as it has been described in "The Shell Controller object" paragraph earlier in this document, the messages sent from the Core module are fetched inside a reference Shell module by the Shell Controller's "ShellExec()" method, which, in turn, invokes the Core module Intercom's "GetMessage()" method (see Fig.14); in this context, and given the fact that the Intercom's "GetMessage()" method is allowed to be invoked from any OS thread without restrictions (see the description of the Intercom object in the "Intercom messages" paragraph earlier in this document), the libagents library imposes no restrictions regarding which OS thread is executing the Shell Controller's "ShellExec()" method that extracts the intercom messages from the Core object Intercom.

In conclusion, the reference Shell module architecture presented in the "Reference Shell module architecture" paragraph earlier in this chapter does not impose, in and by itself, any restrictions regarding which OS threads are executing the Shell objects' methods that send, and respectively extract and process, intercom messages to/from the application's Core module; however, various multi-threading restrictions do apply for the reference Shell module objects, depending on the characteristics of the application's underlying host framework - this issue is addressed in the following paragraph "Host framework integration".

Host framework integration

As it has been previously explained in "The Shell module" paragraph earlier in this document, the Shell module of a libagents application serves the role of interfacing the application core logic with the application's operating environment, i.e. all the system integration functions required by a libagents application must be provided by the application's Shell module. In this context, this chapter describes the detailed architecture of the Shell objects and of the Shell Controller object of a reference Shell module architecture (see Fig.14), such that the internal architecture of said objects makes use of the specific functionalities provided by the target host framework all while complying with the generic architectures presented in "The Shell objects" and "The Shell Controller object" paragraphs above.

Based on how the characteristics of a libagents application's host framework impact the implementation details of a reference Shell module's components, we shall consider a simple host framework taxonomy consisting of two categories, namely "Event-driven host frameworks" and "Procedural host frameworks"; the essential characteristics which differentiate these two classes of host frameworks, together with the way said characteristics impact the architectural details of the Shell objects and of the Shell Controller object of a reference Shell module architecture, are discussed in the following paragraphs.

Event-driven host frameworks

The essential characteristic of an event-driven host framework is that the services it provides to a hosted application come in the form of a collection of predefined "host framework-native objects" which each implement a specific set of control and query methods that can be invoked by the hosted application, and which are each capable of detecting and handling a specific set of "events" that may occur during the execution of the application.

With respect to the event detection mechanism, each event-driven host framework-native object can be programmed to execute a specified "event handler method" whenever a specific event occurs during application execution; then, whenever an event will occur during application execution, the internal mechanisms of the host framework will automatically execute the event handler method associated with said event.

In terms of internal implementation of the event handling mechanism, any application that is built "on top of" a typical event-driven host framework is automatically set up by the host framework to continuously run an internal code loop, commonly referred to as the host framework's "event loop", and said event loop continuously monitors all the conditions/events for which the application has programmed an event handler method [of a host framework native object]; then, whenever one of the monitored conditions/events occurs, the event loop automatically invokes (read: calls) the event handler method associated with said condition/event.

The top-level architecture of an event-driven host framework (as described above), and the way it integrates with a hosted application, are illustrated in Fig.18 below:


Fig.18: top-level architecture of an application built "on top of" an event-driven host framework

As it can be seen in Fig.18 above, the execution of a user application that is built "on top of" an event-driven host framework essentially consists of:

  1. the application's host framework detects various events/conditions that occur in the application's operating environment, then

  2. the host framework's event loop [automatically] invokes the event handler methods of the host framework-native objects which have been programmed to respond to the events/conditions that have occurred, then

  3. the event handler methods notify the user application about the events/conditions that have occurred by invoking the required application functions and/or object methods, and

  4. finally, the user application's functions and/or object methods (may) react upon the application's operating environment by sending commands to the host framework-native objects (by invoking their control methods)

In terms of execution threads, the OS thread that executes the event loop of an event-driven application is commonly referred to as the "event loop thread", and all the host framework objects' event handler methods are executed by the event loop thread (see Fig.18 above). Consequently, all the user application's methods which are invoked by the host framework objects' event handlers are also executed by the event loop thread, and, furthermore, any host framework-native objects' control methods which are called back by the user application methods are also executed by the event loop thread. In conclusion, an event-driven application which does not create OS threads will have all its methods, may they be methods of the user application itself or methods of the host framework's objects, executed exclusively by the host framework's event loop thread.

Event-driven Shell objects

We will here-forth use the term "Event-driven Shell objects" to designate the Shell objects of a reference Shell module architecture which is built "on top of" an event-driven host framework (see the "Reference Shell module architecture", "The Shell objects" and "Event-driven host frameworks" paragraphs earlier in this chapter).

As it has been previously explained in "The Shell objects" paragraph earlier in this chapter, the reference Shell module architecture specifies a generic architecture for the Shell objects (see Fig.15), while the implementation details of the Shell objects depend on the characteristics of the application's host framework; in this context, the following apply to the event-driven Shell objects that are part of a reference Shell module architecture

In terms of execution threads, the control methods and query methods of an event-driven Shell object are executed by the OS thread that invokes said methods, while the event handler methods of an event-driven Shell object are executed by the host framework's event loop thread.

Event-driven Shell Controller object

We will here-forth use the term "Event-driven Shell Controller object" to designate the Shell Controller object of a reference Shell module architecture which is built "on top of" an event-driven host framework (see the "Reference Shell module architecture", "The Shell Controller object" and "Event-driven host frameworks" paragraphs earlier in this document).

As it has been previously explained in "The Shell Controller object" paragraph earlier in this chapter, the reference Shell module architecture specifies a generic architecture for the Shell Controller object (see Fig.16), while the implementation details of the Shell Controller object depend on the characteristics of the application's host framework; in this context, the following apply to the event-driven Shell Controller object that is part of a reference Shell module architecture:


Fig.20: architecture of an event-driven Shell Controller object

In terms of execution threads, the 'Start()' method of an event-driven Shell Controller object is executed by the application's main thread (because the 'Start()' method is invoked by the application's startup function, see the "The application startup function" paragraph earlier in this document), the 'NotifyCore()' method is executed by the OS thread that invokes the method (which may be any OS thread, see "The Shell Controller object" paragraph earlier in this document), and the 'ShellExec()' method, together with any other object methods and/or functions that it may invoke during its execution, are executed by the host framework's event loop thread (because the "Ticker mechanism" is implemented as a host framework-native Timer object, and thus its 'tick' events are generated by the host framework's event loop thread).

Thread-safety in an event-driven reference Shell module

An important characteristic of most event-driven host frameworks that are actively used and/or maintained at the time of writing this document (e.g. Qt, GTK, wxWidgets, C++ Builder, etc) is that by default they implement a single event loop, and thus all the event handler methods of all the host framework-native objects are executed by default by a single OS thread, namely by the host framework's event loop thread (see the "Event-driven host frameworks" paragraph earlier in this document); additionally, many event-driven host frameworks which implement a single event loop mandate that their native objects' methods are invoked exclusively from the framework's event loop thread.

Given the above, the following conditions are sufficient for a reference Shell module implemented "on top of" an event-driven host framework to be single-threaded, and thus compatible with a very broad range of event-driven host frameworks:

  1. the application's host framework implements a single event loop (and thus a single event loop OS thread, see the "Event-driven host frameworks" paragraph earlier in this document)

  2. the Shell objects and the Shell Controller object are implemented strictly as described in the "Event-driven Shell objects" and "Event-driven Shell Controller object" paragraphs above

  3. any and all of the Shell objects' methods are invoked exclusively by other Shell objects' methods, and/or by the Shell Controller's 'ShellExec()' method, i.e. none of the Core module objects should invoke any method of any Shell object

If the three conditions above are obeyed, then any and all activities (read: call chains) inside the reference Shell module will always be triggered exclusively by a host framework event, and thus any and all of the Shell objects' methods will always be executed exclusively by the host framework's event loop thread.

Procedural host frameworks

The essential characteristic of a procedural host framework is that the services it provides to a hosted application come in the form of a collection of predefined "host framework-native API functions" and/or "host framework-native objects", where said host framework-native API functions and the methods of the host framework-native objects can be invoked by the hosted application. In other words, a procedural host framework can only execute commands in response to having its native API functions and/or native objects' methods invoked, but it cannot monitor, nor process, any events/conditions that may occur during application execution.

The top-level architecture of a procedural host framework (as described above), and the way it integrates with a hosted application, are illustrated in Fig.21 below:


Fig.21: top-level architecture of an application built "on top of" a procedural host framework

As it can be seen in Fig.21 above, the execution of a user application that is built "on top of" a procedural host framework essentially consists starting up the application by running its startup function 'main()', and then the 'main()' function invokes various application functions and methods in order to complete its task; in turn, the application functions and methods can then invoke the methods of the host framework-native objects and/or the host framework's API functions whenever the application needs to interact with its operating environment (e.g. read/print a string from/to the application console, send/receive data to/from a network socket, etc).

In terms of execution threads, the OS thread that executes the user application's startup function (illustrated as "main()" in Fig.21 above) is commonly referred to as the application's "main thread", and an application that is built "on top of" a procedural host framework and which does not create OS threads will have all its methods, may they be methods of the user application itself or methods of the host framework's objects and/or API, executed exclusively by the application's main thread.

Procedural Shell objects

We will here-forth use the term "Procedural Shell objects" to designate the Shell objects of a reference Shell module architecture which is built "on top of" a procedural host framework (see the "Reference Shell module architecture", "The Shell objects" and "Procedural host frameworks" paragraphs earlier in this chapter).

As it has been previously explained in "The Shell objects" paragraph earlier in this chapter, the reference Shell module architecture specifies a generic architecture for the Shell objects (see Fig.15), while the implementation details of the Shell objects depend on the characteristics of the application's host framework; in this context, the following apply to the procedural Shell objects that are part of a reference Shell module architecture:

In terms of execution threads, the control methods and the query methods of a procedural Shell object are executed by the OS thread that invokes said methods, while the Shell object's event handler methods are executed by the OS thread that executes the Shell object's 'monitor()' method

Procedural Shell Controller object

We will here-forth use the term "Procedural Shell Controller object" to designate the Shell Controller object of a reference Shell module architecture which is built "on top of" a procedural host framework (see the "Reference Shell module architecture", "The Shell Controller object" and "Procedural host frameworks" paragraphs earlier in this document).

As it has been previously explained in "The Shell Controller object" paragraph earlier in this chapter, the reference Shell module architecture specifies a generic architecture for the Shell Controller object (see Fig.16), while the implementation details of the Shell Controller object depend on the characteristics of the application's host framework; in this context, the following apply to the procedural Shell Controller object that is part of a reference Shell module architecture:


Fig.23: architecture of a procedural Shell Controller object

In terms of execution threads, the 'Start()' method of a procedural Shell Controller object is executed by the application's main thread (because the 'Start()' method is invoked by the application's startup function, see the "The application startup function" paragraph earlier in this document), the 'NotifyCore()' method is executed by the OS thread that invokes the method (which may be any OS thread, see "The Shell Controller object" paragraph earlier in this document), and the 'ShellExec()' method, together with any other object methods and/or functions that it may invoke during its execution, are executed by the dedicated OS thread that executes the Shell Controller's 'ShellTicker()' method.

Thread-safety in a procedural reference Shell module

An important characteristic of most procedural host frameworks that are actively used and/or maintained at the time of writing this document (e.g. Win32/64, POSIX-compatibles, etc) is that they do not provide any inbuilt multi-threading protection for many of their their native objects' methods and/or API functions; in this context, the Shell module of a user application that is built "on top of" a typical procedural host framework should either be single-threaded, or it should provide its own multi-threading protection mechanism when invoking the host framework's object methods and/or API functions.

Given the above, the following conditions are sufficient for a reference Shell module architecture implemented "on top of" a procedural host framework to be single-threaded, and thus compatible with a very broad range of procedural host frameworks:

  1. the Shell objects and the Shell Controller object are implemented strictly as described in the "Procedural Shell objects" and "Procedural Shell Controller object" paragraphs above

  2. any and all of the Shell objects' methods are invoked exclusively from methods of other Shell objects, and/or from the Shell Controller's 'ShellExec()' method, i.e. none of the Core module objects should invoke any method of any Shell object

  3. the procedural Shell objects' "ticker mechanism" (see the "Procedural Shell objects" paragraph above) is executed by the same OS thread as the procedural Shell Controller's "ticker mechanism"(see the "Procedural Shell Controller object" paragraph above): a simple implementation of this condition is to have the Shell Controller's 'ShellExec()' method successively invoke, at the beginning of its execution, each of the procedural Shell objects' 'monitor()' method; this is illustrated in Fig.24 below:


Fig.24: thread-safe architecture for a procedural reference Shell module:
all the call chains inside the Shell module start from the Shell Controller's 'ShellTicker()' method, and thus all the methods in the Shell module are executed the OS thread that runs the 'ShellTicker()' method, i.e. the Shell module is single-threaded and thus thread-safe

If the three conditions above are obeyed, then any and all activities (read: call chains) inside the Shell module will originate exclusively from the Shell Controller's 'ShellTicker()' method, and thus any and all of the Shell objects' methods will always be executed exclusively by a single OS thread, namely the OS thread that executes the Shell Controller's 'ShellTicker()' method.

Porting to a new host framework

As it has been previously described in "The architecture of a libagents application" paragraph earlier in this document, all the integration functions of a libagents application with its operating environment are performed exclusively by the application's Shell module, while the application's core logic is implemented in a platform-independent way by the application's Core module; in this context, and given the reference Shell module architecture as described in the "Reference Shell module architecture" paragraph earlier in this document, the process of porting a libagents application from one host framework to another can be easily formalized, and it consists of the following steps:

  1. re-writing the implementation of the Shell objects' methods: because the signature and functionality of the Shell objects' methods is host framework-independent, only the implementation, but not the functionality, of the Shell objects' methods needs to be changed (see "The Shell objects" paragraph earlier in this document)

  2. re-writing the implementation of the Shell Controller object: this step will require re-writing the implementation of the Shell Controller's "Start()" method, and, in most cases, changing the implementation of the Shell Controller's "Ticker mechanism" and changing the Shell Controller's base class (if any, see "The Shell Controller object" paragraph earlier in this document)

  3. re-writing the implementation of the application's startup function: as it has been previously described in the "The application startup function" paragraph earlier in this document, the startup function of a libagents application consists of one or two host framework-specific, application-independent, code block(s), and a contiguous host framework-independent, application-specific, code block; in this context, the process of porting the application's startup function consists of changing only the host framework-specific code block(s) in the startup function according to the new host framework specifications (no changes will [generally] be required to the application-specific code block)

The libposif library

The 'libposif' library is a multi-platform implementation of several key system interface objects (e.g. networking objects, file system interface, utility objects, etc) which can be used out-of-the-box as Shell objects in a reference Shell module implementation.

The libposif library is available at http://www.itgroup.ro/libposif and http://libposif.sourceforge.net.

Implementation of cross-referencing objects

A problem which will often be encountered when implementing a libagents applications will be object cross-referencing, i.e. when the declarations of two classes A and B both contain references to objects of the other class' type. In this case, the naive C++ class layout where the header file of a class simply #includes the header files of all the classes it depends upon (whether by instantiation of objects, or by object references) cannot be used without special precautions in conjunction with the cross-referencing classes, because said naive solution would lead to 'undefined symbol' errors during compilation.

The C++ language allows for two main approaches for solving the class cross-referencing problem:

  1. the first approach consists of using the 'class' forward declaration prefix when declaring a reference to an object inside the referrer class declaration, and #including the header files of the cross-referencing classes in each others' header files (note that without using the 'class' forward declaration keyword prefix, an 'undefined symbol' error would be issued at compilation in the referred class' header file); this approach is illustrated for two cross-referencing classes Class1 and Class2 below:

    • // Class1 header file
      #ifndef _class1_h_
      #define _class1_h_
      #include "class2_h" // include Class2's header file in Class1's h file
      class Class1 {
      class Class2 *class2Reference; // use the 'class' prefix keyword
      [...]
      }
      #endif

      // Class1 implementation file
      #include "class1_h"
      [...]
      // code has access to both Class1 and Class2's header files
    • // Class2 header file
      #ifndef _class2_h_
      #define _class2_h_
      #include "class1_h" // include Class1's header file in Class2' h file
      class Class2 {
      class Class1 *class1Reference; // use the 'class' prefix keyword
      [...]
      }
      #endif

      // Class2 implementation file
      #include "class2_h"
      [...]
      // code has access to both Class1 and Class2's header files

    The approach (1) illustrated above is an elegant solution to the class cross-referencing problem in the sense that it uses the naive coding style whereby the header file of a class #includes the header files of all the object types it makes use of, such that when inspecting the #include section of a class' header file it is immediately visible what other classes it depends upon (i.e. including any classes it references). However, this solution has a significant drawback, namely: consider a set of classes {A1...An} whose header files #include the header file of a class B, and also consider that class B is cross-referenced with a class C and thus it #includes class C's header file; in this case, each time the header file of class C changes, both class B and all the classes {A1...An} need recompilation when building the project (because the header files of the classes {A1...An} #include [indirectly], via class B's header file, the header file of class C)

  2. the second approach consists of using the 'class' forward declaration prefix when declaring a pointer to a referenced object inside the referrer class declaration (i.e. same as in (1) above), and #including the header file of cross-referencing classes in each others' definition (cpp) files (note that without using the 'class' forward declaration keyword, an 'undefined symbol' error would be would be issued at compilation in the referrer class' header file); this approach is illustrated for two cross-referencing classes Class1 and Class2 below:

    • // Class1 header file
      #ifndef _class1_h_
      #define _class1_h_
      class Class1 {
      class Class2 *class2Reference; // use the 'class' prefix keyword
      [...]
      }
      #endif

      // Class1 implementation file
      #include "class1_h"
      #include "class2_h" // include Class2's header file in Class1's cpp file
      [...]
      // code has access to both Class1 and Class2's header files
    • // Class2 header file
      #ifndef _class2_h_
      #define _class2_h_
      class Class2 {
      class Class1 *class1Reference; // use the 'class' prefix keyword
      [...]
      }
      #endif

      // Class2 implementation file
      #include "class2_h"
      #include "class1_h" // include Class1's header file in Class2's cpp file
      [...]
      // code has access to both Class1 and Class2's header files

    The approach (2) illustrated above solves the problem of class cross-referencing, but it lacks in elegance in the sense that, in order to identify the classes upon which a given class depends on, one has to inspect both the class' header file (for any header files it may include), and also the class' definition file (for the header files of the classes it references). However, this approach (2) has a significant advantage over (1): specifically, given a set of classes {A1...An} whose header files #include the header file of a class B, and if class B is cross-referenced with a class C and thus its .cpp definition file #includes class C's header file, then a change in class C's header file will require recompilation of only class C and B, while none of the classes {A1...An} will need recompilation when building the project (because each of {A1...An} includes only class B's header file, which neither changed, nor does it #include the changed header file of class C)

The two approaches presented above both solve the cross-referencing classes problem in C++, and [one of them] must be used on all occasions where class cross-referencing needs to be implemented in a C++ application. In terms of choosing one solution over the other, the relative advantages and disadvantages of the two solutions (as presented above) should be considered.

Cross-referenced 'enum's

None of the two solutions presented in the "Implementation of cross-referencing objects" paragraph above can be used for solving cross-referencing 'enum' values in the .h declaration files of cross-referencing classes; more specifically, two cross-referencing classes can use each others 'enum' definitions (e.g. for initializing a data member, or for dimensioning an array, etc) exclusively inside their .cpp definition file (e.g. in the constructor definition), but not inside their .h declaration file; else, the compilation process will fail with 'undefined symbol' errors.
The example below illustrates the correct, and respectively incorrect, usages of cross-defined enum values in two cross-referencing classes A and B:

Caveats and limitations

Messaging performance

The transmission of a message, whether it is a targeted message or a broadcasted message, incurs a cost of ~0.05ms/message for the message sender, and an additional ~0.05ms/message for each message receiver, on a typical x86@2GHz single-threaded CPU.

For example:

Profiling tests have been performed on several libagents applications, and they suggest that memory allocations and deallocations take about 80% of the time the application spends with executing libagents-1.0.x library methods. The tests suggest that replacing several key storage objects in the libagents-1.0.x library which are implemented as dynamic objects with (quasi)-statically allocated objects could improve the messaging performance by a factor of up to 4x. Attempting the above-mentioned optimizations is planned for a future revision of the libagents library.

Download

The latest version of the libagents library source code and documentation is available at: