-
-
Notifications
You must be signed in to change notification settings - Fork 2
Parasol Objects
- Introduction
- Object Identity
- The Local Object Tree
- Object Tree Resource Management
- The Importance of Context
- Access Management
- Object Locking
- Introspection
- Threads
- A Closer Look at the Object Structure
Parasol objects are run-time instances of class interfaces. They carry state in the form of a C struct
and are all backed by a common C++ class
definition named Object
. Parasol's objects have inherent features that go beyond those typically found in OO languages. This includes:
- An ownership model - an object is always owned by another object.
- Locking - any object can be locked to prevent conflict with other threads.
- Identification - all objects have a UID and can optionally be given a name.
- Discoverability - objects can be found using their names.
- Resolvability - the pointer to any object can be resolved through their UID.
- Independent processing - object functions can be run in their own thread, independent from the caller.
- Messaging - non-urgent object functions can be executed via messages.
- Context - all operations are carried out in the context of an object, i.e. the equivalent of
this
is tracked at all times, and can be referenced in regular functions. - Resource tracking - the ownership model ensures that an object can never be left dangling as a resource leak.
The creation of an object can be achieved in numerous ways depending on the programming language, but ultimately all objects are processed through a single Core API function known as NewObject()
. For instance:
objAudio *audio;
auto error = NewObject(ID_AUDIO, NF::NIL, &audio);
After configuring an object, it is initialised using InitObject()
:
error = InitObject(audio);
The entire process of creating, configuring and initialising an object can be simplified using decorators, and C++ includes the Create
class for this purpose. The basic interface can be used as follows:
pf::Create<objFile> file = {
fl::Path("temp:new_file.txt"),
fl::Flags(FL::WRITE|FL::NEW),
fl::Permissions(PERMIT::READ|PERMIT::WRITE)
};
if (file.ok()) {
...
}
Note the use of the ok()
call to confirm that object was initialised successfully. An error
field is also provided that will be set to a value other than ERR::Okay
if a problem occurred.
Parasol classes in C++ also implement alternative means of creation that are more concise, with the addition of global()
, local()
and untracked()
variants:
objFile *file = objFile::create { fl::Path(glClassBinPath), fl::Flags(FL::READ) };
extFile *file = extFile::create::global(fl::Path(path));
extVectorScene *scene = extVectorScene::create::local(fl::PageWidth(1024), fl::PageHeight(768));
extbitmap *bmp = extBitmap::create::untracked(fl::Width(1024), fl::Height(768));
Note that these variations return a direct pointer to the object or NULL
if an error occurs. The original error code is discarded when these interfaces are used.
All objects are given a unique ID on creation, which is directly readable from the UID
field. A useful benefit of UIDs is that they can be stored as object references instead of pointers. UID's have a safety advantage over pointers because they need to be resolved with GetObjectPtr()
or AccessObject()
prior to use. Successful resolution guarantees that the object exists. A pointer on the other hand can carry the risk of the object being collected, leaving a now invalid pointer reference. Always give consideration to this risk when storing naked pointers.
Objects can also be given a name identifer on their creation by the client. Named objects are easier to track in debug logs and can be searched for by FindObject()
. The name of an object can be read from its Name
field.
All programs written in Parasol have their own local object tree (OT) that is self-managed. The tree itself is a side-effect of Parasol's ownership model, which is a straight-forward guarantee that every resource will be owned by an object. This makes it impossible for an object to be left as a dangling resource - even an object that has been globally scoped will be tracked back to its process, which is represented as a Task object. The ownership model works both ways - an object not only has an owner, but it can also own many children. An event system also exists whereby changes in the ownership model will trigger callbacks.
The OT is frequently leveraged as an asset because it can build meaningful structures of parent-child relationships. The vector scene graph is one such example of a complex structure that we use to represent each program's entire user interface.
The OT is more than just a representative structure, it also enforces resource tracking constraints on the program. Terminating an object will also terminate the child objects that are associated with it. This enforced collection process is both convenient and powerful, resulting in immediate resource collection without the unpredictability of a garbage collector.
It is important to bear in mind that Parasol's ownership model applies to all resources and not just objects. This means that memory allocations are also affected. Whenever a class allocates memory, it will be owned by the object that created it. When the object is terminated, all memory allocations will be removed with it. This is extremely convenient, but be aware that this is not considered an excuse to leave memory allocations completely orphaned. Failing to terminate memory resources appropriately will still be reported to the debug log.
As stated previously, Parasol guarantees that resources are always tracked to an object. It does this in the background and without involving client code. This is achieved by maintaining a persistent context at all times, which is a direct reference to the currently active object. Conceptually this bears a similarity to the C++ this
keyword, but unlike C++, the context is accessible at all times, from any part of the program. The context is managed with a stack, much like a function stack. When an object makes a call to another object, a new context is pushed onto the context stack and this will be popped when the interface returns. The context stack gives rise to features like Parasol's context-based debug log:
Show() [systemdisplay:713]
NewObject() [systemdisplay:713] Pointer #1725, Flags: $1
CreateObject() [Pointer:1725] Config
Init() [Pointer:1725] Config #1726, Owner: 1725
CreateObject() [Config:1726] File
Init() [Config:1726] File #1728, Owner: 1726
Init: [File:1728] File not found "user:config/pointer.cfg/".
Free() [File:1728] Owner: 1726
SetField: [Pointer:1725] 123456789ABCDEF
Free() [Config:1726] Owner: 1725
Init() [systemdisplay:713] Pointer #1725, Name: systempointer, Owner: 512
CreateObject() [systempointer:1725] Bitmap
Init() [systempointer:1725] Bitmap #1731, Name: customcursor, Owner: 1725
Init() [customcursor:1731] Size: 32x32 @ 32 bit, 4 bytes, Mem: $00000000, Flags: $00000840
Show() [systempointer:1725]
Free() [Module:710] vector, Owner: 676
As seen here, the use of an operating context in conjunction with the function stack tells us the exact object that made each call.
The current context can be retrieved using the Core's CurrentContext()
function, and it will always return an object that supports the C++ Object
interface and the Core's generic object functions. CurrentContext()
is often used in callback functions where recalling the active object is necessary to interact with it.
When writing code that involves managing objects and their references, it is important to ensure that the code is operating safely. This primarily means that i) retained object pointers do not go stale; ii) there are no conflicts between threads using the same object(s).
Storing direct pointers to objects is generally safe. Direct object pointers can even be shared between threads safely if they are locked correctly with the provided methods. However, if there is a chance that an object can be collected via means outside of client control, pointer access could be deemed unsafe. In such cases, store the object reference as a UID instead and resolve the UID to its pointer form as needed.
Resolving an object UID to its pointer can be achieved with GetObjectPtr()
. This is the fastest means of pointer resolution but comes with the caveat that there is no protection from race conditions involving other threads that could terminate the object.
If your code prevents the possibility of an object being involved in an unscheduled termination, C++ threads can take advantage of using the object pointer directly and lock the object using in-built atomic operations. The Object.lock()
and Object.unlock()
C++ methods are available for this:
Gains direct access to an object with an atomic operation. If the operation fails because the object is already in use, the function reverts to using LockObject()
and the thread will sleep until the object becomes available.
Note that this call can fail if the object is marked for collection, as objects in a collecting state are considered unsafe for active use.
Reverses an earlier call to Object.lock()
. If there are threads waiting on the object's availability, the function will defer to ReleaseObject()
so that the next thread in the queue is woken correctly.
To lock an object that is referenced via its UID, use the AccessObject()
and ReleaseObject()
functions. These functions guarantee safety from all potential race conditions.
The ScopedObjectLock
C++ class provides an interface for both UID and direct pointer locks. It guarantees that the lock is released when its instance goes out of scope. Available constructors are:
ScopedObjectLock(OBJECTID ObjectID, LONG TimeOut = -1)
ScopedObjectLock(OBJECTPTR Object, LONG TimeOut = -1)
ScopedObjectLock(T *Object, LONG TimeOut = -1)
It is possible for lock attempts to fail. The client must call the granted()
method to confirm that the object was locked successfully. To access the object fields and methods, use the ->
operator. A direct pointer to the object can also be gained by using the *
operator.
Parasol supports the run-time introspection of classes. This is achieved by gaining access to the relevant MetaClass
object, which can be achieved by either i) Retrieving the class directly from an object of interest; ii) Calling the FindClass()
function. Detailed information is available in the MetaClass
manual, but in summary the following features are supported:
- Read the field table to retrieve name and type information for all publicly accessible fields, both physical and virtual.
- Read the action table to determine the level of support for contract interfaces.
- Read the method table to learn all available method names and their parameters.
- List all currently active run-time objects for the class.
- Read additional meta information such as file associations, optional flags and unique identifiers.
Parasol integrates threads as a key element of its object oriented design. Conventional threads will typically be written for a very specific purpose that can be executed in isolation. In Parasol it is possible to go further and call object interfaces in their own thread, with minimal overhead in your code. This can be very efficient for off-loading simple tasks in parallel, like decompressing an archive, processing an image or parsing a file.
Executing an object interface via its own thread is managed by one functon, AsyncAction()
, and will work for both action and method interfaces. The call is largely identical to Action()
but for the addition of a callback feature that is executed after the thread has finished. This example calls the Activate
action asynchronously and receives a callback once the process is complete:
auto callback = C_FUNCTION(activate_completed);
AsyncAction(AC::Activate, object, NULL, callback);
Sending message packets in an application is common practice, and is particularly useful for managing non-urgent activities that can be delayed until the next message processing cycle. Application complexity can lead to messages needing to be handled in conjunction with other events and queues, an issue which is handled in POSIX with the select()
function for instance. Parasol's equivalent is the WaitForObjects()
function, which accepts a list of objects and will not return until they are all signalled.
An object enters a signalled state when it is either destroyed or it is manually signalled with the Signal()
action. Some classes have signalling built in for certain conditions being met; for instance threads will enter a signalled state when they are finished executing.
Because signals are thread-safe, WaitForObjects()
can be used to turn the main thread into a message processor while a set of threads perform their tasks. WaitForObjects()
will not break until all of the child threads have finished.
The Object
structure is Parasol's most critical type structure, forming the basis of all instantiated objects no matter the originating class. Because the Object
interface is universal, developing a familiarity of its
public variables and methods is helpful in accelerating development for new users. The following table lists the publicly declared fields:
Field | Type | Description |
---|---|---|
Class | objMetaClass * |
A direct reference to the class that manages the object, this is defined on creation. |
ChildPrivate | APTR |
For sub-classes only, allows private storage to be allocated and referenced here. |
CreatorMeta | APTR |
For client use only, allows custom data of any kind to be stored. This is the only field that can be written directly by the client. |
Owner | Object * |
A direct reference to the object that has ownership, this is defined on creation. |
UID | OBJECTID |
The unique identifier for this object. |
The following C++ methods are available for reading Object
state information.
Method | Description |
---|---|
bool initialised() |
Returns true if the object has been initialised. |
bool defined(NF Flags) |
Returns true if any of the NF type Flags are currently defined for this object. |
bool isSubClass() |
Returns true if the object is the member of a sub-class. |
OBJECTID ownerID() |
Returns the ID of the owner. |
CLASSID classID() |
Returns the ID of the object's class (can be a sub-class). |
CLASSID baseClassID() |
Returns the ID of the object's base-class (never a sub-class). |
NF flags() |
Return all NF state flags currently defined for this object. |
CSTRING className() |
Return the name of the class that this object belongs to. |
bool collecting() |
Returns true if the object is being collected, or otherwise marked for collection. |
bool terminating() |
Returns true if the object is being terminated. |
bool hasOwner(OBJECTID ID) |
Return true if the referenced object ID has ownership. |
The following C++ methods control object locking.
Method | Description |
---|---|
ERR lock(LONG Timeout) |
Efficiently obtains an object lock without a call to LockObject() , keeping overhead to the bare minimum. Will fail if the object is being collected. |
void unlock() |
Reverse a previous call to lock() . |
The following C++ methods simplify the process of getting and setting fields via virtual accessors.
Method | Description |
---|---|
ERR set(ULONG FID, T Value) |
Set a field of type T. All primitive types are supported, while special types include std::arrray , std::vector , FRGB , FUNCTION , std::string and Unit . |
ERR setFields(Args&&... pFields) |
Set one or more field values using field initialisers (same technique as create() ). |
ERR setScale(ULONG FID, DOUBLE Value) |
Set a field with Value and apply the SCALE indicator. |
ERR get(ULONG FID, T *Value) |
Retrieve a field value of type T . |
ERR getPtr(ULONG FID, APTR Value) |
Retrieve an untyped pointer field value. |
ERR getScale(ULONG FID, DOUBLE *Value) |
Retrieve a field value in its scaled format instead of the fixed default. |
T get(ULONG FID) |
Retrieve a field value of type T . No error checking is supported, and the value will be zero on failure. |
During Parasol's build process, C++ interfaces are automatically generated for classes so that our framework is more closely integrated with the C++ language itself. These interfaces are not directly documented, but they are intended to be browsed via your IDE, e.g. via the CTRL + SPACE
convention.
All actions and methods listed for a class get their own C++ stub. For instance, the GetKey
action is supported with a stub of ERR getKey(CSTRING Key, STRING Value, LONG Size)
. The parameters will always match those documented for the given action or method.
Stubs are also generated for setting fields. The use of these stubs is encouraged as they are type-checked and eliminate the question of whether to set a field virtually or through a direct write. Additionally, if the field is virtual then the stub will use an approach that eliminates much of the overhead that result from setting a field by its identifier. The template generated for each field will be in the format ERR setName(T Value)
.