Object Management In Depth

As the Pandora Engine is heavily based around object orientation, it is not only important to understand the basics behind the concept, but also to learn the method of implementation that was used in creating the Pandora Engine. Although the foundation of the system design is grounded in the basic concepts, its continued development over the years has seen it evolve into something else, which you already know as MOO. In this section we look at the object management of Pandora in more detail so that you may gain a more thorough understanding as to how it all works.

Creating New Objects

There are two functions in the object kernel that allow you to create new objects: CreateObject() and NewObject(). The CreateObject() function is provided for ease-of-use, as it is possible to duplicate its functionality outside of the object kernel. NewObject() sits much closer to the heart of the system, and is the only function that is capable of creating new object structures. Calling the function is simple - all you need to provide is the ID of the class that the object is to be based on, a variable to store a reference to the new object, and any special flags that may be required. Here is a code segment that creates an object based on the Picture class:

   OBJECTPTR Picture;
   if (NewObject(ID_PICTURE, NULL, &Picture, NULL) IS ERR_Okay) {
      ...
   }

If you have come from a background in object oriented programming you can already see that the process of creating a new object is more complex than in a language like C++, but for good reason. In Pandora, objects can be created with all kinds of attributes, such as whether they are public, private, shared, networkable and whether they are acting as children or parents. The object kernel keeps track of who owns each object, the resources that are owned by each object and who-is-doing-what with each individual object. With the background process being much more complicated, the options that are available to you as a developer are markedly increased. Try to keep this in mind, as we don't want you to be overawed by the level of functionality that is available to you if you are new to this type of methodology.

The code example that we used above showed you how to create a private object - now let's compare it to this next code segment that creates a public Picture object:

   OBJECTID PictureID;
   if (NewObject(ID_PICTURE, NF_PUBLIC, NULL, &PictureID) IS ERR_Okay) {
      ...
   }

The key difference between the two examples is that the object handle that we received in the first case was a direct address, while the public version receives an ID based object handle. Learning how to manage objects based on ID numbers is an important issue - gaining access to an object is not always as simple as talking to it directly through an address. The exact differences between private and public objects, and how to manage object ID's will be examined in the next section.

For a thorough overview of the two functions that we have mentioned, refer to the CreateObject() and NewObject() sections in the kernel manual.

Public, Private or Shared?

Knowing the difference between public and private objects and how to interact with them is one of the key factors that you need to learn before diving into your first Pandora based project. This can be considered unusual, as most systems will allow programmers to develop even the most complex projects without the developer ever needing to know the difference between public and private memory. In comparison, the Pandora Engine's use of public memory is of significant note, and you will be interacting with such memory areas from the outset.

If you're not sure what the difference between public and private memory is, here is a quick explanation. Private memory is the default type of memory allocation - only your program can access it, which means that it is also protected from illegal accesses from other programs. On the other hand public memory is the exact opposite, which means that every program can gain access to it. In order to prevent programs clambering for access to the same memory block at once, access to public memory is regulated through management functions that ensure that only one program can obtain access to a public memory block at any given time. The benefit of public memory is that it makes it easy for programs to share data, and can often assist in preventing data from being duplicated in memory.

So what's the difference between public and private objects? The principle is much the same - private objects are accessible by your program only, while public objects are accessible by all programs. A public object is typically created when an object should have no relationship to the program that created it, i.e. the object needs to function independently, as if the object itself were an API or program. When the program that created the object dies, the object can remain in memory so that others can continue to interact with it. As you can guess, this certainly has its uses and it won't be long before you start encountering these objects during your project development cycle. The impending question is that of access - how do you access a public object and prevent others interfering with your use of it?

Two functions are provided for gaining exclusive access to objects: AccessObject() and ReleaseObject(). Here is a code segment that attempts to gain access to the Picture object that we created earlier, and releases it if it is successful:

   if (AccessObject(PictureID, 5000, &Picture) IS ERR_Okay) {
      ...
      ReleaseObject(Picture);
   }

In order to gain access to a public object you only need to know one thing - the ID number. In the above case we knew the ID because we had created the object earlier and stored its ID in the PictureID variable. If someone else created the object that you need to use, there are a number of other functions that you can use to aid you in searching for it, such as FindObject(). When gaining access to an object, you also need to specify a time-out period in milli-seconds so that your program will give up waiting if someone has locked the object for an extended period of time. In our example, a setting of 5000 will cause the program to wait for up to 5 seconds before giving up. When a call to AccessObject() executes successfully, an address pointer will be returned that gives access to the object structure. Once you have the address you may do with the object as you please, before finally releasing it with a call to ReleaseObject().

Hopefully you've grabbed the basics, as we're now going to add an element of confusion known as the shared object. By their definition, public objects are shared by default, because creating a public object that you're not going to share with anyone is a wasted exercise. But you might be interested to know that you can also share private objects with other programs by using a special flag when calling NewObject(), known as NF_SHARED. Now we did say that private memory areas were accessible to your program and no others, so if a program cannot gain access to an object's memory, the obvious question is how does this work? The secret is in messaging. For all intents and purposes, privately shared objects are treated just as if they were public objects, but in order to talk to them you need to send messages to the owner of the object. When the owner of the object receives your messages it will process them and if necessary, send results back to your program. Shared objects also differ slightly in that when the parent program dies, any shared objects that were created will go down with it. The main advantage of shared objects is that they are protected within the owner's memory space, but they have a slight speed disadvantage because of the messaging factor.

In summary, here is a list of the different types of objects with their associated advantages and disadvantages:

 PUBLIC
  • Can be accessed and interacted with by any application directly, because the object structure and its data lies in public memory.
  • They do not use the messaging system.
  • They take up public memory which on some systems can be a limited commodity.
  • They are potentially easier to corrupt by programs that are written incorrectly.
  • When exclusively accessed, they sometimes need a little extra time to resolve internal memory addresses.
 SHARED
  • These objects are allocated within the memory space of the tasks that create them.
  • A shared object can only be accessed directly by its parent task.
  • Other tasks can discover shared objects by looking for them on the system's shared object list.
  • If an external task tries to gain access or use the object, messages are sent between tasks to attain the desired results.
 PRIVATE  
  • These objects can only be accessed and interacted with by their parent task.
  • Private objects are invisible to the outside world, but it is possible for other tasks to send messages to them if the ID is intentionally distributed to external tasks.
 CHILD  
  • This object type can only be allocated when writing code for a class module.
  • Child objects have all the attributes of a private object, but can only be accessed when the task is running in the context of the child's parent. There is no other way to gain access to these object types.
  • Child objects do not appear in object lists, effectively making them hidden from everything except the parent object.

Structure Layout

Now that you know how to create objects, its time we gave you an idea of how objects are 'physically constructed' when they are created. All object types, whether they are public, shared or private have a physical structure that can be accessed and manipulated by program and class code alike. Object structures are split into two parts - a standard set of fields at the beginning of the structure, known as the 'header'; followed by the object fields that contain data for the object, as defined by its class. The object header is always present and can be easily accessed, but the object fields can only be accessed directly if they have been 'published' by the developer. If the fields are not published, then they have to be accessed using the field support functions - more on this later in the document.

Here's a breakdown of the object header format:

 TypeNameDescription
 CLASSIDClassIDThe ClassID refers to the unique ID of the class that the object belongs to. E.g. If the object belongs to the Screen class, then its ID will be ID_SCREEN, as defined in the system/registry.h include file. Whenever you want to identify an object as quickly as possible, this is the field where you should look first.
    
 CLASSIDSubIDIf the object is being managed by a sub-class, then the SubID will be set to the ID of the sub-class that is managing the object on behalf of the base-class. You can read more about base-classes and sub-classes in the Class Manual.
    
 OBJECTIDUniqueIDThe UniqueID field will tell you the unique identifier that has been assigned to the object by the system. If you don't know the ID of an object but have its address, this field provides a quick way of obtaining the unique ID.
    
 OBJECTIDOwnerID  The OwnerID field refers to the unique ID of the object's owner. In almost all cases, an object will have an owner unless it has been marked as an untracked object. Knowing the owner of an object can be exceptionally useful, as it designates its position within the system's object hierarchy. This also has an impact on resource tracking - if the owner of an object is destroyed, then the object will also be eliminated as it will be treated as a resource of the owner. The owner of an object can be altered through the SetOwner() function - do not attempt to change the owner by writing to the OwnerID field directly.
    
 WORDObjectFlags  

This field can provide you with interesting information on an object's status. All flags are read-only and most are private to the system. Here are the flags that you are allowed to check:

 NF_NOTRACKIf this flag is set then the object is not being tracked as a resource.
 NF_PUBLICIf the object is publicly accessible, this flag will be set.
 NF_NETWORKThis flag is set if the object has been given permission to be shared over a local network or the Internet.
 NF_INITIALISED  If the object has been initialised then this flag will be set.
 NF_SHAREDIf the object is available to all tasks, this flag is set.
    
 WORDMemFlags Recommended memory allocation flags are set in this field. This field is extremely important for developers writing code for new and existing classes. When allocating memory blocks that are to be resourced by an object, this field must be referred to so that the correct type of memory is allocated. For example, if the object is public, this field will be set with the MEM_PUBLIC flag so that correct memory types are allocated. Further information is provided in the Class Development Tutorial.
    
 OBJECTPTR  ClassYou can read this field to find out the various details about an object's class. Refer to the Class Manual for details.
    
 struct Stats *StatsThis field is private, and is only for use by the object kernel.

Please note that the entire object header is read-only. Writing to the fields will corrupt the object and potentially cause your program to crash.

Field Management

The vast majority of classes that have been developed using the Pandora Engine use fields for storing and retrieving data that is specific to their objects. For instance, the Picture class stores information regarding image height, width, colours, file location and other details required to effectively manage image data. Although the concept itself is a basic one, the circumstances surrounding field management become complicated very quickly when one examines the more crucial issues. For instance, if the string field of a public object is set by your program, what will happen if some other program tries to read the same string address through the object? If the string was allocated as a memory block, who is responsible for deallocating it? What about fields that need to calculate their values dynamically? If a field is a simple type such as a number, do you need to waste time reading and writing to it through function calls, or can you access it directly?

Although there are some strict guidelines surrounding some of these issues, the exact terms surrounding field access will be determined by the developer of an object's class. Depending on what the class is intended for, the developer may designate that certain parts of the field structure will be directly accessible, other parts indirectly accessible, or the entire object may be accessible only through indirect means. Furthermore, fields may not only be designated as readable and writeable, but various combinations thereof can also be declared - for instance, a field may be directly readable, but indirectly writeable. Confused? Let's provide you with some clarification. A direct field access is as you might suspect, a simple instruction that copies or reads data directly from an object's field. Here are some examples:

   1. Bitmap->Width = 100;

   2. Height = Bitmap->Height;

   3. Font->String = "Hello World";

On the other hand, an indirect field access involves reading or writing a field through an abstract mechanism, which will normally involve some type of function call. Object oriented languages will typically offer indirect field access through method calls only. For instance, C++ does little to distinguish the difference between method-based fields and embedded functions. The MOO design structure requires that a clear line is drawn between the two, requiring developers to make an exact distinction between fields and methods. So while a call such as Bitmap->SetWidth(100) may be commonplace in some OO languages, it is an illegal design construct in Pandora. Instead, special functions are provided for field access. They are:

Get FunctionsSet Functions
GetField()SetField()
GetFields()SetFields()
GetFieldVariable()SetFieldVariable()

For detailed information on how these functions operate, refer to their relevant sections in the Object Kernel Manual. Using the listed functions, the direct field examples that were given earlier could be rewritten as follows:

   1. Width = 100;
      SetField(Bitmap, FID_Width, FT_LONG, &Width);

   2. GetField(Bitmap, FID_Height, FT_LONG, &Height);

   3. SetFieldVariable(Font, "String", "Hello World");

An important question remains - how do you know when to use the field management functions as opposed to direct field access? The only way to know outright is to check the documentation for the class that you are using. By default, it is recommended that you always use the field management functions except in cases where you have thoroughly read the documentation for the class and understood the level of access that has been provided to you by the developer. Function calls are always safe to use, while direct access is only safe if the developer has allowed for it.

Finally we come to the issue of permitted field types and type naming conventions. There are a limited number of field types available for use in object construction, as shown in this table:

 TypeDescription
 LONGA 32-bit integer value ranging from -2,147,483,647 to 2,147,483,648.
 LARGEA 64-bit integer value, large enough for almost any situation.
 FLOATA 32-bit floating point value with low accuracy and potential for rounding errors.
 DOUBLEA 64-bit floating point value with high accuracy and small potential for rounding errors.
 POINTERA standard 32-bit address space pointer.
 STRINGA 32-bit address space pointer that refers to a null-terminated string.
 VARIABLE  A variable field can support some, to all of the above field types.

Objects cannot declare 8 or 16 bit field types within their structure unless they are private to the class. No mechanism is provided for the support of unsigned integers. The field management functions provide full support for type-conversion, so you can for example write to a float based field using long, large or double values. If a field is accessed using a type that cannot be converted, a type mismatch occurs and the read/write procedure is aborted.

Object Interaction: Actions and Methods

The most important aspect of object management is interaction. Creating an object is one thing, but they are next to worthless until you actually do something with them. That's where actions and methods come into the picture, as they form the backbone of the object communication system. Before you even begin programming, a thorough background knowledge of how the communication system works is essential.

Let's start with an explanation of what we mean when using the term 'action'. An action is a predefined function call that can be called on any object. At the time of writing, the Pandora Engine supports 46 different action types, all of which have their own associated name and ID number. Examples of commonly used actions are 'Init', 'Free', 'Read', 'Draw' and 'Activate'. As you might notice the action names tend towards ambiguity; for instance it is difficult to tell what the result would be if we were to execute the 'Read' action on an object, unless we were to refer to the documentation for the object's class and find out what sort of data it uses for I/O operations. How is this helpful? Predictability is the first reason. If you want to read data from an object, you'll know to use the Read action because it is the system standard for reading raw data streams. If you need more detail on how the object's class handles support for reading data, simply check the documentation for the object's class under the Read section. Abstraction is the second reason. Certain actions such as 'Move' lend themselves particularly well to this - if you want to move an object to a new position then you simply call its Move support function. If the object supports the concept of movement, off it will go. This is the principal beauty behind the action system - each class follows a general guideline for action implementation to give a certain level of predictability, but the end result and the methods used to obtain that result lie in the hands of the class developers.

So how do you get an object to execute an action for you? Here are a few examples:

   1. Action(AC_Init, Picture, NULL);

   2. ActionMsg(AC_Free, Picture, NULL);

   3. struct acMove move;
      move.XChange = 30;
      move.YChange = 15;
      move.ZChange = 0;
      Action(AC_Move, Window, &move);

As you can see there are two primary functions available for action execution - Action() and ActionMsg(). The Action() function executes the requested action immediately, while ActionMsg() uses a delayed execution model via messaging. The difference between the two will be examined in the next section, but for extensive information on how these functions operate, please read up on the Action(), ActionMsg() and ActionTags() functions in the Object Kernel Manual. You may also be wondering where you can find documentation on all the different action types and what they do - if so, check the Action Document as it documents all the actions and their associated guidelines in significant detail.

Now that you know what an action is, we can move onto methods. A method is defined as a function call that is specific to the class of a given object. A class may define as little or as many methods as it likes, all of which can be given their own name and associated arguments. For the most part, methods are rarely implemented as part of a class' development as most functionality can be shoe-horned into the action and field support of the system. However, as the available actions cannot provide cover for every possible situation, methods are still needed on an occasional basis.

The good news about methods is that they use the same functions provided by the kernel for action execution. In fact, in all cases the object kernel has been designed to treat actions and methods as all being under the same umbrella. What's more, you can execute methods using the message system, a feature which is very particular to the Pandora Engine's design. Here are some examples of valid method calls:

   1. struct mtReadConfigInt read;
      read.Section = "Section";
      read.Item    = "Item";
      Action(MT_ReadConfigInt, Config, &read);

   2. ActionMsg(MT_About, InspectorID, NULL);

The best way to obtain information on the methods supported by a class is to read the class documentation. This will tell you the names of any supported methods, details on any arguments you are expected to provide, and an overview of how each method operates. Also, keep in mind that methods are not interchangeable, i.e. do not try to execute a method documented in one class on an object that belongs to some other class. Unpredictable results will almost certainly occur.

Direct Calls Vs Object Messaging

In the previous section we used two similar looking functions, Action() and ActionMsg(). Knowing the difference between these two functions, and when to use one over the other is absolutely vital as the end results are not identical.

Calling the Action() function results in the immediate execution of an action routine on an object. As the routine will be executed in your own task space, it is essentially the same as making a direct function call. The only restriction is that you need to have the object's address in order to make a direct action call, which restricts you to using it on objects local to your task, or objects that have been allocated publicly. The primary advantage of calling an action directly is speed - with no messaging system to get in the way, the fastest route is always taken. Apart from its restrictions, there are no disadvantages in using the Action() function.

At first glance the ActionMsg() function looks almost identical to Action(), apart from one important factor, which is that it takes object ID's rather than addresses. The fact that it is based on unique identifiers means that it can execute actions on objects that are contained within a task space separate to your own, simply by passing messages through the system. The primary advantage of this function is that it can be used on any object type, and provides a certain level of crash protection for your task if there are any problems with the object's structure. Unfortunately, it is disadvantaged in that message passing is much slower than direct execution, and there is always potential for problems if the other task fails to respond to the message request. Still, being able to talk to other tasks within the system can be essential and you will most likely find yourself using the messaging system more often than you might imagine.

Summary

If you have read and understood all of the information in this section of the manual, congratulations. You now have enough information to start analysing the source code of other Pandora based programs and even start thinking about writing your own programs from scratch. You may also want to start looking at the functionality of the object kernel in more detail to start understanding how other parts of the system work. From this point the documentation gets very detailed - if you're not sure that you understood this document fully, examine the source of some existing Pandora programs, then return here and revise the information to obtain a thorough understanding. Good luck!


Copyright (c) 2000-2001 Rocklyte Systems. All rights reserved. Contact us at feedback@rocklyte.com