3. ORE Object and Constraint System

This chapter describes ORE, the object and constraint level of Amulet. ORE allows programmers to create objects as instances of other objects, and define constraints among objects that keep properties consistent. For advanced users and researchers, ORE allows demons to be defined on various object operations, slot inheritance to be controlled, and even entirely new constraint solvers to be written.

3.1 Introduction

This is the chapter for the Amulet object and constraint system, nicknamed ORE which stands for Object Registering and Encoding. This portion of the manual covers the basic operation and use of ORE and its facilities. The basic operation of ORE covers general use of objects and the kinds of values that can be stored in them. Also covered is how to make and use formulas that can be used to attach values together. At the end of this chapter, the means for writing new kinds of value types called wrapper types is covered.

ORE is used in Amulet as the means for representing all higher-level graphical concepts. Rectangles, for instance, are created using an exported object called Am_Rectangle. The process for moving a rectangle is also represented as an object. It is called Am_Move_Grow_Interactor. Lower-level graphical features like colors and fonts are not ORE objects in Amulet. Instead they are ``wrapper types'' or ``wrapper values,'' ``wrapper objects,'' or just plain ``wrappers.'' What makes wrappers different from regular C++ objects is that they contain data that derives from the class Am_Wrapper. This makes it easy to fetch and store them in ORE objects.

The coding style for ORE objects is declarative. That means the values and behaviors of objects are specified mostly at the time an object gets created by storing initial values and declaring constraints in the needed slots. All high level objects defined in Amulet are designed to be used in a declarative way. Normal programming practice consists of choosing the kinds of objects your program needs, finding out what slots these objects contain and what the semantics of the slots do, and finally assigning values to the slots and grouping the objects together to make the final application.

Many of the concepts and processes in ORE are derived from the Garnet object system called KR. KR differs from ORE in that it was originally designed to be a artificial intelligence knowledge representation framework. The graphics came later and KR underwent an evolution that made it more compatible with the demands of a graphical system. ORE begins where KR left off. In ORE, some KR features were abandoned like multiple inheritance. Many of the good KR features that only made it into KR in the last couple releases have been put into ORE right from the start. These features include dynamic type checking and an efficient algorithm for formula propagation and evaluation. And, of course, there are many brand new features in ORE that were never part of KR. Things like the owner-part hierarchy and the ability to install multiple constraint solvers which are hoped to become very useful to Amulet programmers.

ORE features like defining new wrapper types and writing a new constraint solver are quite advanced and are covered in Section 3.11 of this chapter. These sorts of features are not necessary for the novice Amulet programmer to make working applications, but are intended to be used by system programmers or researchers that want to extend Amulet.

3.2 Include Files

The various objects, types and procedures described in this chapter are spread out though several .h files. Typically, one will include amulet.h in the code which automatically includes all the files needed. This chapter tells where various things are defined so one can look up their exact definitions. The main include files relevant to ORE are:

For more information on Amulet include files, and how you should use them in your program, see Section 1.6 in the Overview chapter.

3.3 Objects and Slots

The Amulet object system, ORE, supports a ``prototype-instance'' object system. Essentially, an object is a collector for data in the form of ``slots.'' Each slot in an object is similar to a field in a structure. Each slot stores a single piece of data for the object. A Am_Rectangle object, for example, has a separate slot for its left, top, width, and height.

An ORE object is different from a C++ object in many ways. The slots of ORE objects are dynamic. A program can add and remove slots as required by the given situation. Whole new types of objects can be created on demand without requiring anything to be recompiled. In C++, only the object's data can be modified and not its structure without recompiling. Furthermore in ORE, the types stored into each slot can change. For instance, the Am_VALUE slot can hold an integer at one time, and then a string later. ORE keeps track of the current type stored in the slot, and supports full integration with the C++ type system, including dynamic type checking.

3.3.1 Get and Set

The basic operations performed on slots are Get and Set. A slot is essentially a key-value pair. Get takes a slot key and returns the slot's value. Set takes a key and value and replaces the slot's previous value with the one given. Creating new slots is done by performing Set using a key that has not been used before in that object. Another way to think of an object is as a name space for slot keys. A single key can have only one value in a given object.

my_object.Set (Am_LEFT, 5); // Set left slot to value 5
int position = my_object.Get (Am_TOP); // Get the value of slot Am_TOP
my_object.Set (Am_ANGLE1, 45.3f); // Set the angle1 slot to float 45.3
Calling Get on a slot which does not exist raises an error. Make sure slots are initialized with values to avoid this problem. To test whether a slot exists yet, use the Get_Slot_Type method on objects (see Section 3.3.3), or else use the Am_Value form of Get (see Section 3.3.9).

For convenience, a special Get_Object method is available to fetch slots that are known to store a Am_Object value. This is useful for chaining together a series of Gets onto a single line without having to store into a variable or use an explicit cast. It is an error to use this method on a slot which does not store an object.

int i = object.Get_Object (OBJECT_SLOT).Get_Owner ().
     Get_Part (PART_SLOT).Get (Am_LEFT);

3.3.2 Slot Keys

A slot key in ORE is an unsigned integer. An example of a slot key is Am_LEFT. Most slot keys are defined by the Am_Standard_Slot_Keys enumeration in the file standard_slots.h. Am_LEFT turns out to be the integer 100, but one uses the name Am_LEFT because it is more descriptive. The Am_LEFT slot is used in all the graphical objects like rectangles, circles, and windows and it represents the leftmost position of that object. Potentially, the slot key 100 could be used in another object with semantics completely different from those used in graphical objects, in essence 100 could be a key besides Am_LEFT. However, ORE provides mechanisms to avoid this kind of inconsistency and makes certain that integers and slot names map one to one. The string names associated with slots are mainly used for debugging. For example, they are printed out by the inspector. The string names for slots are not used during normal program execution.

Programmers can define new slot keys for their own use by using functions defined in standard_slots.h. There are four essential functions to do this: Am_Register_Slot_Name, Am_Register_Slot_Key, Am_Get_Slot_Name, and Am_Slot_Name_Exists.

Am_Register_Slot_Name is the major function for defining new slot keys. The function returns a key which is guaranteed not to conflict with any other key chosen by this function (it is actually just a simple counter). The return value is normally stored in a global variable which is used throughout the application code. If the string name passed already has a key associated with it, Am_Register_Slot_Name will return the old key rather than allocating a new one. Thus, Am_Register_Slot_Name can also be used to look up a name to find out its current key assignment.

Am_Slot_Key MY_FOO_SLOT = Am_Register_Slot_Name ("My Foo Slot");
We recommend that programmers define their slots this way, as shown in the various example programs.

Am_Register_Slot_Key is for directly pairing a number with a name. This is useful for times when one does not want to use a global variable to store the number returned by Am_Register_Slot_Name. The number and name chosen must be known beforehand not to conflict with any other slot key chosen in the system. The range of numbers that programmers are allowed to use for their own slot keys is 10000 to 29999. Numbers outside that range are allocated for use by Amulet. The number of new slot keys needed by an application is likely to be small so running out of numbers is not likely to be a problem. The main concern will be conflicting with numbers chosen by other applications written in Amulet.

  #define MY_BAR_SLOT 10500
  Am_Register_Slot_Key (MY_BAR_SLOT, "My Bar Slot");
The functions Am_Get_Slot_Name and Am_Slot_Name_Exists are used for testing the library of all slot keys for matches. This is especially useful when generating keys dynamically from user request.

const char* name = Am_Get_Slot_Name (MY_BAR_SLOT);
cout << "Slot " << MY_BAR_SLOT << " is named " << name << endl;

if (Am_Slot_Name_Exists (name)) cout << "Slot already exists\n";

3.3.3 Value Types

The value of Am_LEFT in a graphical object is an integer specifying a pixel location. Hence slot values have types, specifically the Am_LEFT slot has integer type in graphical objects. The type of a slot's value is determined by whatever value is stored in the slot. A slot can potentially have different types of values at different times depending on how the slot is used, but a given value has only one type so that a slot has only one type at a time. Thus, slots are ``dynamically typed'' like variables in Lisp.

The types supported in ORE are the majority of the simple C++ types including integer, float, double, character, and boolean. Also supported are some more high-level types like strings, ORE objects, a function type, and void pointers. Although void* can be used to store any type of object, ORE supports a type called Am_Wrapper which is used to encapsulate C++ classes and structures so that general C++ data can be stored in slots while still maintaining a degree of type checking.

my_object.Set (Am_LEFT, 50);
Am_Value_Type type = my_object.Get_Slot_Type (Am_LEFT);
// slot_type == Am_INT
my_object.Set (Am_FILL_STYLE, Am_Blue);
Am_Value_Type type = my_object.Get_Slot_Type (Am_FILL_STYLE);
// type == Am_Style
A Am_Value_Type is an unsigned short with two bit-fields. The lower 12 bits are the type base. These bits are used to distinguish individual members of a type. The upper 4 bits are the type class. Currently, there are four kinds of classes, the basic types, Am_WRAPPER, Am_METHOD, and Am_CONSTRAINT. The basic types include C++ types like Am_INT and Am_FLOAT. The Am_WRAPPER class is used to denote wrappers like Am_Object and Am_Style. Am_Method denotes types that are methods like Am_Object_Method and Am_Where_Method. Am_CONSTRAINT types are usually not stored in slots. Testing the class of a value type is performed using the macro, Am_Type_Class, which will strip off the type's base bits so that the class can be compared. A similar macro, Am_Type_Base will strip off the class bits so that the base can be compared.

Am_Value_Type type = object.Get_Slot_Type (Am_FILL_STYLE); // A Am_Style.
Am_Value_Type type_class = Am_Type_Class (type);
// type_class == Am_WRAPPER

3.3.4 The Basic Types

As shown by the examples above, the Set and Get operators are overloaded so that the normal built-in C++ primitive types can be readily used in Amulet. This section discusses some details of the primitive types, and the next few sections discuss some specialized types.

Usually, the C++ compilers can tell the appropriate types of slots from the various declarations. Thus, the compiler will correctly figure out which Set to use for each of the following:

my_object.Set (Am_LEFT, 50); //uses int
my_object.Set (Am_TEXT, "Foo"); //uses Am_STRING
my_object.Set (Am_PERCENT_VISIBLE, 0.75); //uses Am_FLOAT
long lng = 600000
my_object.Set (Am_VALUE_1, lng);
However, in some cases, the compiler cannot tell which version to use. In these cases, the programmer must put in an explicit type cast:

 //without cast, compiler doesn't know whether to use bool, int, void*, ...
if((bool)my_object.Get (Am_VISIBLE)) ...
 //without cast, compiler doesn't know whether to use int, long or float
int i = 5 + (int)my_object.Get(Am_LEFT);
Am_INT is the same as Am_LONG on Unix, Windows 95, and NT (32 bits), but on the Macintosh, an Am_INT is only 16 bits, so one must be careful to use long whenever the value might overflow 16 bits when one wants to have portable code.

The Am_Ptr type (defined in types.h) should be used wherever one would normally use a void* pointer, because Visual C++ cannot differentiate void* from some more specific pointers used by Amulet. Am_Ptr is defined as void* in Unix and unsigned char* in Windows.

3.3.5 Bools

Amulet makes extensive use of the bool type supplied by some C++ compilers (like gcc). For compilers that do not support it (Visual C++, ObjectCenter, etc.), Amulet defines bool as int and defines true as 1 and false as 0, so a programmer can still use bool in one's code. When bools are supported by the compiler, Amulet knows how to return a bool from any kind of slot value. For example, if a slot contains a string and it is cast into a bool, it will return true if there is a string and false if the string is null. However, for compilers that do not support bool, conversion to an int is not provided, so counting on this conversion is a bad idea. Instead, it would be better to get the value into a Am_Value type and test that for Valid (see Section 3.3.9).

3.3.6 The Am_String Class

The Am_String type (defined in object.h) allows simple, null terminated C++ strings (char*) to be conveniently stored and retrieved from slots. It is implemented as a form of wrapper (see Section 3.3.7). An Am_String can be created directly from a char* type, likewise it can be compared directly against a char*. Because Am_String is a Am_Wrapper which is a reference counted structure, the programmer need not worry about the string's memory being deallocated in a local variable even if an object slot that holds a pointer to the same string gets destroyed.

The Am_String class will make a copy of the string if the programmer wants to modify its contents. The Am_String class does not allow the programmer to perform destructive modification on the string's contents.

Listed below are the basic methods defined for Am_String:

Am_String ()
Am_String (const char* initial)
The constructor that takes no parameters essentially creates a NULL char pointer. It is not equivalent to the string ``''. The second constructor creates the Am_String object with a C string as its value. The C string must be '\0' terminated so as to be usable with the standard string functions like strcpy and strcmp. The Am_String object will allocate memory to store its own copy of the string data.

operator const char* ()
operator char* ()
These casting operators make it easy to convert a Am_String to the more manipulable char* format. When a programmer casts to const char*, the string cannot be modified so no new memory needs to be allocated. When the programmer casts to char*, however, the copy of the string stored in object slots are protected by making a local copy that can be modified. The modified string can be set back to an object slot by calling Set.

3.3.7 Using Wrapper Types

Although one could store C++ objects into ORE slots as a void*, ORE provides the Am_Wrapper type to ``wrap'' C++ objects. Am_Wrappers provide dynamic type checking and memory management to the objects. These wrapper objects add a degree of safety to slots without sacrificing the dynamic aspects. Making new wrapper types is discussed in Section 3.11.2 and requires some practice. On the other hand, using wrapper types is simple. Notable wrapper types in Amulet are Am_Style, Am_Font, Am_String, Am_Value_List (see Section 3.8), and especially Am_Object, itself. Getting and setting a wrapper is syntactically identical to getting and setting an integer.

Am_Style blue (0.0, 0.0, 1.0); // Am_Style is a wrapper type.
my_object.Set (Am_FILL_STYLE, blue); // Using a wrapper with Set.
Am_Style color = my_object.Get (Am_FILL_STYLE); // Using a wrapper with Get.
A wrapper's slot type is available for testing purposes. Common wrapper types have a constant slot type such as Am_OBJECT and Am_STRING. Other wrapper types can be found using the class' static Type_ID method.

if (object.Get_Slot_Type (MY_SLOT) == Am_Style::Type_ID ())
  Am_Style color = object.Get (MY_SLOT);

3.3.7.1 Standard Wrapper Methods

Amulet wrappers provide a number of useful methods for querying about their state and for testing whether a given Am_Wrapper* belongs to a given class. These methods are common across all wrapper objects that Amulet provides. The methods are also available when programmers build their own wrapper objects using the standard macros.

The first thing that all built-in wrappers have is not a method but a special NULL object. The name of the NULL object is Am_No_Typename where typename is replaced by the actual name for the type. Examples are Am_No_Font, Am_No_Object, Am_No_Value, and Am_No_Style. All of the NULL wrapper types are essentially equivalent to a NULL pointer. To test whether a wrapper is NULL or not one uses the method Valid(). If a wrapper is not valid, then it should not be used to perform operations.

// Here the code checks to see that my_obj is not a NULL pointer by using
// the Valid method.
Am_Object my_obj = other_obj.Get (MY_OBJ);
if (my_obj.Valid ()) {
  my_obj.Set (OTHER_SLOT, 6);
}
Besides using the Type_ID method, a programmer can check the type of a value using the static Test method. Test takes a Am_Value as its parameter and returns a bool.

// Here the Test method is used to test which kind of wrapper type the
// value holds.
Am_Value_List my_list;
Am_Object my_object;
Am_Value val = obj.Get (MY_VALUE); // Am_Value is discussed later
if (Am_Value_List::Test (val))
  my_list = val;
else if (Am_Object::Test (val))
  my_object = val;

3.3.8 Storing Methods in Slots

ORE treats methods (procedures) stored in slots exactly the same as data. Thus, method slots can be dynamically stored, retrieved, queried and inherited like all other slots. Method types are dynamically stored just as wrapper types. Macros are provided to make defining and using methods easier.

First, to define a method whose type already exists, one uses the Am_Define_Method macro. In the following example, an Am_Object_Method is defined. An object method has a void type and takes a single Am_Object as a parameter. Other method types can have different signatures.

Am_Define_Method (Am_Object_Method, void, my_method,(Am_Object self))
{
  self.Set (A_SLOT, 0);
}
By using the macro, the compiler can check to make sure that the actual method signature matches the one defined in the type. To set the value into a slot and retrieve the typename value, one uses Set and Get in the usual way.

object.Set (SOME_SLOT, my_method);
Am_Object_Method hold_method = object.Get (SOME_SLOT);
To call a method, one invokes the Call field of the method's class. For instance, an Am_Object_Method has a Call field that is a procedure pointer that returns void and takes an Am_Object parameter.

Am_Object_Method my_method = object.Get (SOME_METHOD);
my_method.Call (some_object);
To check the type of method one can use its type's static *_ID method. The ID methods are named using the method's name so an Am_Object_Method's ID is named Am_Object_Method_ID.

if (object.Get_Slot_Type (MY_SLOT) ==
    Am_Object_Method::Type_ID ()) {
  Am_Object_Method method = object.Get (MY_SLOT);
  method.Call (some_object);
}
Like common wrapper types, method wrappers also have a static Test method.

Am_Value value = object.Get (MY_SLOT);
if (Am_Object_Method::Test (value))
  Am_Object_Method method = value;
To define other method types, use the macros Am_Define_Method_Type and Am_Define_Method_Type_Impl. The first macro declares the type of the method. It is normally put into a .h file so that other parts of the code can use it. The IMPL macro is used to store the ID number of the method type. It must be stored in a .cc file to be compiled together with the program. In this example, a method is defined that takes two integers and returns a boolean.

// In the .h file
Am_Define_Method_Type (My_Method, bool, (int, int));

// In the .cc file
Am_Define_Method_Type_Impl (My_Method);
With these defined, a programmer can create methods of type My_Method and they will behave as all other ORE methods.

Am_Define_Method (My_Method, bool, equals, (int param1, int param2))
{
  return param1 == param2;
}

object.Set (EQUALS_SLOT, equals);
The procedure stored in the global method declaration can be used directly by calling its Call field. For example, using the equals method defined above, one can call:

bool result = equals.Call (5, 12);

3.3.9 Using Am_Value To Get A Slot Without Errors

Most of the time a programmer knows precisely what sort of value is stored in a slot. For these situations, the most convenient form of Get is the one that returns the value directly. This form has the declaration:

const Am_Value& Get (Am_Slot_Key key) const;
Am_Value is a union for all the ORE types. The Am_Value type can be coerced to all the standard Amulet types including wrappers. Normally, the programmer simply sets the return value directly into the final destination variable. But there are times when the programmer will want to call Get on a slot but does not know what type the slot contains (or whether the slot even exists). To determine the type, one can either query the type of the slot using the Get_Slot_Type method for objects, or the programmer can use the other form of Get that ORE provides. This form takes a Am_Value& as parameter instead of returning one. It has the declaration:

void Get (Am_Slot_Key key, Am_Value& value) const;
It is always valid to use the parameter style of Get. The return value form of Get will generate an error if the slot does not exist or is uninitialized. The paramter style generates an error only if it is called on an invalid or destroyed object. If the slot does not exist, the parameter form will set the Am_Value with type Am_NONE. If the slot is not initialized, then the type will be Am_UNINIT. (Uninitialized values concern slots with formulas [see Section 3.7].)

  int i_value; float f_value;
  Am_Value value;
  my_object.Get (SOME_SLOT, value);  // Get the value regardless of type
  if (value.type == Am_INT)          // The type field contains the type of value retrieved
    i_value = value;                 // Am_Value defines many casting operators
  else if (value.type == Am_FLOAT)   //  as assignment and constructors to aid
    f_value = value;                 //  setting and retrieving the value from
                                     //  the Am_Value
The Am_Value type has a number of methods, including printing (<<), ==, !=, Exists, and Valid. Exists and Valid are used to check the contents of the Am_Value. Exists returns true only if the type is not Am_NONE or Am_UNINIT which would happen if the slot has no value or is uninitialized. Valid returns true if the value exists (as in Exists) and if the value is not zero as well:

Am_Value value;
my_object.Get (SOME_SLOT, value); // Get the value regardless of type
if (value.Exists()) {
  // then it is safe to use value, but value could still be zero
   ...
}
my_object.Get (SOME_SLOT, value); // This time we know slot must be an Am_Object
                                  // or uninitialized.
if (value.Valid ()) { // Checks both existing and value != 0
  // safe to use value.
   ...
}

3.4 Inheritance: Creating Objects

The inheritance style of ORE objects is prototype-instance (as opposed to C++ which is class-instance). A prototype-instance object model means that objects are used directly as the prototypes of other objects. There is no distinction between instances and classes; in essence, there are only instances. Specialization of sub-objects into new types is performed by adding slots to the sub-object or changing the contents of existing slots defined by the prototype.

Here is an example of creating an ORE object and setting some of its slots:

  Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
     .Set (Am_LEFT, 10)
     .Set (Am_TOP, 20)
     .Set (Am_WIDTH, 100)
     .Set (Am_HEIGHT, 150)
     ;
A major style convention in ORE is to write an object all in one expression. This is so that the programmer need not repeat the name of the object variable over and over. This works because Set returns the original object. The main components of the creation action involves:

Although this manual uses the one expression convention for brevity and to familiarize programmers with its use, it would be just as correct to write out each individual Create and Set call on its own line.

  Am_Object my_rectangle;
  my_rectangle = Am_Rectangle.Create ("my rect");
  my_rectangle.Set (Am_LEFT, 10);
  my_rectangle.Set (Am_TOP, 20);
  my_rectangle.Set (Am_WIDTH, 100);
  my_rectangle.Set (Am_HEIGHT, 150);
Objects inherit all the slots of their prototype that are not set locally. Thus, if the Am_Rectangle object defines a color slot called Am_LINE_STYLE with a value of Am_Black, then my_rectangle will also have a Am_LINE_STYLE slot with the same value. If a slot is inherited, it will change value if the prototype's value changes. Thus, if Am_LINE_STYLE of Am_Rectangle is set to Am_Blue, then my_rectangle's Am_LINE_STYLE will also change. However, the Am_LEFT of my_rectangle will not change if the Am_LEFT of Am_Rectangle is set because my_rectangle sets a local value for that slot. See Section 3.11.5 for a discussion about how a programmer can control the inheritance of slots.

The inheritance of Amulet objects' print names is dealt with slightly differently. Objects created with a name parameter to the Create call will keep that name. Objects created without a string parameter will get the name of their prototype plus a number appended at the end to distinguish it from the prototype.

The root of the inheritance tree is Am_Root_Object. Programmers will typically create instances of the pre-defined objects exported by the various Amulet files (as shown in the examples in this manual), but Am_Root_Object is useful if one defines application-specific objects that do not concern the Amulet toolkit per se.

The Copy method can also be used to make new objects. Instead of being an instance of the prototype object, a copied object will become a sibling of the object. Every slot in the prototype is copied in the same manner as in the original. If a slot is local in the original, it will be local in the copy likewise if the slot is inherited, the copy will also be inherited.

Other useful methods relevant to inheritance include:

3.5 Destroying Objects

Objects are wrapper types which means that there is an internal reference counter within them that counts how many times the object is used. For objects which do not contain a reference to themselves, this means that simply eliminating all references to the object will destroy it. Am_Command objects (see the Interactor chapter, Section 5.6) are like this. A command object never stores a reference to itself so when all variables that refer to the command object are reset or destroyed, the command object will go away. Unfortunately, this kind of automatic deallocation is not guaranteed. Objects are containers that can hold arbitrary wrapper objects including references to itself. Any circularity will defeat the reference counting scheme; hence, most objects have to be explicitly destroyed.

To destroy an object, call the method Destroy. This method cleans out the contents of the objects making it have no slots, parts, or instances. Some operations, like Get_Name, will still work on a destroyed object but most methods will generate an error. Destroy will also destroy the object's instances and parts (parts are described in the next section). If one wants to preserve any parts of a destroyed object, one must be certain to call Remove_From_Owner or Remove_Part before the call to Destroy to salvage them. Note that objects that are not parts but simply stored in a slot will not be destroyed if the slot is destroyed. The vast majority of objects defined by Amulet require the programmer to call Destroy in order to free their memory. Forgetting to call Destroy is the most likely source of memory leaks.

3.6 Parts

In ORE, it is possible to make objects become part of another object. The subordinate object is called a ``part'' of the containing object which is called the ``owner.'' The part-owner relationship is used heavily in Amulet programs.

The Opal level of Amulet defines the Am_Window and Am_Group objects which are designed to hold graphical parts (rectangles, circles, text, etc.). Thus, if you want to make a composite of graphical objects, they should be added as part of a window or group. Non-graphical objects can be made parts of any kind of object. Thus, an Interactor object can be a part of a rectangle, group, or any other object. Similarly, any kind of object that an application defines can be added as a part of any other object. For a graphical object, its ``owner'' will be a window or a group, but the owner of an interactor or application object can be any kind of object. Opal does not support adding graphical objects as parts of any other graphical objects (so that one cannot add a rectangle directly as a part of a circle; instead, one creates a group object and adds the rectangle and circle as parts of the group).

3.6.1 Parts Can Have Names

A very important distinction among parts is whether or not the part is named. A ``named'' part has a slot key. One can generate a key for a part the same way that they are generated for slots. When a part is named, it becomes possible to refer to it by that name in the method Get_Part (or by regular Get) and it also takes on other properties. If the part is unnamed, then the part cannot be accessed from the owner except through the part iterator (Section 3.9) or else by reading it from a list like the Am_GRAPHICAL_PARTS slot in group objects (described in the Opal chapter, Section 4.7.1).

In some ways, a named part of an object is like a slot that contains an object. A named part has a value and can have dependencies just like a slot. Parts are different from slots in that their type can only be Am_Object and that any particular object can only be assigned as a part to only one owner. Parts cannot be generated by a constraint and the inheritance mechanism for parts is not as sophisticated as that for slots.

New names for parts are defined the same way as new slot keys:

Am_Slot_Key MY_FOO_PART = Am_Register_Slot_Name ("MY_FOO_PART");
Am_Object my_obj = Am_Group.Create("My_Obj")
    .Add_Part(MY_FOO_PART, Am_Rectangle.Create("foo")); //named part

3.6.2 How Parts Behave With Regard To Create and Copy

When an instance is made of an object which has parts, then instances are made for each of the parts also. When an object is copied, copies are made for each part. This instancing and copying behavior can be overridden by specifying false in the Add_Part call when the part is added. Only unnamed parts can be set to be not inherited. Regular slots which contain objects will share the same object when the slot is instanced or copied.

Thus:

Am_Object my_obj = Am_Group.Create ("My_Obj")
  .Add_Part (MY_PART, Am_Rectangle.Create("foo")) // named part
  .Add_Part (Am_Roundtangle.Create ("bar") // unnamed part
  .Add_Part (Am_Circle.Create ("do not instance"), false)
       // unnamed part that isn't inherited
  .Set(Am_PARENT, other_object); //slot containing an object

Am_Object my_obj2 = my_obj.Create();
// my_obj2 now has a rectangle part called MY_PART which is an instance of foo.
// It also has a roundtangle part that is not named. It does not have a circle part,
// since that part was unnamed and specified not to be inherited by the false paramter
// given in the Add_Part call. The Am_PARENT slot of both my_obj and my_obj2 point
// to the same value, other_object.
When an object is copied using the Copy method, all parts are copied along with the object. Both named and unnamed parts are copied unless the part is specified not to be inherited. Uninherited parts are never copied. If a part has a string name (the string given in the Create call, not the slot name), the same name will be used in the copy but a number is appended to the end of the name to distinguish it from the original.

3.6.3 Other Operations on Parts

Other methods on objects relevant to parts are listed below.

For example:

Am_Slot_Key RECT = Am_Register_Slot_Name ("RECT");
Am_Object my_window = Am_Window.Create ("a window")
  .Add_Part (RECT, Am_Rectangle.Create ("a rectangle"))
  .Add_Part (Am_Line.Create ("a line"));

Am_Object rect = my_window.Get_Part (RECT);
my_window.Remove_Part (RECT);
As mentioned above, when one destroys an object, all of its parts are destroyed also. Removing a part does not destroy the part.

3.7 Formulas

Formulas are used to connect together the values of slots. With formulas, the programmer assigns the value of one slot to be dependent on the value of other slots. When the dependent slots change, the value of the formula will be recomputed and the formula's slot will take on the computed value.

This method of computing values from dependencies is often called constraint maintenance. ORE's mechanisms for constraint maintenance are actually more general (and complicated) than the formula constraint mentioned here. The full ORE constraint mechanism will be described in a later revision of this manual. The general mechanism allows more than one constraint system to be included in the system at the same time. The formula constraint is just one of many possible constraints that may be used in ORE. For example, Amulet currently also contains a ``Web'' constraint used to support multi-way interactions described in Section 3.11.3.

3.7.1 Formula Functions

An ORE formula consists of a C++ function that defines the dependencies of a slot and returns the value to set into the slot. The parameter list of a formula function is always the same two parameters. The first parameter, self, is an Am_Object which points to the object containing the slot. The second parameter, cc, is an Am_Constraint_Context&. The cc is an opaque handle (which means that its internal representation is not visible) to the state of the formula. It is used internally by the constraint system, but is not meant to be manipulated directly by the programmer. The cc parameter is used to distinguish the two forms of Get: one which just returns the value of the slot, discussed above, the other which returns the value and also sets up a dependency link. There are also constraint versions for most of the Get_XX functions like Get_Part and Get_Owner. The return value of the formula function is the same type that the slot will be when it takes on the returned value.

This example creates a new formula constraint. Since it uses no macros, it looks more complicated than needed.

// Example of a formula function. This formula returns a value for
// Am_LEFT which will center itself within its owner's dimensions.
static int my_left_formula_proc (Am_Constraint_Context& cc, Am_Object self)
{
  Am_Object owner = self.Get_Owner (cc);
  int owner_width = owner.Get (cc, Am_WIDTH);
  int my_width = self.Get (cc, Am_WIDTH);
  return (owner_width - my_width) / 2;
}
Am_Formula my_left_formula (my_left_formula_proc, "my_left_formula");
The above example uses no macros so it is clear where variables are defined, and which methods take the special cc parameter. The global variable my_left_formula is the actual constraint which one would use to set a slot. Because formula functions have such a generic format, the macro Am_Define_Formula is usually used to save writing. Likewise, using the cc parameter in Get is common enough that macros like GV are available that automatically add the cc parameter. Using macros, the function above would look like the function below. (This particular formula definition could easily be reduced to one line.)

// Example of a formula function. This formula returns a value for
// Am_LEFT which will center itself within its owner's dimensions.
Am_Define_Formula (int, my_left_formula) {
  Am_Object owner = self.GV_Owner ();
  int owner_width = owner.GV (Am_WIDTH);
  int my_width = self.GV (Am_WIDTH);
  return (owner_width - my_width) / 2;
}
There also exists a Set version of GV called SV, that is used in other kinds of constraints like, in particular for constraints with multiple outputs, like Am_Web constraints. Though it is possible to use SV in a formula, it is generally easier to return the desired value. None of the formulas defined in the sample code in this section use SV.

There are also macros for defining formulas that return many of the built-in wrapper types. For instance, the macro Am_Define_Style_Formula returns the type Am_Style. Actually, all wrapper formulas return the same type, Am_Wrapper*. Since it is confusing to look at a formula function that is supposed to return Am_Style or Am_Object and see that it returns Am_Wrapper*, these macros are available to make the code better reflect what is really intended. The various wrapper types are described in the Opal chapter:

Am_Define_Formula (type, formula_name) - General purpose: returns specified type.
Am_Define_No_Self_Formula (type, function_name) - General purpose: returns specified type. Used when the formula does not reference the special self variable, so compiler warnings are avoided.
Am_Define_Value_Formula (formula_name) - Return type is void. This formula has a Am_Value& parameter named value which the programmer uses to return a value. Used when the formula might return different types, described in Section 3.7.1.2.
Am_Define_Object_Formula (formula_name) - Return type is Am_Object.
Am_Define_String_Formula (formula_name) - Return type is Am_String.
Am_Define_Style_Formula (formula_name) - Return type is Am_Style.
Am_Define_Font_Formula (formula_name) - Return type is Am_Font.
Am_Define_Point_List_Formula (formula_name) - Return type is Am_Point_List.
Am_Define_Image_Formula (formula_name) - Return type is Am_Image_Array.
Am_Define_Value_List_Formula (formula_name) - Return type is Am_Value_List.
Am_Define_Cursor_Formula (formula_name) - Return type is Am_Cursor.

3.7.1.1 Declaring Formulas

In order to use a formula constraint outside of the file that defines it, C++ expects an external declaration. The formula procedure does not need to externally declared. In fact formula procedures are normally declared static so that they do not infringe on the C++ external namespace. The Am_Formula variable is the name that needs to be externally declared. For example:

extern Am_Formula my_formula;
The type of the formula is not important in this case. All formula procedures get put into an Am_Formula variable that remembers the type internally.

3.7.1.2 Formulas Returning Multiple Types

Some formulas do not return only one type of value. For these formulas, the programmer cannot define a single return type. To remedy this situation, the formula constraint system can use the Am_Value as return value or a parameter instead of using a normal return value.

One kind of value formula declaration has return value of void and it has one extra parameter of type Am_Value& whose name is ``value.'' The self and cc parameters are the same as in normal formulas and are used in the same way. The standard macro for declaring a multiple-type formula of this kind is Am_Define_Value_Formula. Note that this type of formula does not have return value. Instead, the value is returned by setting the Am_Value& value parameter.

Am_Define_Value_Formula (my_formula)
{
  if ((bool)self.GV (Am_SELECTED))
    value = 5;
  else
    value = Am_Blue;
}
In the above example, if the slot Am_SELECTED is true, the formula will return an integer value of 5. If it is false, it returns the color blue.

Here is an example that passes a value from a different slot and without checking what type it is:

Am_Define_Value_Formula (my_copy_formula) {
    other_obj.GVM (SOME_SLOT, value);							// value is what is returned from
							// this formula
}
To read a slot set with a value formula the programmer can use either the Am_Value form of Get or can call Get_Slot_Type.

One can also create a formula that returns a const Am_Value as a return type:

Am_Define_Formula (const Am_Value, my_formula)
{
  if ((bool)self.GV (Am_SELECTED)
    return 5;
  else
    return Am_Blue;
}

3.7.2 Using GV

When the Get method is used with a constraint context (or equivalently, when the GV macro is used), the constraint solver decides how the actual Get is performed. The formula constraint solver sets up a dependency to the slot being fetched. Whenever the fetched slot changes value, the formula will be notified by the system and will automatically update its value by calling the formula procedure. The formula:

Am_Define_Formula (int, my_left) {
  int owner_width = self.GV_Owner ().GV (Am_WIDTH);
  int my_width = self.GV (Am_WIDTH);
  return (owner_width - my_width) / 2;
}
defines three slots as dependencies: the object's Am_OWNER slot (the GV_Owner macro expands into a GV on the Am_OWNER slot), the owner's Am_WIDTH slot, and the object's Am_WIDTH slot. A formula changes dependencies when it calls GV on different slots. For instance, the above formula's dependency on the owner's width will change if the object ever gets moved to a new owner.

A programmer can use a regular Get without the constraint context in a formula function, but the slot fetched will not become a dependency. Forgetting to use GV instead of Get is a common mistake for Amulet programmers. If it ever seems that a formula is not updating when it is supposed to, check to make sure that GV is being used in the right places.

There exists a form of GV for all forms of Get but one, Get_Prototype. Since the prototype of an object is fixed and can never change, there is never a need to install a dependency to it. The full list of GV forms is:

3.7.3 Putting Formulas into Slots

To install a formula in a slot, one needs a Am_Formula variable. Normally, the Am_Formula variable will be defined using the standard macros in which case the programmer calls Set with the formula name to install it. If the programmer has defined the formula procedure without using the macros, then one then needs to create a formula object using that procedure. The formula object is then Set into a slot.

Am_Define_Formula (int, rect_left)
{
  return (int)self.GV_Owner ().GV (Am_WIDTH) / 2;
}

// Here the formula is used.
Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
   .Set (Am_LEFT, rect_left))
   ;
In this example, the programmer defines a Am_Formula variable explicitly:

static int rect_left_proc (Am_Object& self, Am_Constraint_Context& cc)
{
  return (int)self.GV_Owner ().GV (Am_WIDTH) / 2;
}

// Here the formula procedure is used.
Am_Object my_rectangle = Am_Rectangle.Create ("my rect")
  .Set (Am_LEFT, Am_Formula (rect_left_proc, "name of formula"))
  ;
Formulas are evaluated eagerly, which means that the formula expression may be evaluated even before all the slots it depends on are defined. Since a formula cannot detect when external references are changed, it is wise to make sure that any global variable that a formula references are defined before the formula is set into a slot. Formulas that reference undefined slots or NULL objects will return the value Am_UNINIT. It is an error to fetch the value of an uninitialized slot from outside a formula so it is important that anything that a formula will depend on becomes valid as soon as possible. One can check if a slot is uninitialized by using Get_Slot_Type or fetching the slot as a Am_Value.

// This formula will become uninitialized if
//  a) it doesn't have an owner.
//  b) the owner's left slot doesn't exist.
//  c) the owner's left slot is uninitialized.
Am_Define_Formula (int, my_form)
{
  return self.GV_Owner ().Get (Am_LEFT);
}

3.7.4 Slot Setting and Inheritance of Formulas

When a slot is set, any formula that was previously in that slot will be removed by default. Just like setting a slot with a new value removes the old value of the slot, setting a slot with a value or a new formula removes the old value that was there, even if the old value was a formula.

Like values, formulas are inherited from prototypes to their instances. However, the formula in the instance might compute a value different from the formula in the prototype if the formula contains indirect links. For example, the formula that computes the width of a text object depends on the text string and font, and even though the same formula is used in every text object, most will compute different values.

Sometimes, constraints from a prototype should be retained in instances even if the local value is set. This requires declaring that the slot is not Single_Constraint_Mode, which is an advanced feature covered in Section 3.11.5.

3.7.5 Calling a Formula Procedure From Within Another Formula

Given a Am_Formula variable it is possible to call the formula procedure embedded within it. This process is typically done within another formula since one needs to have both a self reference and a cc parameter. Typical use for this ability is to reuse the code of another formula instead of rewriting it. An Am_Formula's procedure call method returns type const Am_Value which can be cast to the desired return type.

Am_Define_Formula (int, complicated_formula)
{
  // Perform some hairy computation.
}

Am_Define_Fomula (int, complicated_formula_plus_one)
{
  return (int)complicated_formula (cc, self) + 1;
}

3.8 Lists

Lists are widely used in Amulet. For example, many widgets require a list of labels to be displayed. The standard ORE list implementation is the Am_Value_List type. ORE defines this list class so that it can be a wrapper and more easily be stored as slot values. The operations on Am_Value_Lists are provided in the file value_list.h. Like slots, Lists can hold any type of value. A single list can also contain many different types of values at the same time.

Because the Am_Value_List is a form of wrapper, it supports all the standard wrapper operations, including:

3.8.1 Current pointer in Lists

In addition to data, Am_Value_Lists contain a pointer to the ``current'' item. This pointer is manipulated using the following functions:

The standard way to iterate through all items in a list in forward order is:

for (my_list.Start (); !my_list.Last (); my_list.Next ()) {
  something = my_list.Get ();
  // Use something here
}
Similarly, to go in reverse order, one would use:

for (my_list.End (); !my_list.First (); my_list.Prev ()) {
  something = my_list.Get ();
  // Use something here
}
Note that the pointer is not initialized automatically in a list, so the programmer must always call Start or End on the list before accessing its value with Get.

Am_Value_Lists are circular lists. Prev() and Next() wrap around, but be aware that there is a NULL list item, which you cannot do a Get() on, between the Last and First elements in the list. To wrap around, you should do an extra Next() or Prev() at the end of the list:

my_list.Start();
while (true) { // endless circular loop
  do_something_with(my_list.Get());
  my_list.Next();
  if (my_list.Last()) my_list.Next(); // an extra Next at the end of the list
}

3.8.2 Adding items to lists

There are two mechanisms for adding items to a list one element at a time: either always at the beginning or end, or at the position of the current pointer. One can also append two lists together.

To add items at the beginning or end of the list, use the Add method. Since this does not use the current pointer, one do not need to call Start. The first parameter to Add is the value to be added, which can be any primitive type, an object, a wrapper, or a Am_Value. The second parameter to Add is either Am_TAIL or Am_HEAD which defaults to Am_TAIL. This controls which end the value goes. Add returns the original Am_Value_List so multiple Adds can be chained together:

Am_Value_List l;
l.Add(3)
 .Add(4.0)
 .Add(Am_Rectangle.Create())
 .Add(Am_Blue)
 ;
To add items at the current position, the programmer must first set the current pointer. First, Start, End, Next, and Prev are used to position the current pointer as the desired location then Insert is called. The first parameter of Insert is the value to be stored, and the second parameter specifies whether the new item should go Am_BEFORE or Am_AFTER the current item. There is no default for this parameter. The current pointer does not change in this operation.

Lists can be appended with the Append method. Append is called on the lists whose elements will be at the beginning of the final result. The parameter is a list whose elements will be appended. The result is stored directly in the list on which the method is called. This method returns this just as the Add method so that it can be cascaded.

Am_Value_List start = Am_Value_List ().Add (1).Add (2); // list (1, 2)
Am_Value_List end = Am_Value_List ().Add (3).Add (4); // list (3, 4)
start.Append (end); // start == (1, 2, 3, 4)

3.8.3 Other operations on Lists

The Get method retrieves the value at the current position. Like Am_Object's Get, it returns an Am_Value which can be cast into the appropriate type. Another Get is available that returns Am_Value as a parameter. For example:

Am_Value v;
for (my_list.End (); !my_list.First (); my_list.Prev ()) {
	my_list.Get(v);
	cout << "List item type is " << v.type << endl << flush;
}
To find the type of an item without fetching the value use Get_Type().

Set is used to change the current item in the list. This is differs from Insert in that it deletes the old current item and replaces it with the new value.

The Delete method destroys the item at the current position. It is an error to call Delete if there is no current element. The current pointer is shifted to the element previous to the deleted one. Make_Empty deletes all the items of the list.

Am_Value_Lists support a membership test, using the Member method. This starts from the current position, so be sure to set the pointer before calling Member. For example, to find the first instance of 3 in a list:

l.Start();
if (l.Member(3)) cout << "Found a 3";
Member leaves the current pointer at the position of the found item. Calling Set or Delete after a search will affect the found item. Calling Member again finds the next occurrence of the value.

Length returns the current length of the list.

Empty returns true if the list is empty. Note that there is a difference between being Valid and being Empty. Not being valid means that the list is NULL that is, it does not exist. An invalid list is also empty by definition, but an empty list is not always invalid. The list may exist and be made empty by deleting its last element, for instance. In this case, Empty would return true and Valid will also return true.

3.9 Iterators

For efficiency, ORE does not allocate an Am_Value_List for some types of list-like information, and instead supplies an ``iterator.'' An iterator object contains a current pointer and allows the programmer to examine the elements one at a time. There are three kinds of iterators available in ORE: one for slots, one for instances, and another for parts. Each of the iterators has the same basic form, and the interface is essentially the same as for Am_Value_Lists.

3.9.1 Reading Iterator Contents

The iterator methods treat the list of items like a linked list rather than as an array. The main operations are Start, Next, and Get. Start places the iterator at the first element. Next moves the iterator to the next element. And Get returns the current element. To initialize the list, assign the iterator with the object that contains the information that the programmer want to iterate upon. For example:

  cout << "The instances of " << my_object << " are:" << endl;
  Am_Instance_Iterator iter = my_object;
  for (iter.Start (); !iter.Last (); iter.Next ())  {
    Am_Object instance = iter.Get ();
    cout << instance << endl;
  }
The first line of the example is used to initialize the iterator. The example prints out the instances of my_object so my_object is the object to assign to the iterator. The Last method is used to detect when the list is complete. These iterators can only be traversed in one direction.

3.9.2 Types of Iterators

The Am_Part_Iterator iterates over the parts of an object. Its Get method returns an Am_Object which is the part. To list the parts stored in Am_Groups and Am_Windows, however, it is better to use the Am_Value_List stored in the Am_GRAPHICAL_PARTS slot instead of the part iterator. The part iterator would list all parts in the object including many that are not graphical objects.

To iterate over the instances of an object, use an Am_Instance_Iterator. Its Get method returns an Am_Object which is the part. To list an object's slots (both inherited and local), use the Am_Slot_Iterator. Its Get method returns the Am_Slot_Key of the current slot. You can use the object method Is_Slot_Inherited to see if the slot is inherited or not.

3.9.3 The Order of Iterator Items

When an iterator is first initialized, there is no particular order imposed on the list. The order of the elements will be such that a single element will not repeat if the list is read from beginning to end, but the order may change if the iterator is restarted from the beginning. (Opal keeps track of the Z order [stacking or covering order] of the parts by using the Am_GRAPHICAL_PARTS slot which contains an Am_Value_List of the graphical parts, sorted correctly).

When items are added to an iterator's list while the list is being searched, the items added are not guaranteed to be seen by the iterator. The iterator may have already skipped them. The value returned by the Length method will be correct, but the only way to make certain all values have been seen is to restart the iterator.

Likewise, the order in which the elements are stored in each iterator is not guaranteed to be maintained when an item is deleted from the list. The iterators themselves cannot be used to destroy an item, but other methods like obj.Destroy, obj.Remove_Part, and obj.Remove_Slot will affect the contents of iterators that hold those values.

When an iterator has a slot or object as its current position, and that item gets removed, the affect on the iterator is not determined. An iterator can be restarted by calling the Start method in which case it will operate as expected. Though an iterator will not likely cause a crash if its current item is deleted, continued use of it could cause odd results.

For iterators that iterate over objects (specifically Am_Part_Iterator and Am_Instance_Iterator), it is possible to continue using the iterator even when items are deleted. If the programmer makes certain that the iterator does not have the deleted object as the current position when the object is removed, then the iterator will remain valid. For example:

// Example: Remove all parts of my_object that are instances of Am_Line.
Am_Part_Iterator iter = my_object;
iter.Start ();
while (!iter.Last ()) {
  Am_Object test_obj = iter.Get ();
  iter.Next ();
  if (test_obj.Is_Instance_Of (Am_Line))
    test_obj.Remove_From_Owner ();
}
In the above example, the call to Next occurs before the call to Remove_From_Owner. If these method calls were reversed, then iterator would go into an odd state and one would get undetermined results.

The Am_Slot_Iterator type does not have the same deletion properties as the object iterators. If a slot gets removed from an object used by a slot iterator (or the prototype of the object assuming the slot is defined there), then the affect on the iterator is undetermined. The slot iterator must be restarted whenever a slot gets added or removed from the list in order to guarantee that all slots are seen.

3.10 Errors

Whenever Amulet notices an error, it calls the Am_Error routine which prints out the error and then aborts the program. If you have a debugger running, it should cause the program to enter the debugger.

3.11 Advanced Features of the Object System

3.11.1 Destructive Modification of Wrapper Values

Some wrappers, like Am_Style's, are immutable, which means that once created, the programmer cannot change their values. Other wrapper objects, like Am_Value_Lists are mutable. The default Amulet interface copies the wrapper every time it is used and automatically destroys the copies when the programmer is finished with them (explained in Section 3.11.2.2). This design prevents the programmer from accidentally changing a wrapper value that is stored in multiple places, and it helps prevent memory leaks. However, for programmers that understand how Amulet manages memory, it is unnecessarily wasteful since making copies of wrappers is not always required. This section discusses how you can modify a wrapper value without making a copy. See also the discussion of the wrapper implementation in Section 3.11.2.

When the programmer retrieves a wrapper value out of a slot, it points to the same value that is in the slot. If the value of the wrapper is changed destructively, then both the local pointer and the pointer in the slot will point to the changed value. To control this, most mutable wrappers provide a make_unique optional parameter in their data changing operations. The default for this parameter is true and makes the method create a copy of the value before it modifies it. If you call the procedure with false, then it will not make a copy and the value you modify will be the same as the value pointed to by all the other references. If the wrapper is pointed to in a slot, the object system will not know that the value has changed. To tell the object system that a slot has changed destructively, the programmer calls Note_Changed on the object after the modifications are complete. ORE responds to Note_Changed the same way it would respond if the slot were set with a new value. Thus, Amulet will redraw the object if necessary and notify any slots with a constraint dependent on this slot. For example:

obj.Make_Unique (MY_LIST_SLOT); // make sure that only one value is modified
Am_Value_List list = obj.Get (MY_LIST_SLOT);
list.Start ();
list.Delete (false); // destructively delete the first item
obj.Note_Changed (MY_LIST_SLOT); // tell Amulet that I modified the slot
The purpose of the Make_Unique call at the beginning of the previous segment concerns whether the wrapper value is unique to the slot MY_LIST_SLOT or not. Amulet shares wrappers between multiple slots that are not explicitly made unique. A major source of sharing occurs when an object is instantiated. For example, if one makes an instance of obj in the example above called obj_instance, then both obj_instance and obj will point to the same list in the MY_LIST_SLOT. In that case, the above code would modify the list for both obj and obj_instance, but Amulet would not know about the change to obj_instance. Thus, to make sure the MY_LIST_SLOT is unique, the programmer explicitly calls Make_Unique before the value of the slot is fetched. If it is beyond doubt that a wrapper value is not shared or that it is only shared in places which the programmer knows will be affected, then the Make_Unique call can be eliminated. Make_Unique will do nothing if the slot is already unique.

It is important that Make_Unique be called before one actually Get's the value. By storing the wrapper in a local variable, the wrapper will not be unique and Make_Unique will make a unique copy for the slot. If the programmer would like to examine the contents of a slot before deciding whether to perform a destructive change, then one can use the Is_Unique method. Is_Unique returns a bool that tells whether the slot's value is already unique. By storing the value from Is_Unique before calling Get, the programmer can decide whether to perform destructive modification or not.

// Example which uses Is_Unique to guarantee the uniqueness of MY_SLOT's  value. 
bool unique = obj.Is_Unique (MY_SLOT);
Am_Value_List list = obj.Get (MY_SLOT);
if (... list ...) {
  list.Add (5, Am_TAIL, !unique); // perform destructive change if unique
  if (unique)
    obj.Note_Changed (MY_SLOT);
  else
    obj.Set (MY_SLOT, list);
}

3.11.2 Writing a Wrapper Using Amulet's Wrapper Macros

You should consider creating a new type of wrapper whenever you need to store a C++ object into a slot of an Amulet object. A wrapper manages two things. First, it has a simple mechanism that supports dynamic type checking, so it is possible to check the type of a slot's value at run time. Second, wrappers use a reference counting scheme to prevent the value's memory from being deleted while the value is still in use.

Wrappers are created in two layers. The outermost layer is the C++ object layer used by programmers to refer to the object. The type Am_Style is the object layer for the Am_Style wrapper. Inside the object layer is the data layer. For Am_Style, the type is called Am_Style_Data. Normally, programmers are not permitted access to the data layer. The object layer of the wrapper is used to manipulate the data layer which is where the actual data for the wrapper is stored.

3.11.2.1 Creating the Wrapper Data Layer

Both the typing and reference counting is embodied by the definition of the class Am_Wrapper from which the data layer of all wrappers must be derived. The class Am_Wrapper is pure virtual with seven methods, six of which all wrappers must define. Most of the time, the programmer can use the pre-defined macros Am_WRAPPER_DATA_DECL and Am_WRAPPER_DATA_IMPL to define these six methods.

void Note_Reference ()
unsigned Ref_Count ()
Am_Wrapper* Make_Unique ()
void Release ()
operator== (Am_Wrapper& test_value)
Am_ID_Tag ID ()
Of the six methods, three are used for maintaining the reference count and making sure that only a unique wrapper value is ever modified. These are Note_Reference, Make_Unique, and Release. The seventh method is void Print_Name (ostream&) which is used to print out the value of the wrapper in a human-readable format. Unlike the other methods, this method has a default implementation though it may be defined by programmers. This method is used mostly for debugging as in the inspector or for printing out values in a property sheet. It is good to implement this method for wrappers that one intends to release for use by the public.

Note_Reference tells the wrapper object that the value is being referenced by another variable. The reference could be a slot or a local variable or anything else. The implementation of Note_Reference is normally to simply add one to the reference count. Release is the opposite of Note_Reference. It says that the variable that used to hold the value does not any longer. Typical implementation is to reduce the reference count by one. If the reference count reaches zero, then the memory should be deallocated. Ref_Count returns the value of the reference count so that external code can count how many of the total references it holds.

Make_Unique is the trickiest of these methods to understand. The basic idea is that a programmer should not be allowed to modify any wrapper value that is not unique. For example, if the programmer retrieves a Am_Value_List from a slot and adds an item to the list, this destructive modification should normally not affect the list that is still in the slot. The way to maintain this paradigm is for the method used to modify the wrapper's data to first call Make_Unique. If the reference count is one, the wrapper value is already unique and Make_Unique simply returns this. If the reference count is greater than one, then Make_Unique generates a new allocation for the value that is unique from the original and returns it. Either way only a unique wrapper value will be modified. Some wrapper types have boolean parameters on their destructive operations that turn off the behavior of Make_Unique to allow the programmer to do destructive modifications (see Section 3.11.1).

The operator== method allows the object system to compare two wrapper values against one another. The system will automatically compare the pointers so the == method must only compare the actual data. Simply returning false is sufficient for most wrappers.

The final operator is used to handle a primitive dynamic typing system. Each wrapper type is assigned a number called an Am_ID_Tag which is an unsigned integer. Integers are dispensed using the function Am_Get_Unique_ID_Tag. Normal procedure is to define a static member to the wrapper data class called id which gets initialized by calling Am_Get_Unique_ID_Tag. This function takes a name and a class ID number to generate an ID for the wrapper. ID tags and value types are one and the same concept. The wrapper ID is the same value that is stored as the wrapper's type.

All of the methods can be defined instantly by using the Am_WRAPPER_DATA_DECL and Am_WRAPPER_DATA_IMPL macros. The macros require that the user define at least two methods in the wrapper data class. The first required method is a constructor to be used by Make_Unique. The method is used to create a copy of the original value which can be modified without affecting the original. This can be done by making a constructor that takes a pointer to its own class as its parameter. Make sure that in all the data class constructors to initialize the refs member (defined by Am_WRAPPER_DECL) to 1. The second required method is an operator== to test equality. The == method does not need to check that the parameter is of the correct type because that is handled by the default implementation of the operator== that takes a Am_Wrapper& and which calls the specific == routine if the types are the same.

For example:

class Foo_Data : public Am_Wrapper {
    Am_WRAPPER_DATA_DECL (Foo)
   public:
    Foo_Data (Foo_Data* prev)
    {
      ... // initialize member values
      refs = 1; // Do not forget this line!
    }
    operator== (Foo_Data& test_value)
    {
      ... // compare test_value to this
    }
   protected:
    ... // define own members
  };

// typically this part goes in a .cc file
  Am_WRAPPER_DATA_IMPL (Foo, (this))
All the standard wrapper macros take the name of the type as their first parameter. The string ``_Data'' is always automatically appended to the name meaning your wrapper data classes must always end in _Data. If one wants the name of the wrapper type to be Foo, the data layer type must be named Foo_Data. The Am_WRAPPER_DATA_IMPL macro takes a second parameter which is the parameter signature to use when the Make_Unique method calls the data object's constructor. In the above case, ``(this)'' is used because the parameter to the Foo_Data constructor is equivalent to the this pointer in the Make_Unique method. That is, the Make_Unique method will sometimes have to create a new copy of the wrapper object. The new copy will be created using one of the object's constructors. In the above case, the programmer wants to use the constructor Foo_Data(Foo_Data* prev). This constructor requires Make_Unique to pass in its this pointer as the parameter. Therefore, the parameter signature declared in the macro Am_WRAPPER_DATA_IMPL is ``(this).'' If the programmer wanted a different constructor to be used, the parameter set put into the macro would be different.

3.11.2.2 Using The Wrapper Data Layer

The wrapper data layer is normally manipulated only by the methods in the wrapper outer layer. One can take the Am_Foo_Data* and manipulate it as a normal C++ object with the following caveat. One must be sure that the reference count is always correct. When one uses the data pointer directly, the methods Note_Reference or Release are not being called automatically so it must be done locally in the code.

Consider the following example: The programmer wants to return a Am_Foo type but currently has a Am_Foo_Data* stored in his data.

// Here we move a Foo_Data pointer from one variable to another. The
// object that lives in the first variable must be released and the
// reference of the object moved to the new variable must be incremented
Foo_Data* foo_data1;
Foo_Data* foo_data2;

//... assume that foo_data1 and foo_data2 are somehow initiallized with
// real values...

if (foo_data1)
  foo_data1->Release ();
foo_data1 = foo_data2;
foo_data1->Note_Reference ();
To keep changes in a wrapper type local, one must call the Make_Unique method on the data before making a change to it. If the wrapper designer wants to permit the wrapper user to make destructive modifications, a boolean parameter should be added to let the user decide which to do.

void Foo::Change_Data (bool destructive = false)
{
  if (!destructive)
    data = data->Make_Unique ();
  data->Modify_Somehow ();
}
Sometimes a programmer will want to use the wrapper data pointer outside the outer wrapper layer of the object. To convert from the wrapper layer to the data layer, one uses Narrow.

Foo my_foo;
Foo_Data* data = Foo_Data::Narrow (my_foo);
data->Use_Data ();
data->Release (); // Release data when through
The way to test if a given wrapper is the same type as a known wrapper class is to compare IDs. The static method TypeName_ID is provided in the standard macros.

Am_Wrapper* some_wrapper_ptr = something;
Am_ID_Tag id = some_wrapper_ptr->ID ();
if (id == Foo_Data::Foo_Data_ID ())
  cout << "This wrapper is foo!" << endl;
Other functions provided for wrapper data classes by the standard macro are Is_Unique, and Is_Zero with are boolean functions that query the state of the reference count.

3.11.2.3 Creating The Wrapper Outer Layer

The standard macros for building the wrapper outer layer assume that the class for the wrapper data is called TypeName_Data where TypeName is the name for the wrapper outer layer. Like the data layer macros, there are two outer layer macros, one for the class declaration part and one for the implementation.

// Building the outer layer of Foo. This definition normally goes in
// a .h file.
class Foo { // Note there is no subclassing.
  Am_WRAPPER_DECL (Foo)
 public:
  Foo (params);
  Use ();
  Modify ();
};

  // This normally goes in the .cc file.
  Am_WRAPPER_IMPL (Foo)
The wrapper outer layer is given a single member named data which is a pointer to the data layer. In all the wrapper methods, one performs operations on the data member.




// A Foo constructor - initializes the data member.
Foo::Foo (params)
{
  data = new Foo_Data (params);
}
For methods that modify the contents of the wrapper data, one must be sure that the data is unique. One uses the Make_Unique method to manage uniqueness.




Foo::Modify ()
{
  if (!data)
    Am_Error ("not initialized!");
  data = data->Make_Unique ();
  data->Modify ();
}
Methods that do not modify data do not need to force uniqueness so they can use the wrapper data directly.

Foo::Use ()
{
  if (!data)
    Am_Error ("not initialized!");
  data->Use ();
}
Somewhere in the code, the wrapper will actually do something: calculate an expression, draw a box, whatever. Whether the programmer puts the implementation in the data layer or the outer layer is not important. Most wrapper implementations will put define their function wherever it is most convenient.

Putting a wrapper around an existing C++ class is not difficult. One can make the original class a piece of data for the wrapper data layer. If the programmer does not want to reimplement all the methods that come with the existing class, one provides a single method that returns the original class and calls the methods on that. Be certain that Make_Unique is called before the existing object is returned. If the object can be destructively modified, then the wrapper must be made safe before the modifications occur. However, if the programmer wants the wrapper object to behave as if it were the original class then some reimplementation may be required.

3.11.3 The Am_Web Constraint

The Am_Web constraint is a multi-way constraint solver. That means it can store values to more than one slot at a time. On the other hand, the one-way Am_Formula constraint can only store values to one slot, the slot in which the formula is stored. Multi-way constraints tend to be more difficult to use and understand than one-way constraints, which is Am_Web is described here in the advanced section. One would use a web instead of a formula to build a constraint that involves tying several slots together so that their values act as a unit. Also, sometimes it is more efficient to make a single web where many formulas would be required to accomplish the same task. For example, Opal uses a web to tie together the left, top, width, height slots with the x1, y1, x2, y2 slots in the Am_Line object. Finally, the web provides information about which dependencies have been changed during re-validation which may be required to implement certain kinds of constraints.

A web constraint consists of three procedures as opposed to one procedure in a formula. The types of these procedures are Am_Web_Create_Proc, Am_Web_Initialize_Proc, and Am_Web_Validate_Proc. These procedures always use the same signature since there is no return type to worry about. The validation procedure is the most similar to the formula's procedure. It is executed everytime the web is reevaluated and it typically consists of the most code. The initialization procedure is run once when the web is first created. Its purpose is to set up the web's dependencies, especially the output dependencies. The create procedure concerns what happens when a slot containing a web constraint is copied or instantiated. Since a web can have multiple output dependencies, it does not really belong to any one slot. So, a slot is chosen by the programmer to be the ``primary slot.'' That slot is considered to be the web's owner and the location where a new web will be instantiated. The create procedure lets the programmer identify the primary slot.

3.11.3.1 The Validation Procedure

For the example, we will construct a multi-direction web constraint that ties together three slots: ADDEND1, ADDEND2, and SUM. SUM is constrained to be ADDEND1 + ADDEND2, and ADDEND1 is constrained to be SUM - ADDEND2. ADDEND2 is not constrained. First, we will write the validation procedure:

void sum_two_slots_val (Am_Constraint_Context& cc, Am_Web_Events& events)
{
  events.End ();
  if (events.First ()) // No events
    return;
  Am_Slot slot = events.Get ();
  Am_Object self = slot.Get_Owner ();
  switch (slot.Get_Key ()) {
  case ADDEND1: { // ADDEND1 has changed last.
      int addend1 = self.GV (ADDEND1);
      int addend2 = self.GV (ADDEND2);
      self.SV (SUM, addend1 + addend2);
    }
    break;
  case ADDEND2: { // ADDEND2 has changed last.
      int prev_value = events.Get_Prev_Value ();
      int addend2 = self.GV (ADDEND2);
      events.Prev ();
      if (events.First () || events.Get ().Get_Key () != SUM) {
        int addend1 = self.GV (ADDEND1);
        self.SV (SUM, addend1 + addend2);
      }
      else { // SUM was set before ADDEND2. Must propagate previous result to ADDEND1.
        int sum = self.Get (SUM);
        self.SV (ADDEND1, sum - prev_value);
        self.SV (SUM, sum - prev_value + addend2);
      }
    }
    break;
  case SUM: { // SUM has changed last.
      int sum = self.GV (SUM);
      int addend2 = self.GV (ADDEND2);
      self.SV (ADDEND1, sum - addend2);
    }
    break;
  }
}
If the code looks complicated, it is because it is complicated. What makes writing web constraints difficult is that more than one slot can change between validations. The code above has to first check to see what slots have changed. Then it sees if something else has changed before that and then finally carries out the computation. To read which slots have changes, one uses the Am_Web_Events class. This contains a list of the slots that have changed value since the last time the web was validated in the order in which they were changed. This class is structured like the other iterator classes. It has Start, End, Next, Prev, First, and Last methods for traversing its items. Its Get method returns a Am_Slot. There is no self parameter because webs are not attached to a specific slot or object. A web could hypothetically float around between objects as it gets reevaluated; though, this is generally not the case. To get a self pointer, one reads the owner of one of the slots in the event list. It should always be possible to determine a web's location using any one of its slots as a reference. The other method available in the events list is Get_Prev_Value which returns the value that the slot contained before it was changed. To get the slot's current value, one uses Get or GV.

Using SV in a web is similar to using GV except that it is used for output instead of input. Every slot set with SV will be assigned a constraint pointer to that web making the slot dependent on the web. It is legal to both GV and SV a single slot. In that case, the web would both be a constraint and a dependency on that slot. Both the constraint and dependency are kept whenever either are used in the validation procedure. Thus, in the above example, when GV is called on the SUM slot, the web's constraint on that slot will be kept. If neither GV or SV are called on a slot during validation, the dependency and/or constraint will be removed.

3.11.3.2 The Create and Initialization Procedures

The next procedure examined will be the create procedure. The semantics of the create procedure is that it returns true when it is passed the primary slot. For our example web, we have a choice of primary slots. It could be any of ADDEND1, ADDEND2, or SUM. We will choose SUM.

bool sum_two_slots_create (const Am_Slot& slot)
{
  return slot.Get_Key () == SUM;
}
Although our sum_two_slots web will normally be connected to three slots, when it is inherited, it is only connected to the primary slot. The purpose of the initialize procedure is to reconnect the web to all its other dependencies. Essentially, it is used to fix up the lost connections caused by inheritance.

void sum_two_slot_init (Am_Constraint_Context& /*cc*/, const Am_Slot& slot,
                        Am_Web_Init& init)
{
  Am_Object_Advanced self = slot.Get_Owner ();
  init.Note_Input (self, ADDEND1);
  init.Note_Input (self, ADDEND2);
  init.Note_Input (self, SUM);
  init.Note_Output (self, ADDEND1);
  init.Note_Output (self, ADDEND2);
  init.Note_Output (self, SUM);
}
The Am_Web_Init class is used to make dependency and constraint connections without performing a GV or SV on any slot. The method Note_Input creates a dependency and the method Note_Output creates a constraint. Some programmers may want to perform computation during initialization, in which case the usual cc parameter is available for calling GV or SV. The slot parameter provides the primary slot to which the web was attached.

3.11.3.3 Installing Into a Slot

A web is put into an object by setting it into its primary slot. First, a Am_Web variable is created using the three procedures defined above. This variable is stored into the primary slot just as a Am_Formula is stored, by calling Set.

Am_Web my_web (sum_two_slots_create, sum_two_slots_init,
               sum_two_slots_val);
object.Set (SUM, my_web);
Once a web is stored in the primary slot, its initialization procedure will take over and attach the web to any other slots it needs.

3.11.4 Using Am_Object_Advanced

There are several extra methods that can be used on any Amulet object that are not available in the regular Am_Object class. A programmer can manipulate these methods by typecasting a regular Am_Object into a Am_Object_Advanced class. For instance, in order to retrieve the Am_Slot form for a slot one uses Get_Slot:

#include OBJECT_ADVANCED__H  // Note need for special header file

Am_Object_Advanced obj_adv = (Am_Object_Advanced&)my_object;
Am_Slot slot = obj_adv.Get_Slot (Am_LEFT);
A programmer must be careful using the advanced object and slot classes. Many operations can break the object system if used improperly. A general principle should be to use the advanced features for a short time right after an object is first created. After the object is manipulated to add or change whatever is needed, the object should never need to be cast to advanced again.

A number of methods in the advanced object class are used to fetch slots. The method Get_Slot retrieves a slot and returns it as the advanced class Am_Slot. Get_Slot will always return a slot local to the object. If the slot was previously not local because it is still inherited or for other reasons, Get_Slot will make a placeholder slot locally in the object and return that. If the slot does not exist at all, Get_Slot will return Am_NULL_SLOT. There are two other methods used for fetching slots: Get_Owner_Slot, and Get_Part_Slot. These methods are similar to Get_Slot except they are to be used only for fetching part or owner slots. It is entirely possible to use Get_Slot instead of these specialized methods, but the specialized methods are more efficient. Other Am_Object_Advanced method are:

3.11.5 Controlling Slot Inheritance

An innovation in Amulet is that the programmer can control the inheritance of each slot. This is useful if you want to make sure that certain slots are not shared by a prototype and its children. For example, the Amulet Am_Window object has a slot that points to the actual X/11 or MS Windows window object associated with that window. This slot should not be shared by multiple objects. The choices are defined by the enum Am_Inherit_Rule and are:

To set the inheritance rule of the Am_DRAWONABLE slot of new_win to be local:

new_win.Set_Inherit_Rule (Am_DRAWONABLE, Am_LOCAL);
The inherit rule may also be set directly on a Am_Slot:

#include <am_inc.h> // defines OBJECT_ADVANCED__H for machine independance
#include OBJECT_ADVANCED__H // for slot_advanced

Am_Object_Advanced obj_adv = (Am_Object_Advanced&)new_win;
Am_Slot slot = obj_adv.Get_Slot(Am_DRAWONABLE);
slot.Set_Inherit_Rule(Am_LOCAL);
Using the Am_Slot method is useful when one wants to perform several manipulations on a single slot. By fetching the Am_Slot once and reusing the value, one can save the time needed to search for the slot.

The default rule with which an object will create all new slots added to an object can be changed using the Set_Default_Inherit_Rule method. Likewise, the current inherit rule can be examined using Get_Default_Inherit_Rule.

((Am_Object_Advanced&)my_object).Set_Default_Inherit_Rule (Am_COPY);
Am_Inherit_Rule rule = my_adv_object.Get_Default_Inherit_Rule ();

3.11.6 Controlling Formula Inheritance

For slots that are inherited normally, sometimes you still might want to control Formula inheritance separately. Remember from Section 3.7.4 that instances inherit formulas from their prototypes, but that setting the instance's slot normally removes the inherited formulas. There are times, however, when constraints from a prototype should be retained in instances even if the instance's value is set. For example, the Am_VALUE slot of widgets contain formulas that compute the value based on the user's actions. However, programmers are also allowed to set the Am_VALUE slot if they want the widget to reflect an application-computed value. In this case, the default formula in the Am_VALUE slot should not be removed if the programmer sets the slot. To achieve this, the programmer must set the slot's Single_Constraint_Mode to false (the default is true).

obj.Set_Single_Constraint_Mode (Am_VALUE, false);
The same parameter can be set directly on the slot as well:

Am_Object_Advanced obj_adv = (Am_Object_Advanced&)obj;
obj_adv.Get_Slot (Am_VALUE).Set_Single_Constraint_Mode (false);
Now, if obj contains a constraint, any instances of obj will always retain that constraint, even if another constraint or value is set into the instance. Furthermore, calls like Remove_Constraint on the instance's slot will still not remove the inherited constraint (though it will remove any additional constraints set directly into the instance).

3.11.7 Writing and Incorporating Demon Procedures

Amulet demons are special methods that are attached directly to an object or slot. Demons are used to cause frequently occurring, autonomous object behavior. The demons are written as procedures that are stored in an object's ``demon set.'' The demon set is shared among objects that are inherited or copied from another.

The demon procedures that operate on an object have very specific purpose. There are five demons that can be overridden on the object level. Three of the demons deal with object creation and destruction, the other two handle part management.

Demons that are attached to slots behave similar to formulas. The slot demons are more generic than object level demons. Slot demons can detect when the slot value changes or is invalidated. Several slot demons can be assigned to a single slot making it possible for the slot to have multiple effects with a single event.

When a demon event occurs, the demon affected is put into the demon queue to be invoked later. All the demons put into demon queue are invoked, in order, whenever any slot is fetched by using Get. By invoking the demons on Get, Amulet can simulate the effects of eager evaluation because any demon that affects the value of different slots will be invoked whenever a slot is fetched.

3.11.7.1 Object Level Demons

The three demons that handle object creation and destruction are the create, copy, and destroy demons. Each demon is enqueued on its respective event. The create and copy demons get enqueued when the object is first created depending on whether the method Create or Copy was used to make the object. The destroy demon is never enqueued. Since the Destroy operation will cause the object to no longer exist, all demons that are already enqueued will be invoked and then the destroy demon will be called directly. This allows the programmer to still read the object while the destroy demon is running.

The creation/destruction demon procedures have the same parameter signature which takes the object affected by the demon. The type of the procedure is Am_Object_Demon.

// Here is an example create demon that initializes the slot MY_SLOT to
// be zero.
void my_create_demon (Am_Object self)
{
  self.Set (MY_SLOT, 0);
}
Two object-level demons are used to handle part-changing events. These are the add-part and the change-owner demons. The add-part and change-owner demons are always paired: the add-part demon for the part and the change-owner demon for the part's owner. Both demon procedures have the same parameter signature, three objects, which has the type Am_Part_Demon, but the semantics of each demon is different. The first parameter for both procedures is the self object -- the object being affected by the demon. The next two objects represent the change that has occurred. In the add-part demon, the second object parameter is an object that is being removed or replaced. The third parameter is an object that is being added or is replacing the object in the second parameter. For the change-owner demon, the semantics are reversed -- the second and third parameters represent the change that a part sees in its owner. The second parameter is the owner the part used to have, the third parameter is the new owner that has replaced the old owner.

// This owner demon checks to make sure that it's owner is a window. Any
// other owner will cause an error.
void my_owner_demon (Am_Object self, Am_Object prev_owner,
                     Am_Object new_owner)
{
  if (!new_owner.Is_Instance_Of (Am_Window))
    Am_Error ("You can only add me to a window!");
}
The events that generate add-part and change-owner demon events are methods such as Add_Part, Remove_Part, Destroy, and other methods that change the object hierarchy. Note that a given add-part demon always has a corresponding change-owner demon. The correspondence is not necessarily one to one because one can conceive of situations where one part is replacing another and thus two add-part calls can be associated with a single change-owner and vice versa.

Important note: The Opal and Interactor layers of Amulet define important demons for all of these object-level operations, so before setting a custom demon, the code should fetch the demon procedure currently stored in the demon set and call these in addition to the new demon. In a future release, we will make this more convenient to do.

void my_create_demon (Am_Object self)
{
	Am_Object_Demon_Type* proto_create = self.Get_Prototype ().
			Get_Demon_Set ().Get_Object_Demon (Am_CREATE_OBJ);
	if (proto_create)
		proto_create (self);  // Call prototype create demon.
	// Do my own code.
}

3.11.7.2 Slot Level Demons

Slot demons are not given permanent names like the object level demons. The slot demons are assigned a bit in a bit field to serve as their name. The slot demon procedure pointer is stored in the object. By turning on the bit in the slot, the slot will activate the demon procedure from the demon set whenever a triggering event occurs.

There are two parameters that control a slot demon. The first parameter distinguishes what event will trigger the demon. Slot demons can be triggered by one of two slot messages: the invalidate message or the value changed message. Most demons trigger on the value changed message because the demon's purpose is to note the change to other parts of the system. This can also be used to implement an active value scheme with a slot. Triggering using the invalidate message makes the demon act more like a formula. The demon can be used to revalidate the slot if desired. The eager demon uses this message to make Amulet formulas eager.

The other slot demon parameter is used to determine how often the demons will be triggered. Quite often, several slots affect the same demon in the same object. For instance, in a graphical object, the Am_TOP and Am_LEFT slots both affect the position of the object. A demon that handles object motion only needs to be triggered once if either of these slots changes. For this case, we use the per-object style. Whenever multiple slots change in a single object, the per-object demon will only be enqueued once. Only after the demon has been invoked will it reset and be allowed to trigger again. The other style of demon activation is per-slot. In this case, the demons act independently on each slot they are assigned. The demon triggers once for each slot and after it is invoked, it will be reset. The per-slot demon does not check to see if other demons have already been enqueued for the same object.

The slot demon procedure takes as its only parameter the slot that triggered the demon. If the demon could have been triggered by more than one slot (as can be the case when the demon is set to be per object), the slot provided is the very first one that triggered it.

// Here is an example slot demon. This demon does not do anything
// interesting, but it shows how the parameter can be used.
void my_slot_demon (Am_Slot slot)
{
  Am_Object_Advanced self = slot.Get_Owner ();
  self.Set (MY_SLOT, 0);
}

3.11.7.3 Modifying the Demon Set and Activating Slot Demons

To activate any demon, the object must know the demon procedure. Objects keep the list of available procedures in a structure called the demon set which is defined by the class Am_Demon_Set. Objects inherit their demon set from their prototype. The demon set is shared between objects in order to conserve memory. To modify the demon set of an object, one must first make the set a local copy. The demon set's Copy method is used to make new sets.

// Here we will modify the demon set of my_adv_obj by first making
// a copy of the old set and modifying it.  The new demon set is then
// placed back into the object.
Am_Demon_Set my_demons (my_adv_obj.Get_Demon_Set ().Copy ());
my_demons.Set_Object_Demon (Am_DESTROY_OBJ, my_destroy_demon);
my_adv_obj.Set_Demon_Set (my_demons);
When demon procedures are installed for object level demons, the demons will trigger on the next occurrence of their corresponding event. Note that the create and copy demon's events have already occurred for the prototype object where the demon procedures are installed. However, instances of the prototype as well as new copies will cause the new procedures to run. To make the demon procedure run for the current prototype object, one calls the demon procedure directly.

The demon set holds all demon procedures for the object, including the demons used in slots. The slot demons are installed by assigning each demon a bit name that will be stored in the slot. By setting the demon bit in a slot, events on that slot will activate the corresponding demon procedure. The bit name is represented by its integer value so bit 0 is number 1, bit 5 is number 32 (hex 0x0020). Section 3.11.7.5 discusses how to allocate demon bits.

// Here we install a slot demon that uses bit 5. The slot demon's semantics
// are to activate when the slot changes value and only once per object.
// Make sure that the demon set is local to the object (see above section).
my_demons.Set_Slot_Demon (0x0020, my_slot_demon,
                           Am_DEMON_ON_CHANGE | Am_DEMON_PER_OBJECT);
After the demon procedure is stored, one sets the bits on each slot that is able to activate the demon.

// Here we set a new bit to a slot. To make sure we do not turn off
// previously set bits, we first get the old bits and bitwise-or the new one.
Am_Slot slot = my_adv_obj.Get_Slot (MY_SLOT);
unsigned short prev_bits = slot.Get_Demon_Bits ();
slot.Set_Demon_Bits (0x0020 | prev_bits);
To cause newly created objects to have certain demon bits set, one changes the default demon bits.

// Make the new slot demon default.
unsigned short default_bits = my_adv_obj.Get_Default_Demon_Bits ();
default_bits |= 0x0020;
my_adv_obj.Set_Default_Demon_Bits (default_bits);
Another factor in slot demon maintenance is the demon mask. The demon mask is used to control whether the presence of a demon bit in a slot will force the slot to make a temporary slot in every instance. A temporary slot is used by ORE to provide a local slot in an object even when the value of the slot is inherited. If a temporary slot is not available, then there will be no demon run for that object. This is necessary when one wants inherited objects to follow the behavior of a prototype object. For instance, in a rectangle object, if one changes the Am_LEFT slot in the prototype, one would like a demon to be fired for the Am_LEFT slot in every instance. That requires there to be a temporary slot for every instance. Set the demon mask bit for all demons that require a temporary slot. For all other demons put zero. A temporary slot will be created for slots whose demon bits contain at least one bit stored in the demon mask.

// Setting the demon mask
unsigned short mask = my_adv_obj.Get_Demon_Mask ();
mask |= 0x0020;  // add the new demon bit.
my_adv_obj.Set_Demon_Mask (mask);

3.11.7.4 The Demon Queue

The demon queue is where demon procedures are stored when their events occur. Objects hold the demon queue in the same way that they keep their demon set: the same queue is shared when objects are instanced or copied. However, Amulet never uses more than one demon queue. There is only one global queue for all objects. It is possible to make a new queue and store it in an object, but it never happens.

// Here is how to make a new queue.
// It is unlikely that anyone will need to do this.
Am_Demon_Queue my_queue;
my_adv_obj.Set_Queue (my_queue);
To find and manipulate the global demon queue, one can take any object and read its queue.

Am_Demon_Queue global_queue =
           ((Am_Object_Advanced&)Am_Root_Object).Get_Queue ();
The demon queue has two basic operations: enqueuing a new demon into the list and causing the queue to invoke. Invoking the queue causes all stored demon procedures to be read out of the queue, in order, and executed. While the queue is invoking, it cannot be invoked recursively. This prevents the queue from being read out of order while a demon is still running.

The demon queue is automatically invoked in some circumstances. First, the queue is invoked whenever the method, Get, is called on an object. This makes sure that any demons that affect the slot being retrieved are brought up to date. Another time is when the Destroy method is called on the object. The other time the queue is invoked is when updating occurs in the main loop and other window updating procedures. When windows are updated, the demon queue is invoked to update changed object values.

The demon queue is not a true queue in that it does not have a dequeue operation. The dequeue is wrapped in the Invoke method. The queue does have a means for deleting entries. One deletes all the demons that have a given slot or object as a parameter by using the Delete method.

3.11.7.5 How to Allocate Demon Bits and the Eager Demon

In order to develop new slot demons, one must provide a bit name for it. Presently, Amulet does not provide a means for dispensing bit names for demons. To see if a demon bit is being used by an object, read the slot demons from the demon set and see which bits are not being used. This procedure is presently sufficient since one never modifies an object's demon set more than once. Generally, only prototype objects need to be manipulated and one can often know which demons are set in a given prototype object.

Some of the demon bits are off limits to Amulet programmers. Amulet reserves two bits for use by the object system and another three bits for opal. The object system uses bits 0 and 1, opal uses bits 2, 3, and 4. Bits 5, 6, and 7 are available for programmers. Presently there are only the eight bits available for slot demons.

Bit 0 in the object system is for the eager demon. The eager demon is a default demon that all slots use. The demon is used to validate the slot whenever it becomes invalid. This makes the formula validation scheme eager hence the name. A programmer can turn off eager evaluation by turning off the eager bit in all the slots that one wants to be lazy. One can also set the eager demon procedure to be NULL in the demon set. When adding new demons to a slot, one must be careful not to turn off the eager bit by accident.


Last Modified: 03:19pm EDT, May 24, 1996