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. Advanced users and researchers can use ORE to define demons on various object operations, to control slot inheritance, and even to write entirely new constraint solvers.

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.14 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 who want to extend Amulet.

3.2 Version

The version of Amulet you are using is stored in the variable Am_VERSION which is in amulet.h. The value will be a string, something like:''3.0''.

3.3 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.8 in the Overview chapter.

3.4 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.4.1 Get, Peek, Set and Add

The basic operations performed on slots are Get, Set and Add. A slot is essentially a key-value pair. Get takes a slot key and returns the slot's value. Peek is the same as Get, except it doesn't raise an error if the slot is not there. Set takes a key and value and replaces the slot's previous value with the one given. Add is the same as Set, except it is used to create new slots. Creating new slots is done by performing Add 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.Add (FOO, 45.3f); // Create a new slot called FOO and initialize with the float 45.3
Am_Value v = my_object.Peek(ZIP); // Get the value of ZIP if it is there.
Calling Get on a slot which does not exist raises an error (but see Section 3.4.10 for how to avoid the error). Calling Peek never raises an error, but may return an Am_Value which is an error value. Calling Set on a slot that does not exist, or calling Add on a slot which already exists, also raises an error (but see Section 3.4.10 for how to avoid these errors). The motivation for this is that we found in previous versions of Amulet that users were often Getting and Setting the wrong slot and wondering why the nothing happened. Therefore, the system now helps with this by notifying you when a slot that is being set or get isn't one of the standard slots. If you intend to add a new slot, you must specify that explicitly using Add instead of Set.

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_Object(PART_SLOT).Get (Am_LEFT);

3.4.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. Finally, there is Am_From_Slot_Name which takes a slot name and returns its key or 0 if the slot is not defined.

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";

Am_Slot_Key key = Am_From_Slot_Name("MY_BAR_SLOT");
if (key == 0) cout << "No slot by that name\n";

3.4.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

3.4.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.4.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.4.11).

3.4.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.4.8). 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.4.7 No Type, Zero Type, and Error Types

There are various kinds of error and empty values supported by Amulet, and they are used for different situations:

All of these values return false from the xxx.Valid() and xxx.Exists() tests (see Section 3.4.11).

3.4.8 Using Wrapper Types

Although one could store C++ objects into ORE slots as a void*, ORE provides two different ways to ``wrap'' these pointers so they can be handled in a type-safe manner. The most robust is to use the Am_Wrapper type which provides 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.4 and is somewhat complex. A simpler technique uses the Pointer_Wrapper types, which just provide type safety for the pointers, but no memory management. See *** for how to make new Pointer_Wrapper types.

However, using any of the wrapper types is simple. Notable wrapper types in Amulet are Am_Style, Am_Font, Am_String, Am_Value_List (see Section 3.9), 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.

Am_Value v = object.Get(MY_SLOT);
if (v.type == Am_Style::Type_ID ())
  Am_Style color = v;
Also, as described in the next section, most wrappers supply the Test static method which takes an Am_Value:

Am_Value v = object.Get(Am_VALUE);
if (Am_Value_List::Test(v)) { //then is a value list 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() or Exists() (see Section 3.4.11). 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.Peek (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.4.9 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 returns nothing (it has a void return type) and it 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 Am_Define_Method macro, the compiler can check to make sure that the actual method signature matches the one defined in the type. To set the method into a slot and retrieve the typename value, one uses Set or Add and Get or Peek 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 *Type_ID method.

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 Am_Define_Method_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.4.10 Flag parameters to Get and Set

The Get, Peek, Add and Set methods (and various other methods discussed below) take an optional final parameter which is of type Am_Slot_Flags. These flags control various errors and other properties of the setting and getting operations. The flags are bit fields, so they can be OR'ed together to get multiple affects. The current flags are:

What is the difference between Get and Peek? Between Set and Add:

Calling Set with the parameter Am_OK_IF_NOT_THERE or calling Add with the parameter Am_OK_IF_THERE are entirely equivalent, and will never return an error.

Peek is entirely equivalent to Get with the parameter Am_OK_IF_NOT_THERE.

Some examples:

obj.Set(SLOT, Am_No_Value);
int i = obj.Get(SLOT); //will raise an error because not an integer
Am_Value v = obj.Get(SLOT); //v be Am_No_Value
v = obj.Peek(NO_SUCH_SLOT); //assume slot NO_SUCH_SLOT is not in obj
// v has type Am_MISSING_SLOT
if (v.Valid()) {} // v is not Valid
v = obj.Get(NO_SUCH_SLOT); //will raise an error because no such slot
v = obj.Get(NO_SUCH_SLOT, Am_OK_IF_NOT_THERE); //returns Am_MISSING_SLOT
int i = obj.Get(NO_SUCH_SLOT, Am_OK_IF_NOT_THERE); //error because not int
int i = obj.Get(NO_SUCH_SLOT, Am_RETURN_ZERO_ON_ERROR); // i = 0
// next Set doesn't remove formulas and doesn't start an animation

3.4.11 Using Am_Value

The Get and Peek methods are defined as:

const Am_Value& Get (Am_Slot_Key key, Am_Slot_Flags flags = 0) const;
const Am_Value& Peek (Am_Slot_Key key, Am_Slot_Flags flags = 0) const
       { return Get(key, flags | Am_OK_IF_NOT_THERE); }
The Am_Value type which is used as the return type is a union for all the ORE types. The Am_Value type can be coerced to all the standard Amulet types automatically. Normally, the programmer simply sets the return value directly into the final destination variable:

//Get returns an Am_Value type, and the compiler uses the Am_Value's
// operator int method to turn it into an int automatically.
int i = obj.Get(Am_LEFT);
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 just use the Am_Value returned directly:

Am_Value v = obj.Get(Am_LEFT);
if (v.type == Am_INT) ...
If you are not sure whether the slot is there or not, be sure to use Peek or the Am_OK_IF_NOT_THERE or Am_RETURN_ZERO_ON_ERROR flags, as described in Section 3.4.10.

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.

bool Am_Value::Exists() const;
bool Am_Value::Valid () const
Exists returns false if the type is an error type (Am_FORMULA_INVALID, Am_MISSING_SLOT or Am_GET_ON_NULL_OBJECT) or Am_No_Value (Am_NONE type). Note that Exists returns true for 0 and NULL and for Am_ZERO types.

Valid returns true if the value exists (as in Exists) and if the value is not zero as well. Thus, Valid returns false if the value is false or zero or NULL, or illegal.

Am_Value value;
value = my_object.Get (SOME_SLOT); // Get the value regardless of type
if (value.Exists()) {
  // then it is safe to use value, but value could still be zero
value = my_object.Get (SOME_SLOT); // 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.
obj.Set(Am_LEFT, 0);
bool b = obj.Valid();  //false because is zero
bool b = obj.Exists(); //true because is not an error type
Am_Values also can be used to print and get the names of objects. This normally only works if Amulet is compiled with the DEBUG switch on.

To print objects, use:

value.Print(ostream& out) const;
value.Println() const { Print(cout); cout << endl << flush; }
Print prints the object to the specified stream. The Println version is designed for use in a debugger, and should print out the name of the object. If the value holds a wrapper type which is not an object, then these use the wrapper's type_support mechanism as described in Section

To get the name of an object, use:

const char * To_String() const;
To_String returns the string name of the value. For objects, this looks up the name. Some wrapper types will generate a name. Uses strstream and Print(out) if the value doesn't have a string name, so this is somewhat inefficient.

To get an object given its name, use:

static Am_Value From_String(const char * string, Am_ID_Tag type = 0);
This can be used to look up an object or other type given its name. If the string is in the global name registry, then the type is not needed. If the name is not there, then looks for a Am_Type_Support for the ID. Will return Am_No_Value if can't be found or can't be parsed. As an example, the following will return the rectangle prototype object, if this is compiled with DEBUG on:

Am_Object rect_proto_obj = Am_Value::From_String("Am_Rectangle");

3.4.12 Advanced features of the Am_Value_Type

The Am_Value_Type. which is what is returned by obj.Get_Slot_Type(xxx), is actually 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 3 bits are the type class. Currently, there are five kinds of classes,

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 (this is not usually useful).

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

3.5 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 are:

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 an Am_LINE_STYLE slot with the same value. If a slot is inherited, it will change it 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.14.4 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 original object, a copied object will become a sibling of the original, and an instance of the original's prototype. Every slot 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. Like Create, Copy can take an optional string name for the new object.

Copy_Value_Only is like Copy, except that it throws away all constraints. This is used when you want to ``archive'' an object, and is used by the undo mechanism so command objects on the history list keep the values used when the command was executed, since constraints used during execution should not be re-evaluated at Undo time.

In summary, the ways to make a new object are:

Other useful methods relevant to inheritance include:

3.6 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.7 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.7.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_Object (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.10) or else by reading it from a list like the Am_GRAPHICAL_PARTS slot in group objects (described in the Opal chapter, Section 4.8.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
Named parts that already exist should be set with Set_Part. Thus Add_Part is for adding parts that do not exist (just as Add is for adding slots that don't exist) and Set_Part is for setting parts that already exist (just as Set is for setting slots that already exist). Like Add and Set, Add_Part and Set_Part have an optional Am_Slot_Flags parameter so you can use flags like Am_OK_IF_THERE and Am_OK_IF_NOT_THERE:

// Adds a part to an object. The part either be named by providing a key or unnamed.
Am_Object& Add_Part (Am_Object new_part, bool inherit = true);

//slot must not already exist (unless Am_OK_IF_THERE flag).
//slot must never be a regular slot
Am_Object& Add_Part (Am_Slot_Key key, Am_Object new_part,
			       Am_Slot_Flags set_flags = 0);

//Slot must exist (unless Am_OK_IF_THERE), and must already be a part slot
Am_Object& Set_Part (Am_Slot_Key key, Am_Object new_part,
			       Am_Slot_Flags set_flags = 0);

3.7.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.


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 parameter
// 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.7.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_Object (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.8 Formulas

Formulas are used to connect together the values of slots. With formulas, the programmer constrains the value of one slot to be dependent on the value of other slots. When the dependent slots change, the value of the formula is recomputed and the formula's slot takes 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.14.2.

3.8.1 Formula Functions

An ORE formula consists of a C++ function that computes the value to set into a slot based on the values of the slot's dependencies. The formula function takes a single parameter, self, an Am_Object& that points to the object containing the slot. The return value of the formula function is the type that the slot will be when it takes on the returned value. Dependencies are set up automatically when Get is used to reference another slot, using a internal mechanism known as a constraint context. While constraint contexts were visible in Amulet v2 as opaque handles, and hidden only by macros, they do not clutter the interface in Amulet v3.

New formula constraints are normally defined using a family of Am_Define_Formula macros. For example:

// 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.Get_Owner ();
  int owner_width = owner.Get (Am_WIDTH);
  int my_width = self.Get (Am_WIDTH);
  return (owner_width - my_width) / 2;
When used as the value of a slot, this formula returns half the difference between the containing object's owner's Am_WIDTH and its own Am_WIDTH. Whenever the value of either of these Am_WIDTH slots changes, the formula will be recomputed.

There are 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_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. Defining Formulas without Macros

The formula my_left_formula defined above using the Am_Define_Formula macro expands into the following code.

// Am_LEFT which will center itself within its owner's dimensions.
static int my_left_formula_proc(Am_Object& self) {
  Am_Object owner = self.Get_Owner ();
  int owner_width = owner.Get (Am_WIDTH);
  int my_width = self.Get (Am_WIDTH);
  return (owner_width - my_width) / 2;
Am_Formula my_left_formula (my_left_formula_proc, "my_left_formula");
It defines a global variable of type Am_Formula named my_left_formula, which is the actual constraint that can be used to set a slot, and a static C++ procedure of type int (in this case) named my_left_formula_proc, which is called when the constraint needs to be reevaluated.

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_left_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. Formulas Returning Multiple Types

Some formulas are polymorphic -- that is they do not always return the same type of value. Since any value that can be stored in a slot can be represented as an Am_Value, a polymorphic formula should return a value of type Am_Value, and leave it to the slot's user to interpret the value.

A formula might return an Am_Value because it returns the value of some other slot without checking its type:

Am_Define_Formula (Am_Value, my_copy_formula) {
    return other_obj.Get (SOME_SLOT);
Or a formula might return one type under some conditions and another when those conditions are false. In this 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.

Am_Define_Formula (Am_Value, my_formula) {
  if ((bool)self.Get (Am_SELECTED))
    return 5;
    return Am_Blue;
To read a slot set with an unknown type, the programmer can call Get_Slot_Type or simply retrieve its value into an Am_Value.

3.8.2 Using Slots in a Formula without Creating Dependencies

When the Get method is used with within a formula, a dependency is automatically created on the slot being fetched. Then whenever the fetched slot changes value, the formula is notified by the system and automatically updates its value by calling the formula procedure.

The formula:

Am_Define_Formula (int, my_left_formula) {
  int owner_width = self.Get_Owner ().Get (Am_WIDTH);
  int my_width = self.Get (Am_WIDTH);
  return (owner_width - my_width) / 2;
defines three slots as dependencies: the object's Am_OWNER slot (the Get_Owner macro expands into a Get 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 Get on different slots. Thus the above formula's dependency on the owner's width will change if the object ever gets moved to a new owner.

To avoid creating a dependency on a fetched slot, the programmer should pass the Am_NO_DEPENDENCY flag when calling Get. For example, the Am_Width_Of_Parts formula contains a test to avoid creating a dependency on a slot that isn't there.

  if (self.Peek (Am_H_FRAME, Am_NO_DEPENDENCY).Exists())
    max_x += (int)self.Get (Am_H_FRAME);
  return max_x;
Other formulas make sure that self has an owner before checking any slot values. This technique is useful to prevent a constraint from being marked invalid (See the next section.) due to initialization order dependencies.

  Am_Object button = self.Get_Owner(Am_NO_DEPENDENCY);
  if( button.Valid() )
    return (bool)button.Get( Am_ACTIVE ) && (bool)button.Get( Am_ACTIVE_2 );
  return false;
Note that is perfectly reasonable to use Peek within a formula when a slot may be missing or uninitialized. A dependency will be created, however, unless the Am_NO_DEPENDENCY flag is used.

3.8.3 Putting Formulas into Slots

To install a formula in a slot, one needs an 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.Get_Owner ().Get (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) {
  return (int)self.GV_Owner ().Get (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 variables that a formula references are defined before the formula is set into a slot. Formulas that reference missing or undefined slots or NULL objects never crash; instead the constraint is marked as invalid and Get returns the Am_Value Am_Zero_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.Get_Owner ().Get (Am_LEFT);
It is an error to fetch the value of an uninitialized slot from outside a formula. One can check if a slot is uninitialized by calling Get_Slot_Type or by retrieving the value into an Am_Value using Peek and then calling Exists() or Valid(). For example:

	Am_Value v = obj.Peek(SLOT);
	if (v.Exists()) ... //value is not an error
If the slot is invalid, the Am_Value returned by Peek will have an Am_Error_Value_Type and Exists() will return false.

3.8.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. Sometimes, constraints from a prototype should be retained in instances even if the local value is set. This requires calling the formula's Multi_Constraint() method, for example:

my_rect.Set (Am_LEFT, my_left_formula.Multi_Constraint());
This is an advanced feature covered in Section 3.14.5.

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.

3.8.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. 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 (self) + 1;

3.9 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 stored as a slot value. 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.9.1 Current pointer in Lists

In addition to data, an Am_Value_List contains 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 elements 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:

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

3.9.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 does 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 an 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;
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.9.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. For example:

Am_Value v;
	v = my_list.Get();
	cout << "List item type is " << v.type << endl << flush;
To find the type of an item without fetching the value use Get_Type().

Get_Nth is used to retrieve the nth item in a list (the first item is the 0th item); Move_Nth makes the nth item the current item. Both methods take an int argument.

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.


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:

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. Empty_List() may also be used to create an Empty list that is not NULL.

Finally, Copy creates a new copy of a list.

3.10 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.10.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.10.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.10.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 effect 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.11 Connecting C++ Types

Although the Amulet object system is very convenient and flexible, there are various reasons why you might want to use other C++ types along with Amulet. Your custom C++ objects are probably smaller than Amulet objects, and they may be required to link to some external system.

Amulet provides a number of ways to link to external objects. In all of these, you store the C++ object or a pointer to the C++ object into a slot of an Amulet object. To put any kind of Amulet type or object into your C++ object, you can just declare it normally, like you would as a variable. For example:

class myclass {
  Am_Object my_object_in_class;
  Am_Style my_style_in_class;
  int i_in_class;
  void method_in_class(); 
You can now create instances or subclasses of myclass and assign the Amulet objects. Amulet will correctly handle the storage allocation for these objects and types.

To store your C++ objects into an Amulet object, there are various options depending on how much you need and how much work you want to do. These include coercing a pointer to a void*, using the Pointer Wrapper, using the Enum Wrapper, or creating a full-fledged wrapper type. In brief:

3.11.1 Type Support

The Am_Type_Support class is used to describe the C++ objects stored into slots, mostly for debugging. You will create a subclass of the main Am_Type_Support class to customize the print and read methods, and you can of course add extra instance variables to hold specific information. For example, Am_Enum_Support is a subclass of Am_Type_Support which adds extra fields to store the list of the enumerated values. The methods of the type support subclass that might be overridden include:

As an example, the following is the code used to create the Am_Type_Support for translating slot name strings to and from slot keys.

class Am_Slot_Key_Type_Support_Class : public Am_Type_Support {
  void Print (ostream& os, const Am_Value& value) const {
    os << Am_Get_Slot_Name((Am_Slot_Key)value.value.long_value);
  const char * To_String(const Am_Value &value) const {
    return Am_Get_Slot_Name((Am_Slot_Key)value.value.long_value);
  Am_Value From_String (const char* string) const {
    Am_Slot_Key key = Am_From_Slot_Name (string);
    if (key) {
      Am_Value v(key, Am_SLOT_KEY_TYPE);
      return v;
    else return Am_No_Value;

Am_Type_Support *slot_key_type_support =
new Am_Slot_Key_Type_Support_Class();
To associate the new type support with a particular type, you will probably use one of the macros described in the next sections. If these macros are not appropriate, then there are two other methods for associating a type support pointer with a type. The first is to register the type when you create a new Am_ID_Tag using the form of Am_Get_Unique_ID_Tag:

extern Am_ID_Tag Am_Get_Unique_ID_Tag (const char* type_name,
	       				Am_Type_Support* support, Am_ID_Tag base);
The base is used to separate out the different classes of IDs, as described in Section 3.4.12.

If you already have the Am_ID_Tag, then you can use:

extern void Am_Register_Support (Am_Value_Type type,
Am_Type_Support* support);
For example, the slot key type support created above is registered using the following code:

Am_Register_Support(Am_SLOT_KEY_TYPE, slot_key_type_support);
Finally, to find the type support object associated with a type, you can use Am_Find_Support. This is used by the Inspector. (Am_Value_Type is the same as Am_ID_Tag).

extern Am_Type_Support* Am_Find_Support (Am_Value_Type type);

3.11.2 Pointer Wrappers

A Pointer Wrapper is used to ``wrap'' pointers to C++ objects. This is a relatively simple interface which supports some type checking and debugging support. It does not provide memory allocation, however, so the programmer must be careful about this. To create a pointer wrapper, use the macros Am_Define_Pointer_Wrapper in the .h file, and Am_Define_Pointer_Wrapper_Impl in the .cc file (or use both in the .cc file if there is no .h file). The _Impl version takes a pointer to an Am_Type_Support which you should create for your type, as described in the previous section. Note that in these macros, the Type_name argument is the name of the base type, not of the pointer to the type.


Am_Define_Pointer_Wrapper_Impl(Type_name, Type_Support_Ptr)
The result of these macros is a new type, called Am_Type_name (with Type_name replaced with the actual name of your type) which is used when the pointers are assigned into a slot. Note: be sure to use the wrapper when assigning, or else the type information will not be saved. To get the value out of the Am_Type_name wrapper, use the .value accessor. For example, Am_Type_name(obj.Get(SLOT)).value. If you want to avoid all type-checking on accessing, you can use the void* (Am_Ptr) form on the Get as described above (Section 3.11):

As a complete example:

//In the .h file:

class my_class {
  ... // whatever is required


// In the .cc file:

class my_class_wrapper_support : public Am_Type_Support {
  { my_class * m = Am_my_class(val).value;
    os << print out my_class's data
} my_class_wrapper_support_obj;

Am_Define_Pointer_Wrapper_Impl(my_class, &my_class_wrapper_support_obj);

  my_class *ii = new my_class;
my_class *ii3 = (my_class*)(Am_Ptr)obj.Get(SLOT); //unsafe Get
Remember that pointer wrappers do not deal with memory allocation. Thus, if you create an instance or copy of obj in the code above, both the new object and the old object will share the same pointer to the same my_class value. If you want memory management, you have to use a ``real'' wrapper, as discussed in Section 3.11.4. Thus,

Am_Object new_obj = obj.Create();
my_class *ii4 = Am_my_class(new_obj.Get(SLOT)).value;
ii4 == ii2; //returns true
ii4 == ii;  //also true

3.11.3 Enum Wrappers

The Enum wrapper can be used for any type which can fit into a word (32 bits). It is often used for enumerated types, and special macros are available for associating the string names of the enumeration labels with the various values, so the Inspector will print out and accept typing of the string names. The enum wrapper can also be used for other types which can fit into a single word, such as bit fields. Enumerated Types

For an enumerated type, the programmer can use the macros Am_Define_Enum_Type or Am_Define_Enum_Long_Type in the .h file, and Am_Define_Enum_Support or Am_Define_Enum_Type_Impl in the .cc file. In either case, the programmer uses these macros to define a new type, and then has a declaration using this type of a global value for each value that the user will use. Then, these values can be set directly into slots. The .value of each of the items defined, will be the long or enumerated type associated with it at declaration time.

These macros are used as described in the next sections. Am_Define_Enum_Type
Am_Define_Enum_Type is used when you want to convert the value back and forth from the wrapper form to an enumerated type which you can use in a switch statement. In this case, you would name the enumeration values some internal name (we use xxx_val) and then define each exported name with the associated internal name. For example (from inter.h):

//This enum is used internally; users should use the Am_Move_Grow_Where_Attach values instead.
enum Am_Move_Grow_Where_Attach_vals
  Am_ATTACH_N_val, Am_ATTACH_NE_val,
  Am_ATTACH_E_val, Am_ATTACH_SE_val, Am_ATTACH_S_val, 
  Am_ATTACH_SW_val, Am_ATTACH_W_val, 
  Am_ATTACH_END_1_val, Am_ATTACH_END_2_val,  

// type of the Am_WHERE_ATTACH slot for Move_Grow Interactors
const Am_Move_Grow_Where_Attach 
const Am_Move_Grow_Where_Attach Am_ATTACH_NW(Am_ATTACH_NW_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_N(Am_ATTACH_N_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_NE(Am_ATTACH_NE_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_E(Am_ATTACH_E_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_SE(Am_ATTACH_SE_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_S(Am_ATTACH_S_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_SW(Am_ATTACH_SW_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_W(Am_ATTACH_W_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_END_1(Am_ATTACH_END_1_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_END_2(Am_ATTACH_END_2_val);
const Am_Move_Grow_Where_Attach Am_ATTACH_CENTER(Am_ATTACH_CENTER_val);
Now, users set the Am_Move_Grow_Where_Attach values into slots:

And the programs can get the values out of a slot the usual way:

	Am_Move_Grow_Where_Attach attach = inter.Get(Am_WHERE_ATTACH);
Individual values can be tested using ==, as follows:

	if (attach == Am_ATTACH_WHERE_HIT) ...
but if you want to use a switch statement, you can use the .value which will be the original enum:

	Am_Move_Grow_Where_Attach attach = inter.Get(Am_WHERE_ATTACH);
	switch (attach.value) {
	  case Am_ATTACH_NW_val:
	  case Am_ATTACH_SW_val: Am_Define_Enum_Long_Type
Am_Define_Enum_Long_Type is used when you are happy to define the enumeration values as long numbers. This would be the case where you never expected to need to access the values in a switch statement, but were happy just to use the == form. This saves the trouble of defining a separate enum type. For example (again from inter.h):

	// type of the Am_HOW_SET slot for Choice Interactors
	const Am_Choice_How_Set Am_CHOICE_SET (0);
	const Am_Choice_How_Set Am_CHOICE_CLEAR (1);
	const Am_Choice_How_Set Am_CHOICE_TOGGLE (2);
	const Am_Choice_How_Set Am_CHOICE_LIST_TOGGLE (3);
As before, these values can be set directly into slots, and can be gotten out again and tested with ==:

  inter.Set (Am_HOW_SET, Am_CHOICE_TOGGLE);
  Am_Choice_How_Set how_set = inter.Get (Am_HOW_SET);
  if (how_set == Am_CHOICE_LIST_TOGGLE) ... Am_Define_Enum_Support and Am_Define_Enum_Type_Impl
In addition to the declarations above, you must also add some code to the .cc file. The macro Am_Define_Enum_Support can be used when the enum values start at 0 and increment by one, which is the default if you do not explicitly specify values for the enumeration values. This macro can be used for either Am_Define_Enum_Type or Am_Define_Enum_Long_Type. The other macro, Am_Define_Enum_Type_Impl is more general, since it requires that you provide your own Am_Type_Support object which can do any necessary mapping for the printing and parsing.

Am_Define_Enum_Support is passed the type and a single string which contains all the string names of the enumerations in order with a single space between them. Note that C++ defines that if a string directly follows another string with no intervening operators, that the strings are concatenated together, which is the right way to make a string that is longer than one line, as in the following examples:

Am_Define_Enum_Support (Am_Choice_How_Set, ``Am_CHOICE_SET Am_CHOICE_CLEAR ``

Am_Define_Enum_Support (Am_Move_Grow_Where_Attach,
      ``Am_ATTACH_END_1 Am_ATTACH_END_2 Am_ATTACH_CENTER''); Bit Fields and Longs

Sometimes you might have a 32-bit value which is actually composed of different pieces crammed together. For example, the Am_Input_Char is composed of a number of fields packed into a structure that fits into 32 bits. The Enum Wrapper mechanism also can be used to wrap these types so the Inspector can view and edit them. In this case, you need to create your own Am_Type_Support object that will do the printing and parsing. Then you register this type_support with a new ID allocated for the type. Your class needs to have an operator Am_Value () const which will return the type converted into a long, and with the type set to the ID. You also need a constructor that will create your type from an const Am_Value& value.

As an example, for the Am_Input_Char, part of the definition of the class (in idefs.h) is:

class Am_Input_Char {
  Am_Input_Char (const Am_Value& value) {
    long l = value.value.long_value;
    if (l) {
      if (value.type != Am_Input_Char_ID)
		Am_Error (``** Tried to set an Am_Input_Char with a non ``
				  ``Am_Input_Char wrapper.'');
    //make an Am_Input_Char out of l

  operator Am_Value () const;
Then, in the .cc file:

class Input_Char_Support : public Am_Type_Support {
     Am_Input_Char ch = value;
     os << ch;
  // Reads a string, potentially provided by a user and converts to its own type.
  // Returns Am_No_Value when there is an error.
  Am_Value From_String (const char* string) const {
    Am_Input_Char ch(string, false);
    if (ch.code != 0) return ch;
    else return Am_No_Value;
} Input_Char_Object;  //Input_Char_Object is an instance of Am_Type_Support

// Now allocate the ID Tag using the type_support object.  Be sure to declare it to be Am_ENUM class
Am_ID_Tag Am_Input_Char::Am_Input_Char_ID =
  Am_Get_Unique_ID_Tag (``Am_Input_Char'', &Input_Char_Object, Am_ENUM);

// Convert an Am_Input_Char to a Am_Value:  build a long from the Am_Input_Char's bits
Am_Input_Char::operator Am_Value () const {
  long l;
  //make l out of this object
  return Am_Value (l, Am_Input_Char_ID);
Now, you can set the type into a slot and get it out, and the compiler will insert the correct coersion operators, and the Inspector will be able to view and edit the value of the slot.

Am_Input_Char c, c1;
obj.Set(SLOT, c);
c1 = obj.Get(SLOT);

3.11.4 Writing a Wrapper Using Amulet's Wrapper Macros

If you need full memory management for C++ objects that you store into the slots of Amulet objects, consider creating a new type of wrapper. Recall that 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. 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 many 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.14.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. 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 {
    Foo_Data (Foo_Data* prev)
      ... // initialize member values
    operator== (Foo_Data& test_value)
      ... // compare test_value to this
    ... // 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. Printing and Editing Values

Usually, you will want to override the methods for Printing and getting the values from a string. The default methods just look the value up in the name registry, and if not there, then prints the pointer in hex. These methods are all defined on the Wrapper type itself (that is a subclass of Am_Wrapper. In the example of the previous section, these would be methods of Foo_Data). The methods to define are: 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 initialized 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 = (Foo_Data*) data->Make_Unique ();
  data->Modify_Somehow ();
Note that Make_Unique returns an Am_Wrapper* that must be cast to the appropriate type when assigning the result to the wrapper data pointer.

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. 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.
  Foo (params);
  Use ();
  Modify ();

  // This normally goes in the .cc file.
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->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.12 Saving and Loading Objects from Files

Amulet has a built-in mechanism for saving objects to a file and then reading them back in. The file is is a special format, which is plain ascii text, but not meant for human-reading or for hand-editing. Many Amulet demos, including samples/example/example1.cc, samples/circuit/circuit.cc, and src/widgets/testselectionwidgets.cc use the save and load feature. Gilt (Section 8) also uses it, and Gilt also has a separate mechanism to generate a C++ file that can be compiled from the designed interface. Note that these are two different formats, and Gilt essentially has two different save mechanisms. Gilt cannot read back in the C++ files, only the Amulet-format files described in this section.

An important feature of the generated files is that any type of value in a slot should be savable, including references to other objects. If these other objects have already been created, then a reference to that object is used. Otherwise, the referenced object is saved or loaded, and the object is remembered in case it is referenced again.

3.12.1 Simple Interface For Storing and Loading Objects

There is a very flexible save and load architecture that allows you to create your own save and load mechanism and even your own file formats (see Section 3.12.3). However, most of the time, the basic mechanism is sufficient, and much easier to use. In this basic mechanism, you use Am_Default_Load_Save_Context, which knows how to save the specified slots of the objects.

Each object that you want to save should have a slot called Am_SAVE_OBJECT_METHOD containing Am_Standard_Save_Object and a Am_SLOTS_TO_SAVE which must contain an Am_Value_List containing the slot names of all the slots you want to save. This list must be sufficient to restore the object. Amulet has built-in methods for storing most types to a file (see Section 3.12.2). For example, in samples/example/example1.cc, all the rectangles are the same color, so the following set of slots is sufficient:

  rect_proto = Am_Rectangle.Create(``Rect_Proto'')
    .Set (Am_LEFT, 0)
    .Set (Am_TOP, 0)
    .Set (Am_WIDTH, 12)
    .Set (Am_HEIGHT, 8)
    .Set (Am_LINE_STYLE, Am_Black)
    .Set (Am_FILL_STYLE, Am_Red)
    //Allow this object to be saved to disk and read back in using the standard method
    .Add (Am_SAVE_OBJECT_METHOD, Am_Standard_Save_Object)
    //The standard method needs the list of slots that are different in each object.
    //Here, just the bounding box.
     .Add (Am_SLOTS_TO_SAVE, Am_Value_List()
In src/widgets/testselectionwidget, each rectangle can be a different color, so the slots are:

my_rectangle_proto = Am_Rectangle.Create(``Sel_Rect_Proto'')
   .Add (Am_SAVE_OBJECT_METHOD, Am_Standard_Save_Object)
   .Add (Am_SLOTS_TO_SAVE, Am_Value_List()
If the object you want to save is a group object, with parts that need to be saved in addition to slots, use Am_Standard_Save_Group to set the Am_SAVE_OBJECT_METHOD slot.

Then, for each kind of object that you will save to a file, you need to register the prototype object. When the object is read back in, an instance will be created of this object, and the slots specified in the Am_SLOTS_TO_SAVE will be restored to their old values. You register the prototypes using Am_Default_Load_Save_Context.Register_Prototype. The first parameter is a string that will be what this object is called in the file. This string must NOT contain spaces, and it must be unique among all the types of objects stored to this file. For example, from src/widgets/testselectionwidget.cc:

Am_Default_Load_Save_Context.Register_Prototype(``ARC'', my_arc_proto);
Am_Default_Load_Save_Context.Register_Prototype(``LIN'', my_line_proto);
Am_Default_Load_Save_Context.Register_Prototype(``POLY'', my_polygon_proto);
The final step is to actually cause a Command object to Save and Open (load, read) the file. The easiest way to do this is to use the built-in commands Am_Open_Command, Am_Save_As_Command and Am_Save_Command described in Section 7.4.2. That section describes how to use those commands.

If you want to create your own commands, then the methods of interest are Am_Default_Load_Save_Context.Reset() which initializes the load-save context, and then Am_Default_Load_Save_Context.Load which takes an ifstream initialized with ios::in, and returns an Am_Value, which if valid, will be filled with an Am_Value_List of the objects that were loaded. If saving, then after reset, call Am_Default_Load_Save_Context.Save() which takes an iofstream initialized with ios::out and an Am_Value which should contain an Am_Value_List of the objects to be saved. For example:

void my_save (Am_Value_List items_to_save, Am_String & filename) {
  ofstream out_file (filename, ios::out);
  if (!out_file) Am_Error(``Can't save file'');
  Am_Default_Load_Save_Context.Reset ();
  Am_Default_Load_Save_Context.Save (out_file, Am_Value (items_to_save));

Am_Value_List my_open (Am_String & filename) {
  ifstream in_file ((const char*)filename, ios::in);
  if (!in_file) Am_Error(``Can't open file'');
  Am_Default_Load_Save_Context.Reset ();
  Am_Value contents = Am_Default_Load_Save_Context.Load (in_file);
  if (!contents.Valid () || !Am_Value_List::Test (contents)) 
     Am_Error(``Bad file contents'');
  Am_Value_List loaded = contents;
  return loaded;
If the string names in the file do not match the names used with Register_Prototype, then the load will fail. This can happen if you try to load a file stored by one program into another program that uses different names (e.g., trying to load a program generated by circuit into testselectionwidget). Since the prototypes are not available in the other program, it doesn't know how to create the objects specified in the file. Therefore, it is a good idea to name your types with a unique name.

3.12.2 Types of Data to Store

Amulet has built-in method for storing the following types to a file. If you want to store a different type to a file:

In addition, all the built-in styles are registered, like Am_Red and Am_Line_2. Note that arbitrary fonts and styles are NOT available for loading and storing. To get around this, Gilt creates a table of the fonts and styles, and stores the index into the table to the file. Then, when the object is read back in, Gilt restores the real style and font slots.

If you want to store a new type to a file, then you need to write your own method for storing and retrieving it. The methods are of form Am_Save_Method and Am_Load_Method. For example:

Am_Define_Method (Am_Load_Method, Am_Value, load_long,
		  (istream& is, Am_Load_Save_Context& /* context */))  {
  long value;
  is >> value;
  char ch;
  is.get (ch); // skip eoln
  return Am_Value (value);

Am_Define_Method (Am_Save_Method, void, save_long,
		  (ostream& os, Am_Load_Save_Context& context,
		   const Am_Value& value))  {
  context.Save_Type_Name (os, ``Am_LONG'');
  os << (long)value << endl;
Then, these methods are registered with the context:

  Am_Default_Load_Save_Context.Register_Loader (``Am_LONG'', load_long);
  Am_Default_Load_Save_Context.Register_Saver (Am_LONG, save_long);
There are many other examples in the file src/opal/opal.cc. Whenever an Am_Value with type ID Am_LONG is seen, then save_long will be called, which writes the string ``Am_LONG'' to the file, and then when the reader sees the ``Am_LONG'' string, it will call the load_long method to read the data. Anything can be written to the file, as long as the saver and loader are consistent.

3.12.3 General Mechanism

The general load and save mechanism is defined in types.h. Most programs will find it sufficient to use the Am_Default_Load_Save_Context which is described above (and exported by opal.h), which is an instance of the Am_Load_Save_Context. If you need to create a new kind of load and save, the Am_Load_Save_Context is defined as:

// This class holds the state of load or save in process.  This will keep track of the names of items loaded
//  and will handle references to objects previously loaded or saved.
class Am_Load_Save_Context {
  Am_WRAPPER_DECL (Am_Load_Save_Context)
  Am_Load_Save_Context ()
  { data = NULL; }

  // This method is used to record items that are referred to by the objects being loaded or saved,
  // but the items themselves are permanent parts of the application hence they shouldn't
  //  (or possibly can't) be saved as well.  By providing a base number one can use the same name over
  //  and over.
  void Register_Prototype (const char* name, Am_Wrapper* value);
  void Register_Prototype (const char* name, unsigned base,
						 Am_Wrapper* value);
  //returns the name if the value is registered as a prototype.  If not registered, returns NULL
  const char* Is_Registered_Prototype (Am_Wrapper* value);
  // Load methods are registered based on a string type name.  The type name must be a single 
  // alphanumeric word (no spaces).  The loader is responsible for returning a value for anything
  // stream that is declared with that type name.  If two or more methods are registered on the
  // same name, the last one defined will be used.
  void Register_Loader (const char* type_name, 
const Am_Load_Method& method);
  // Save methods are registered based on a value type.  If a single type can be saved in multiple ways,
  //  it is up to the save method to delagate the  save to the proper method.
  // If two or more methods are registered on the same type, the last one defined will be used.
  void Register_Saver (Am_ID_Tag type, const Am_Save_Method& method);

  // Reset internal counters and references.  Used to start a new load or save session.
  // Should be called once per new stream before the first call to Load or Save. 
  // Will not remove registered items such as loaders, savers, and prototypes.
  void Reset ();

  // Load value from stream.  Call this once for each value saved.
  // Returns Am_No_Value when stream is empty.
  Am_Value Load (istream& is);
  // Save value to stream.  Call once for each value saved.
  void Save (ostream& os, const Am_Value& value);

  // Structures that are recursive must call this function before Load is called recusively. 
  // The value is the pointer to the structure being loaded.  This value will be used by internal values that
  //  refer to the parent structure.
  void Recursive_Load_Ahead (Am_Wrapper* value);
  // This procedure must be called by each save method as the first thing it writes to the stream.
  //  The procedure will put the name into the stream with the proper format.
  // After that, the save method can do whatever it needs.
  void Save_Type_Name (ostream& os, const char* type_name);

3.13 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.14 Advanced Features of the Object System

3.14.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 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.4.

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);
    obj.Set (MY_SLOT, list);

3.14.2 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 every time 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. 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_Web_Events& events) {
  events.End ();
  if (events.First ()) // No events
  Am_Slot slot = events.Get ();
  Am_Object self = slot.Get_Owner ();
  switch (slot.Get_Key ()) {
  case ADDEND1: { // ADDEND1 has changed last.
      int addend1 = self.Get (ADDEND1);
      int addend2 = self.Get (ADDEND2);
      self.Set (SUM, addend1 + addend2);
  case ADDEND2: { // ADDEND2 has changed last.
      int prev_value = events.Get_Prev_Value ();
      int addend2 = self.Get (ADDEND2);
      events.Prev ();
      if (events.First () || events.Get ().Get_Key () != SUM) {
        int addend1 = self.Get (ADDEND1);
        self.Set (SUM, addend1 + addend2);
      } else { // SUM was set before ADDEND2. Must propagate previous result to ADDEND1.
        int sum = self.Get (SUM, Am_NO_DEPENDENCY);
        self.Set (ADDEND1, sum - prev_value);
        self.Set (SUM, sum - prev_value + addend2);
  case SUM: { // SUM has changed last.
      int sum = self.Get (SUM);
      int addend2 = self.Get (ADDEND2);
      self.Set (ADDEND1, sum - addend2);
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.

Using Set in a web is similar to using Get except that it is used for output instead of input. Every slot set with Set will be assigned a constraint pointer to that web making the slot dependent on the web. It is legal to both Get and Set 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 Get is called on the SUM slot, the web's constraint on that slot will be kept. If neither Get or Set are called on a slot during validation, the dependency and/or constraint will be removed. 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 (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);
The Am_Web_Init class is used to make dependency and constraint connections without performing a Get or Set 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 they can use Get or Set as usual. The slot parameter provides the primary slot to which the web was attached. Note that it is not necessary to call Note_Output on the primary slot, since it is already an output slot, by virtue of setting the slot with the web. Installing Into a Slot

A web is put into an object by setting it into its primary slot. First, an 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,
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.14.3 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. Unless the slot has the inheritance rule Am_STATIC (See Section 3.14.4.) in the object's, prototype, Get_Slot will always return a slot local to the object. If the slot was previously inherited, Get_Slot will make a placeholder slot locally in the object and return that. If the slot does not exist locally and is not inherited from the prototype, Get_Slot will return a slot whose type is Am_MISSING_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.14.4 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 defined in object.h and are:

makes the Am_SELECTED slot exist in all instances and copies of obj, but each one will have an independent value, so setting the Am_SELECTED slot of the prototype will not affect the instances.

obj.Set(Am_FEEDBACK_OBJ, rect3)
.Set_Inherit_Rule(Am_SELECTED, Am_COPY);
inst = obj.Create();
This is probably not useful. Each instance of obj (e.g., inst) will also point to rect3, but setting the Am_FEEDBACK_OBJ slot of obj will not affect the values of inst:

obj.Set(Am_FEEDBACK_OBJ, rect4);
inst.Get(Am_FEEDBACK_OBJ);  // ==> returns rect3
Instead, use:

obj.Add_Part(Am_FEEDBACK_OBJ, rect4);
inst = obj.Create();
inst.Get(Am_FEEDBACK_OBJ);  // ==> returns rect3_723
Now, all instances of obj will have independent objects in the Am_FEEDBACK_OBJ slot, which are created as instances of rect4.

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 independence
#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);
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.14.5 Controlling Formula Inheritance

For slots that are inherited normally, sometimes you still might want to control formula inheritance separately. Remember from Section 3.8.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 call the formula's Multi_Constraint method when setting the slot. For example,

obj.Set (Am_SELECTED, button_sel_from_value.Multi_Constraint());
Now obj and any of its instances will always retain that constraint, even if another constraint or value is set into the instance. Furthermore, calls like Remove_Constraint on an instance's slot will still not remove the inherited constraint (though it will remove any additional constraints set directly into the instance). Calling Remove_Constraint on obj itself, however, will destroy the constraint normally.

The Multi_Constraint method is also used to attach multiple constraints to a single slot. Any formula set into a slot using the Multi_Constraint suffix will not be removed by subsequent sets.

3.14.6 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 similarly 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. 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 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* proto_create = self.Get_Prototype ().
			Get_Demons ().Get_Object_Demon (Am_CREATE_OBJ);
	if (proto_create)
		proto_create (self);  // Call prototype create demon.
	// Do my own code.
} 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);
} 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. Thus before modifying 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_Demons ().Copy ());
my_demons.Set_Object_Demon (Am_DESTROY_OBJ, my_destroy_demon);
my_adv_obj.Set_Demons (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 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); 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. How to Allocate Demon Bits and the Eager Demon

In order to develop a new slot demon, 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: 01:38pm EDT, August 13, 1997