Dynamic value mechanism for computer storage container manager enabling access of objects by multiple application programs5652879Abstract Computer apparatus stores a subject value and a chain of sequentially associated value handlers for the subject value. The chain includes a top value handler and a bottom value handler, each of the value handlers in the chain except the bottom value handler invoking the respective next value handler when invoked, the bottom value handler performing an operation on the subject value when invoked. The value operations can be data read operations, data write operations, etc., and the value handlers in the chain can perform data transformations and/or data redirections, transparently to its caller. Claims We claim: Description BACKGROUND
______________________________________
.COPYRGT. 1992 Apple Computer, Inc.
______________________________________
CMSize CMGetValueSize(CMValue value);
CMSize CMReadValueData(CMValue value, CMPtr buffer,
CMCount offset, CMSize maxSize);
void CMWriteValueData(CMValue value, CMPtr buffer,
CMCount offset, CMSize size);
void CMInsertValueData(CMValue value, CMPtr buffer,
CMCount offset, CMSize size);
void CMDeleteValueData(CMValue value, CMCount offset,
CMSize size);
void CMGetValueInfo(CMValue value, CMContainer *container,
CMObject *object, CMProperty *property,
CMType *type, CMGeneration *generation);
void CMSetValueType(CMValue value, CMType type);
void CMSetValueGeneration(CMValue value,
CMGeneration generation);
void CMReleaseValue(CMValue);
______________________________________
As an aside, the present description often uses C-language notation as a shorthand way of describing the steps performed by, or other characteristics of, a Container Manager routine. In this notation, all module names and external data that can possibly be visible to an application programmer begin with the letters "CM" or "cm". The upper case "CM" prefixes all API visible routines and macros. The prefix "kCM" is used for constants. The lower case "cm" is used for all inter-module references within the Container Manager. All other data and modules have no other naming conventions and should not be visible outside of the file in which they occur. Macros used within the Container Manager do not follow these conventions since they are never visible in the generated object modules. Thus names beginning with "cm" or (upper or lower case) are reserved by the API and should not be used by the applications using the API. Also as an aside, routines or code portions which are not described herein are considered self-documenting either due to commenting or due to the use of self-documenting symbol names. For example, it will be apparent to the reader without further explanations that the CMGetValueSize() operation mentioned above returns the size of the specified value. Returning again to the above Container Manager value routines, none can be called for a particular value until one of the following preparatory routines are called for that value: CMNewValue() or CMUseValue(). As described below, if the desired value is a dynamic value, these routines set up the chains of dynamic value handlers needed to support the above routines. When a dynamic value is spawned by CMNewValue() or CMUseValue(), the pointer to the top-most dynamic value header is returned as the refNum. Then, whenever the user passes a refnum to an API value routine, it checks to see if the refNum is a dynamic value. If it is, it initiates the call to the corresponding value handler. That may cause a search up the base value chain to look for an "inherited" value routine. In the limit, we end up using the original API value routine if no handler is supplied and we reach the "real" value in the chain. Thus the handler must be semantically identical to the corresponding API call. These dynamic values only exist from creation during the CMUseValue() until they are released by CMReleaseValue(). A dynamic value can have its own data, but this data is stored in the value's refCon rather than in the value data itself. Dynamic values do not have associated data in the normal sense. A dynamic value is created when a value is created by CMNewValue() or used by CMUseValue(), and the following two conditions occur: 1. The type of the value, or any of its base types, have metahandlers which have been registered by the Container Manager CMSetMetaHandler() routine in a session-wide metahandler symbol table (CMSetMetaHandler() is usually called when a container is first opened); and 2. The metahandlers support a Use Value Handler, and in addition for CMNewValue(), a New Value Handler. The New Value Handlers are used to save initialization data for the Use Value Handlers. The Use Value Handlers are called to set up and return a refCon. Another metahandler address is also returned. This is used to get the address of the value operation handlers corresponding to the standard API CM . . . value routines mentioned above. When a CMNewValue() or CMUseValue() is almost done, a check is made on the value's type, and all of its base types (if any) to see if it has an associated registered metahandler. If it does it is called with a Use value operation type to see if a Use Value Handler exists for the type. If it does, we spawn the dynamic value. The spawning is done by calling the Use Value Handler. The Use Value Handler is expected to set up a refCon to pass among the value handlers and a pointer to another metahandler. These are returned to CMNewValue() or CMUseValue() which does the actual creation of the dynamic value. The extensions are initialized, the metahandler pointer and refCon are saved. The pointer to the created dynamic value header is what CMNewValue() or CMUseValue() returns to the user as the refNum. Now, when the user attempts to do a value operation using this refNum, we will use the corresponding handler routine in its place. The vector entries are set on first use of a value operation. If a handler for a particular operation is not defined for a value, its "base value" is used to get the "inherited" handler. This continues up the chain of base values, up to the original "real" value that spawned the base values from the CMNewValue() or CMUseValue(). Once found, we save the handler in the top layer vector (associated with the refNum) so we don't have to do the search again. Thus, as in C++, dynamic values may be "subclassed" via their (base) types. Note that if we indeed do have to search up the base value chain then we must save the dynamic value refNum (pointer) along with the handler address. This is very much like C++ classes, where inherited methods are called and the appropriate "this" must also be passed. The Container Manager supports layering of dynamic values. The best way to describe layering is in terms of C++. Say we have the following class types (using a somewhat abbreviated notation):
______________________________________
.COPYRGT. 1992 Apple Computer, Inc.
______________________________________
class Layer { // a base class
<layer1 data> // possible data (fields)
Layer1(<layer1 args>); // constructor to init the data
other methods...
// value operations in our case
};
class Layer2 {
// another base class
layer2 data> // possible data (fields)
Layer2(<layer2 args>); // constructor to init the data
other methods...
// value operations in our case
class T: Layer1, Layer2 {
// the class of interest!
<T data> // possible data (fields)
T(<T args>, <layer1 args>, <layer2 args>);
.sup. // constructor to init the data and bases
other methods...
// value operations in our case
};
______________________________________
In Container Manager terminology, T is to be a registered type with other registered types as base types (classes). All type objects are created using the standard API call CMRegisterType(). Base types can be added to a type by using CMAddBaseType(). This defines a form of inheritance like the C++ classes. Type T would be registered with its base types as follows:
______________________________________
.COPYRGT. 1992 Apple Computer, Inc.
______________________________________
layer1 = CMRegisterType(container, "Layer1");
layer2 = CMRegisterType(container, "Layer2");
t = CMRegisterType(container, "T");
CMAddBaseType(t, layer1);
CMAddBaseType(t, layer2);
______________________________________
For the t object, the global name property and value are created as usual by CMRegisterType (container, "T"). The CMAddBaseType () calls add the base types. These are recorded as the object ID's for each base type in the order created as separate value segments for a special "base type" property belonging to the type object. As mentioned above, CMNewValue() or CMUseValue() spawn dynamic values if the original type or any of its base types have an associated Use Value Handler. Assume that was done for "T" in the above example. What happens is that CMNewValue() or CMUseValue() will look at its type object (t here) to see if the base type property is present. If it is, it will follow each type "down" to leaf types using a depth-first search. In the example, "layer1" will be visited, then "layer2", and finally the original type "T" itself. If the "layer1" type object had base types of its own, they would be visited before using "layer1" itself. Hence the depth-first search down to the leaf types. For each type processed, if it has a Use Value Handler of its own, it will be called to get a refCon and value handler metahandler. Note that this scheme allows total freedom for the user to mix types. For example, type T1 could have base types T2 and T3. Alternatively, T1 could just have base type T2 and T2 have T3 as its base type In the C++ class types shown above, note that each class could have its own data along with its own constructor. The T class has a constructor that calls the constructors of all of its base classes. We can carry this analogy with the Container Manager just so far. Here is where it starts to break down. The problem here is that C++ class types are declared statically. A C++ compiler can see all the base classes and can tell what data gets inherited and who goes with what class. In the Container Manager, all "classes" (i.e., our type objects) are created dynamically. So the problem is we need some way to tell what data "belongs" to what type. The solution is yet another special handler, which returns a format specification called metadata. The handler is the Metadata Handler whose address is determined by the Container Manager from the same metahandler that returns the New Value and Use Value Handler addresses. Metadata is very similar to C-language printf() format descriptions, and is used for similar purposes. The next section will describe the metadata in detail. For now, it is sufficient to know that it tells CMNewValue() how to interpret its ". . . " parameters. The rest of this section will discuss how this is done to dynamically create data. As with C++ classes, the data is created when a new value is created, i.e., with a CMNewValue() call. The data will be saved in the container, so CMUseValue() uses the type format descriptions to extract the data for each dynamic value layer. CMNewValue() is defined with the following prototype:
______________________________________
CMValue
CMNewValue(CMObject object, CMProperty property,
CMType type, ...);
______________________________________
The ". . . " is an arbitrary number of parameters used to create the data. Metadata, accessed from the Metadata Handler, tells CMNewValue() how to interpret the parameters just like a printf() format tells printf() how to use its arguments. The order of the parameters is important. Because base types are done with a depth-first search through the types down to their leaves, the CMNewValue() ". . . " parameters must be ordered with the parameters for the first type in the chain occurring first in the parameter list. Note what's happening here is that the user is supplying all the constructor data just like T constructor class example above. The way the data gets written is with a special handler, called the New Value Handler. After CN/NewValue() calls the Metadata Handler, it uses the metadata to extract the next set of CMNewValue() ". . . " parameters. CMNewValue() then passes the parameters along in the form of a data packet to the New Value Handler. The New Value Handler is then expected to use this data, which it can extract with the Container Manager CMScanDataPacket() routine. Once it has the data, it can compute initialization values to write to its base value. It is the data written by the New Value Handler that the Use Value Handler will read to create its refCon. Only CMNewValue() does this. The New Value Handler is only for new values, but the Use Value Handler is used by both CMNewValue() and CMUseValue(). In the simplest case, with only one dynamic value, it can be seen that the data is written to the "real" value. Now if you layer another dynamic value on to this, the next chunk of data is written using that layer's base value and hence its handlers. The second layer will thus use the first layer's handlers. That may or may not end up writing to the "real" value depending on the kind of layer it is. If it's some sort of I/O redirection handler (i.e., it reads and writes somewhere else), the second layer data will probably not go to the "real" value. The Use Value Handler is called both for CMNewValue() and CMUseValue(). The Use Value Handler reads the data from its base value to create its refCon. If the user comes back the next day and does a CMUseValue(), only the Use Value Handler is called. Again it reads the data from its base value to construct the refCon and we're back as we were before in the CMNewValue() case. It should be pointed out here that the Metadata and New Value Handlers will always be executed with a Container Manager running on some particular hardware (obviously). The data packet built from the CMNewValue() ". . . " parameters is stored as a function of the hardware implementation on which it is run (i.e., whatever the sizes are for bytes, words, longs, etc.). How it is stored is a function of the metadata returned from the Metadata Handler. In other terms, the New Value Handler has a contract with both the Container Manager and the Metadata Handler on the meaning of the parameter data. Note, however, it is not required that you be on the same hardware when you come back the next day and to the CMUseValue() that leads to the Use Value Handler call. The handler writer must keep this in mind. Specifically, the Use Value Handler must know the attributes (bytes size, big/little endian, etc.) of the data written out by the New Value Handler so it knows how to use that info. In other words, the Use Value Handler has a (separate) "contract" with its own New Value Handler on the meaning of the data written to the base value. There is another, relatively minor, thing to keep in mind. That is that the value handlers for any one layer must take into account the size of its own data when manipulating additional data created by the handlers for CMReadValueData(), CMWriteValueData(), etc. This simply offsets the write and read value data operations by the proper amount. Remember all operations are on their base values. So if a New Value Handler writes data, this basically prefixes the "real" stuff being written by the handler operations. The Metadata Handler is only needed for CMNewValue() so that the proper number of CMNewValue() ". . . " parameters can be placed into a data packet for the New Value Handler. The Metadata Handler must follow the prototype,
______________________________________
CMMetaData metaData.sub.-- Handler(CMType type);
______________________________________
where "type" is the (base) type layer whose metadata is to be defined. The Metadata Handler simply returns a C string containing the metadata using the format descriptions described above. The type is passed as a convenience. It may or may not be needed. It is possible for a type object to contain other data for other properties. Types, after all, are ordinary objects. There is nothing prohibiting the creation of additional properties and their values. This fact could be used to add additional (static and private) information to a type to be used elsewhere. For example, the type could contain a compression dictionary. The New Value Handler must follow the prototype,
______________________________________
CMBoolean newValue.sub.-- Handler(CMValue baseValue,
CMType type,
CMDataPacket dataPacket);
______________________________________
where baseValue=the base value which is to be used to write the refCon data for the Use Value Handler type=the type corresponding to this New Value Handler dataPacket=the pointer to the data packet, created from the CMNewValue() ". . . " parameters according the types metadata format description. The type is passed again as a convenience just as in the Metadata Handler. It can also be used here to pass to CMScanDataPacket() to extract the dataPacket back into variables that exactly correspond to that portion of the CMNewValue() ". . . " parameters that correspond to the type. It is not required, however that CMScanDataPacket() be used. The Use Value Handler is called for both the CMUseValue() and CMNewValue() cases. If its companion New Value Handler wrote data to its base value, the Use Value Handler will probably read the data to create its refCon. The refCon will be passed to all value handlers. The Use Value Handler returns its refCon along with another metahandler address that is used to get the value handler addresses. These are used to create the dynamic value. The Use Value Handler should follow the prototype,
______________________________________
CMBoolean useValue.sub.-- Handler(CMValue baseValue,
CMType type,
CMMetaHandler *metahandler,
CMRefCon *refCon);
______________________________________
where baseValue=the base value which is to be used to write the refCon data for the Use Value Handler type=the type corresponding to this New Value Handler metahandler=a pointer to the value operations metahandler which is returned by the Use Value Handler to its caller refCon=a reference constant built by the Use Value Handler and returned to its caller. The baseValue and type are identical to the ones passed to the New Value Handler. The type may or may not be needed in the Use Value Handler. Like the Use Value Handler, it could be used to supply additional information from other properties. It is expected that the Use Value Handler will read data from its base value to construct its refCon. The refCon is then returned along with a pointer to another metahandler that is used by the Container Manager to get the addresses of the value operations. Note, both the New Value and Use Value Handlers return a CMBoolean to indicate success or failure. Failure means (or it is assumed) that the handlers reported some kind of error condition or failure. As documented, error reporters are not supposed to return. But in case they do, we use the CMBoolean to know what happened. It should return 0 to indicate failure and non-zero for success. Value Operation Handlers. The value operation handler routines can do a Container Manager CMGetValueRefCon() call on the value which was passed, in order to get at the refCon set up by the Use Value Handler. This provides a communication path among the value handlers. Further, the value handler should usually do its operations in terms of their base value, which can be accessed using the Container Manager CMGetBaseValue() call. The release handler is an exception to this rule. A set of one or more dynamic value layers are spawned as a result of a single CMUseValue() or CMNewValue(). The layers result from the specified type having base types. From the caller's point of view s/he is doing one CMUseValue() or CMNewValue() with no consideration of the base types. That implies that the returned dynamic value should have a single CMRelaseValue() done on it. The handlers have no business doing CMReleaseValue() on their base value. This is detected and treated as an error. A count is kept by the Container Manager of every CMUseValue() and CMNewValue(). Calling CMReleaseValue() reduces this count by one. When the last release is done on the dynamic value (its count goes to 0), the release handler will be called. It is the Container Manager who calls the release handler for all the layers, not the handler. The Container Manager created them as a result of the original type; it is therefore responsible for releasing them. The reason the Container Manager is so insistent on forcing a release for every use of a dynamic value is mainly to enforce cleanup. Most value operation handlers will, at a minimum, use a refCon that was memory allocated by the Use Value Handler. Release handlers are responsible for freeing that memory. In another example, if any files were open by the Use Value Handler, the releases would close those files. A trivial value handler might merely get its base value and use it to recursively call the Container Manager value procedure which initially invoked the handler to do its operation (again except for the release handler). In this case what it is basically doing is invoking the "inherited" value operation. In this case, the value operation could be omitted entirely by having the metahandler for the value's type return NULL when asked for that value operation. The Container Manager uses that as the signal to search up the dynamic value inheritance chain to find the first metahandler that does define the operation. In the limit, it will end up using the original "real" value. Possible Limitations On Value Operations. Value I/O operations are basically stream operations. That is, you read or write information linearly from a specified offset. In addition, the Container Manager provides insert and delete value data API calls CMInsertValueData() and CMDeleteValueData(). Insert and delete can cause problems because base types may want to do certain transformations on their data that depend on what has occurred previously in that stream of data. For example, encryption using a cyclic key, or compression generally cannot be done simply by looking at a chunk of data starting at some random offset. A cyclic key encryption can be deterministic if you can always determine where to start in the key as a function of offset. But you can see that inserts and deletes will change the offsets of following data. You would not know where to start in the key. What all this means is that certain data transformations only make sense if you are willing to refuse to support the insert/delete operations. Basically only data transformations that are position independent can be supported with the full set of value operations. Even simple I/O to a file may create problems, since most file systems do not support inserts and deletes in the middle of a file. If you do want to support inserts and deletes, then you should consider the potential for data intensive and/or computationally intensive operations. C. Format Overview A conceptual description of the Container Manager data format is now presented. As an overview, certain caveats and tricks are omitted at this level which are covered in more detailed parts of this description. Five key ideas underlie the Container Manager format: 1) everything in a container is an object, 2) objects have persistent IDs, 3) all the metadata lives in the TOC (Table of Contents), 4) objects consist entirely of values, and 5) each value knows its own property, type, and data location. The five ideas will each be discussed in turn. Everything is an object. In a Container Manager container, every accessible byte is part of a value of some object. Even the metadata that defines the structure of the container, and the label of the container, are values of an object. Type descriptions are objects, property descriptions are objects, etc. We will exploit this fact in various ways below. Objects have persistent IDs. Every Container Manager object is designated by a persistent ID which is unique within the scope of its container. Objects may have additional IDs and/or names that are unique in larger scopes, but this is not required. Object IDs provide a compact, convenient way to refer to an object. An efficient mechanism is provided to get from any object ID to information about that object. All the metadata lives in the TOC. This is a difference between the Container Manager and most other container formats, such as ASN.1, formats derived from IFF (such as Microsoft/IBM's RIFF), etc. In these other formats, the metadata is associated with the chunks of data that it describes, a design approach that we call internally tagged. There are three reasons for this difference from other formats: a) The Container Manager embodiment described herein is designed to support very flexible layouts, such as multi-media interleaving, and internal tags would be inconvenient and even harmful for this. b) Applications inspecting an object can make decisions about it more efficiently if all of its metadata is concentrated in one place, rather than being spread out over the container with its values. c) We want to be able to assimilate existing formats that contain collections of objects without forcing them to change. This implies that we must be able to designate regions within the existing structure as values, without forcing them to somehow retrofit internal tags. This approach to metadata does impose one significant design constraint. A Container Manager container can only be read by starting with the TOC. This raises two questions: (1) how do we find the TOC, and (2) how do we access the TOC when we need information? 1) In standard Container Manager containers the container label points to the TOC. Possibly some non-standard containers will exist that require other mechanisms. However, these will be exotic cases. 2) Since we need to access the information in the TOC whenever we want to read a value, we have to have it available at all times. This normally means that the container needs to be on a random access device. If a container needs to be read on a device that does not support efficient random access (such as a CD-ROM) the TOC can be split up into sub-TOCs that sit in front of the groups of objects they describe, and then the container can be accessed largely in stream order. Objects consist entirely of values. In the Container Manager, an object has no value as such. Each object has properties, and each property has values. The Container Manager format provides no information about an object except its ID. Of course, an object can have a single value; in that case the value of the property "is" the value of the object. Thus the Container Manager format can easily accommodate this "normal" case. Each value knows its own property, type, and data location. Each value consists of a property ID (or role), a type (or format), and data. For example, a graphic object might have a value that describes its "clip mask"; the property ID would specify what role the value plays, but not what format it is stored in. The type would define how the mask is represented: rectangle, bit mask, path, Mac region, PostScript path, etc. The data would be the representation of the mask itself. At the level of the container standard itself, there are no restrictions on what values an object can have, how many values it can have, etc. However, individual object formats may dictate rules in this area. In general, applications should be prepared to encounter additional values that they do not understand; these can be ignored. This allows other applications to annotate objects with additional values that may not be generally understood. Typically, these values will be associated with properties that are unknown to the application. The data of a value is an uninterrupted sequence of bytes which may be from 0 to 2.sup.64 bytes long, although these limits may vary in a different embodiment. This sequence of bytes has no format requirements or restrictions. Furthermore, the byte sequences representing the data for various values of various objects can be placed anywhere in the container. Thus there are no strong data format requirements for the container as a whole, although it must contain the metadata to define its structure somewhere. Special Cases. All of the mechanisms above are consistent across all the uses of objects. However, there are two special cases that need to be considered. First, The Container Manager format allows a single object to have multiple values with the same property ID. All the values must have different types. Such multiple values are intended to be used as alternative representations of the same information. Second, the table of contents can contain multiple entries for a single value. These entries mean that the value represented by the entry is actually stored in multiple segments. This permits values to be broken up into chunks and interleaved, without creating problems for applications that view them as single values. In addition, it allows an application to build TOC entries that "synthesize" a value out of separate parts, as is required in retrofitting some file formats. Note that these two special cases can be mixed freely. A property can have multiple values, and one or more of the values can be composed of multiple segments. Other Issues--Globally Unique Names. To fulfill the requirement for locally generated unique names for types and properties, the Container Manager embodiment described herein supports identifiers defined in ISO 9070. These are names that begin with a naming authority (assigned to a system vendor or an application vendor), and then continue with a series of more and more specific segments, until they end in a specific type or property name. While another embodiment can use a different naming convention, names generated according to ISO 9070 are both unique and self-documenting. Individual users can generate unique names using this approach. For example, a user developing educational stackware might want to create properties, or even types, to use in scripts. The stackware development environment could automatically generate a unique prefix for the user, based on the serial number of the development tool, and then append the user generated property or type name. This ensures that if that user's scripts and data are combined in a container with other information generated by other users, no naming conflicts can occur. Note that globally unique names are not limited to property and type descriptions. Any object can be given a unique name using exactly the same mechanism, and such object names may be useful in some applications. Note also that objects can be given short names that are only locally unique, as in the RIFF TOC. These would be a different type than Globally Unique Names. Recall that type and property descriptions are objects as well. Since types and properties need to have globally unique names, so that applications can recognize them, type and property descriptions will typically have a globally unique name value. In many cases, this may be the only contents of a type or property description object. In some situations, however, we may wish to put more information into a description. Here are some examples of useful information that can be attached to types or properties: Base types. As previously mentioned, base types allow inheritance of semantics from other existing types for composition into more complex types. Such base type information is intended to include uses such as encryption, compression, I/O redirection, etc. Encoding information. A type definition may indicate the default encoding of its values. Typically, all of the values with the same basic format in a container will have the same encoding, so this new subtype can be shared by all these values. In this case the encoding can be indicated directly in the type description for the format. If values with the same basic format but multiple encodings exist in the same container, a more complex solution is required. In this case, a subtype may be created just to record the encoding. Such a subtype will typically not need a globally unique name. Compression information. In addition to the compression technique, typically recorded via a base type, the type can record compression parameters, the codebook used (if applicable), etc. As with encoding information, a type that exists just to record compression information typically will not need a globally unique name. It will refer to the underlying format type and the compression technique, both of which will have globally unique names. A template or grammar for a type. This allows applications that have never seen this type before to parse values of that type and potentially get some useful information out of them. Examples of description mechanisms that could be used in this way are ASN.1 and SGML. The more general type will be indicated as the super-type. For example, a given SGML DTD as a type will have a specific SGML definition of the DTD. The super-type of this type would be SGML itself, which defines the basic encoding conventions. Method descriptions for a type. A type could have properties that provide method definitions. Providing methods in the container would allow fully encapsulated use of values. D. Format Definition The concrete format of the table of contents (TOC) of a Container Manager container will now be described. The TOC consists of a sequence of entries. Each entry corresponds to a single segment of a value of some object. TOC entries are sorted by object ID, and within a single object they are sorted by property ID. Thus all the entries for a given object are contiguous in the TOC, and all the entries for a given property are contiguous within the object. Also, an object can be found within the TOC, or a property can be found within an object, by binary search. If an object ID or a property is not defined, we can quickly determine that it is not defined. Thus, each object in the container is represented in the TOC by a sequence of entries, one for each segment of a value of the object. The Container Manager has no way to represent an object without at least one value. Since each TOC entry defines a value, we know immediately that it must indicate the object ID, property, type, and data of the value. In addition, it indicates the generation number of the value in order to allow applications to check consistency between different properties. The TOC entry may also contain bookkeeping information for the value. The object ID field in a TOC entry identifies the object that this value is part of. The property field identifies the value's property by the object ID of a property description. The type field indicates the value's type by the object ID of a type description. The entry indicates the value's data by the offset and length of the sequence of bytes representing the value. The offset is a 0 origin byte offset from the beginning of the container. The length is a byte count, and may be 0, indicating a 0 length value. If the data is four bytes long or less, it may be included directly in the TOC as an immediate value, rather than being referenced by offset and length. A TOC entry could simply be defined by putting all the information above in a record. This record would be relatively large, however, and would be very likely to contain redundant and/or unused information. The presently described Container Manager embodiment therefore uses an approach in which each TOC entry contains only the information that is new or different compared with the previous TOC entry. This results in a TOC that is organized as a stream rather than a table, and is parsed as it is read in. The actual format of the TOC is not important for an understanding of the invention. Note that every TOC contains a standard object that is used to describe the TOC itself. In particular, it is object ID 1, so the TOC entries for the TOC itself always come at the beginning of the TOC. (Object ID 0 is never used). Additional TOC properties can be useful. For example, an index to speed access to the entries by ID could be attached to the TOC through another property. Potentially several such indexes, using different formats, could be attached. Object IDs other than IDs of standard objects are generated by sequentially incrementing a counter from 0.times.00010000. Object IDs are never reused in later generations of a container if an object is deleted. The last ID number generated is kept as a property of object #1 to allow generating further IDs without reuse. II. IMPLEMENTATION A. Hardware The Container Manager of the present embodiment is implemented entirely as software instructions and data, to be executed on general purpose computer hardware. No specific hardware platform is required. For completeness, however, FIG. 3 illustrates a typical hardware computer system platform on which the Container Manager might run. The computer system of FIG. 3 comprises a CPU 302, main memory 304, which may be volatile, an I/O subsystem 306, and a display 308, all coupled to a CPU bus 310. The I/O subsystem 306 communicates with peripheral devices including persistent storage devices, such as a disk 312. In typical operation, an application program, together with at least those Container Manager routines which are used by the application program, are retrieved from the disk 312 into main memory 304 for execution by the CPU 302. All of the data structures described below are also created in the main memory 304, in the sense that memory space is allocated for the information to be contained in the data structures, and all of the software routines which read or write to such memory locations do so according to some known definition of fields. In addition, pointers are written to certain of the allocated main memory storage areas, which pointers refer to other structures in memory in a known manner which is defined by the data structure. Thus a data structure, as used herein, is an abstract description of the organization of data in main memory 304; when the data structure is "created" in main memory 304, this description is imposed on regions of main memory 304 so that specific items of information can be found and/or interpreted according to the data structure. The term "pointer", as used herein, is a well-known shorthand for physical signals which are stored as charge, current or voltage levels in the memory cells which implement the main memory 304. These signals "identify" an item of information memory 304 in the sense that, when applied to the memory 304 as an address (either directly or via an address translation mechanism), they cause the memory 304 to read out data from the item pointed to or identified by such signals. Also, it will be understood that even though different types of computer systems implement schemes such as caching and virtual memory, in which some of the data may not actually be located in main memory 304 itself at various times, these mechanisms are transparent to the Container Manager embodiment described herein. Thus, the data is referred to herein as being located "in" main memory 304, even if it is actually, transparently, located elsewhere. B. In-Memory Data Structures FIG. 4 is an overall block diagram of the major data structures which are created in main memory 304 during the pendency of a "session". Data block 402 is a "session global data" block containing all of the session-wide data for a given Container Manager session. There is no static global data in the code. All open containers are tied to the session on a doubly linked list whose head and tail pointers are contained in the session global data. The root of a metahandler table 404 (described below) is kept here as well along with the session handler pointers for malloc, free, and error reporting. Containers are identified in the session global data block 402 by a pointer to the container's Container Control Block (CCB). Each time a container is opened with CMOpenContainer() or CMOpenNewContainer(), a new container control block 406 is created. The pointer to the container control block 406 is what is returned to the user as a container "refNum" (reference number). There are five primary data structures tied to the container. Four are shown in the diagram and are discussed later. The fifth is the "touched chain", used for recording updates. The "touched chain" is not important for an understanding of the invention and is not described herein. The four shown main data structures pointed to from the container control block 406 are the list 408 of deleted values (TOCValueHdr(s)), a list 410 of embedded container pointers, the global name symbol table 412, and a pointer to a TOC 414 control block 416. The table of contents (TOC) 414 is the set of related data structures that organize objects by object IDs. The requirement that objects be kept in sorted order (sorted by object ID) puts certain constraints on its organization. Further, the fact that the IDs are generated sequentially in new containers also must taken into account (for example, binary trees would not be a good choice in such a situation). The method used in the Container Manager of the embodiment described herein is an index table algorithm. It is somewhat memory intensive but allows objects to be accessed linearly in time and keeps the objects in the required sorted order. The index tables correspond to "powers" of a chosen index table size. For example, if the table size is 256 and the maximum ID is 0.times.FFFFFFFF (32-bits unsigned on MC68XXX machines) the access depth will be 4 for any ID. To illustrate this, if we had ID 0.times.00123456 we would have 4 indices: 0.times.00, 0.times.12, 0.times.34, and 0.times.56. Four index tables would exist each corresponding to the indices 00 to 0.times.FF, i.e., mod the size of an index table. Each index is used to index into its corresponding index table. Thus, in this example, the first table would have its 0.times.00'th entry pointing to the next table. That next table would have its 0.times.12 entry pointing to the third table. The third table would have its 0.times.34 entry pointing to the last table. The 0.times.56 entry in the last table would point to the actual object with ID 0.times.00123456. Continuing with this example, if every ID possible were represented, then there would still be only one top level table. But there would be 256 second level tables corresponding to the 256 level-one indices. Each of those 256 level-two tables would have pointers to 256 level three tables and so on down to level 4. Fortunately, new containers are generated with sequential IDs so that only the minimum number of tables is required. But if a new nonsequential number is needed the requisite new tables are generated as needed to go from the top level table to the lowest level table. The routines that maintain this data structure are generalized to support any size table (within limits). There are trade-offs between table size and access time, which are apparent to a person of ordinary skill. Because of this generalization, a TOC has associated with it all the variables that are needed to manipulate the index tables. This is kept in TOC control block 416, pointed to from the container control block. The TOC control block 416 is to TOC object access, what the container control block 406 is to the entire container. The TOC control block points to another data structure not shown here to keep the drawings simple. It is a set of three head/tail list pointers to doubly linked lists of the TOCObject(s). The three lists are for all the objects, property objects, and type objects in the container. Thus the type and property lists are subsets of the object list. These lists are only just for the CMGetNextxxx() routines. These lists are kept as part of the TOC since, there can be only one TOC and one of these list sets. Note that for updating, there can be multiple containers using the same TOC, so putting these data structures here is the most convenient way to deal with them during updating. Note, that since there can be multiple users of a TOC, a TOC requires a "use count" to prevent premature release of the TOC. The lowest level of the TOC index tables 418 contain pointers to the container objects themselves instead of to other index tables. These objects are TOCObjects 420. The TOC entries for an object are linked off of their TOCObject. TOCObjects are returned to the user as object refNums (CMObject, CMType, and CMProperty). The properties, TOCProperties 422, for an TOCObject are contained on a doubly linked list off the TOCObject. The values for each property are on a doubly linked list of value headers, TOCValueHdrs 424, off of each TOCProperty. Finally, a specific real (as opposed to dynamic) value, such as one of the TOCValues 426, is linked to its TOCValueHdr. The reason the values are linked to a value header is because of continued (multi-segment) values. A multi-segment value can have more than one value entry. Hence the chain. Also, it is pointers to value headers that are returned to the user as value refNums (CMValue). As used herein, a "header" for an item or items of information is a logical collection of information which applies generally to the item or items. The header need not be physically located in a contiguous region of memory, nor must it be contiguous with any of the items themselves. Each TOCValue 426 can be either immediate, non-immediate, or a global name. Immediate values contain 1, 2, or 4-byte value data encoded directly in the entry. Non-immediates contain a container offset to the value data and its length. Non-immediates can also represent dynamic values (discussed below). Global name values, such as 428, are pointers to global name symbol table entries (discussed shortly) and once the value data has been written to the container, the container offset. Note the diagram shows, in addition to the doubly linked list structures, a pointer for each TOCValue back to its value header. Similarly, each TOCValueHdr has a pointer back to its TOCProperty. Finally, each TOCProperty has a pointer back to its TOCObject. Not shown is a pointer from each TOCObject and each TOCValueHdr back to its container control block. The result is that anything can be accessed from almost anywhere and in any direction. When a CMRegisterType() or CMRegisterProperty() is done, a check must be made to see if the specified global name already exists. For this, a simple binary tree symbol table 412 is used. Since a global name is itself a type or property object value, there is also a pointer from a TOCValue to the name in the global name symbol table. Each global name symbol table is unique to its container. Hence the container control block has the root to its tree of global names. Whenever a container is opened a set of predefined global names is generated. Basically the equivalent of CMRegisterType() and CMRegisterProperty() is done but the object IDs are standard rather than user IDs. Note, global names are not written to the container at the time they are created. Instead they are kept in the global name symbol table. When a container that was open for writing is closed, the global name symbol table is "walked" and all user defined names written to the container. At that point the TOCValues associated with global names are set with the container offsets for those names. This is done using the back pointer from each global name entry to its TOCValue. The TOC is then written followed by the label. Since the TOC is written after the global names, all the global name offsets will be set by that time. Thus everything is correct when the container is to be read. The Container Manager of the presently described embodiment supports embedded containers. Embedded containers are treated just about like any other. The main difference is that they require a special handler that writes or reads (CMWriteValueData() and CMReadValueData()) to a value that belongs to the parent container. The handlers keeps track of offsets with the value that it is treating as a container. The effect is to write or read a parent value as if it was a container. All the data for the parent value is created as a container, complete with its own TOC and label. The offsets in the TOC are relative to the start of the value, offset 0, just as in the non-embedded case. This means that a parent value could be read to copy the container as is. Aside from the special handlers, most of the other stuff needed to open and close a container is independent of whether it is embedded or not. There are a few wrinkles, however. First, a container can have any number of embedded containers open at the same time. Each of those could also, and so on. The result is essentially a tree of open embedded containers. Since the data for a parent value is its embedded container, then if there are any more deeply embedded containers, they would also be part of the parent's value. This gets very confusing if you try to think of it more than two levels deep. In all cases, when a parent is closed, we want to close all of its descendants. The embedded container list 410 pointed to from the container control block is used so that a parent container can keep track of all of its immediate descendants. Each entry in the list is simply a pointer to a descendent container control block. At open time an entry for the embedded container is created in its parent embedded container list. At close time CMCloseContainer() will go through its list of embedded containers (i.e., the list of its immediate descendants) and recursively call CMCloseContainer() to close those. The net result is the desired one of closing all the descendants of the parent in the tree of embedded containers. An embedded container being closed is responsible from removing itself from its parent's embedded container list so that it won't be "seen" again if a parent further up the tree is closed. Note, the functionality of embedded containers can also be done using dynamic values. However, the Container Manager, not being aware of this use of dynamic values, will not maintain the embedded containers list for it. Thus each dynamic value corresponding to an embedded container must be explicitly "closed" using CMReleaseValue(). When CMDeleteObject() is called, an object is to be deleted. When CMDeleteValue() is called, a value for a property of an object is to be deleted. As mentioned above, the refNums for objects (CMObject, CMProperty, and CMValue) are pointers to TOCObjects. Values (CMValue) are pointers to TOCValueHdrs. Thus we cannot truly delete the items (i.e., free their memory) these point to because there is no reliable way to verify that the pointers are valid. The solution adopted is to put all deleted objects and values on a list of deleted items associated with the container. There are two lists: list 430 for objects pointed to from the TOC control block, and list 408 for deleted values pointed to from the container control block itself. Note, since object refNums are TOCObjects, and value refNums are TOCValueHdrs, the only thing needed on these lists are those data structures. TOCProperties and TOCValues can be freed. The TOCObjects and TOCValues are flagged as "deleted". Whenever any object or value is passed to the API it is checked for the flag. It is an error to use a deleted item. CMSetMetaHandler() is called by the user to record metahandler/type name associations. These are maintained in binary tree symbol table 404. The root of this tree is a "global" in the session data. It is not tied to any one container. When a container is opened, a type name is passed. This is used to look it up in the metahandler symbol table. This yields a metahandler function address which in turn is used to get actual handler routine addresses. The following C-language struct defines the layout of all in-memory TOCObjects. The objects are accessed by their object ID.
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
struct TOCObject { /* Layout of a TOC object: */
CMObjectID objectID;
/* the object's ID (keep first for debugging) */
ListHdr propertyList;
/* list of object property entries */
struct Container *container;
/* ptr to "owning" container control block */
struct TOCObject *nextObject;
/* chain to next object by increasing ID */
struct TOCObject *prevObject;
/* chain to previous object by decreasing ID */
struct TOCObject *nextTypeProperty;
/* chain of next type/property by increasing ID */
struct TOCObject *prevTypeProperty;
/* chain of previous type/property by decr. ID */
unsigned short objectFlags;
/* info flags about the object */
CMRefCon objectRefCon;
/* user's object refCon */
unsigned long useCount;
/* count of nbr of times "used" */
struct TOCObject *nextTouchedObject;
/* link to next touched object */
ListHdr touchedList;
/* values/properties touched IN this object *
};
typedef struct TOCObject TOCObject, *TOCObjectPtr;
__________________________________________________________________________
The following object flags are defined:
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
#define UndefinedObject
0x0001U
/*
1 ==> object created but undefined*/
#define ObjectObject
0x0002U
/* ==> object is a base object */
#define PropertyObject
0x0004U
/* ==> object is a property descriptor*/
#define TypeObject
0x0008U
/* ==> object is a type descriptor*/
#define DeletedObject
0x0010U
/* ==> object has been deleted */
#define DynamicValuesObject
0x0800U
/* ==> object "owns" dynamic values*/
#define TouchedObject
0x1000U
/* ==> object has been "touched" */
#define ProtectedObject
0x2000U
/* ==> object is locked/protected */
#define LinkedObject
0x4000U
/* ==> object linked to master lsts*/
#define UndefObjectCounted
0x8000U
/* ==> object counted as undefined */
__________________________________________________________________________
Note that the properties 502 and 504 in the object of FIG. 5 are described by property descriptors which are themselves objects which follow the above layout. The layout of each of the properties 502, 504 and 542 is defined as follows:
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
struct TOCProperty {
/* Layout of a TOC object property: */
ListLinks propertyLinks;
/* links to next/prev property (must be first) */
TOCObjectPtr theObject;
/* ptr to "owning" object */
CMObjectID propertyID;
/* the property's ID */
ListHdr valueHdrList;
/* list of the property's values */
};
typedef struct TOCProperty TOCProperty, *TOCPropertyPtr;
__________________________________________________________________________
Types, too, are described using objects of the TOCObject form set out above. The structures of TOCValueHdrs 424 and TOCValues are set forth hereinafter. As previously mentioned, the Container Manager routines CMNewValue () and CMUseValue () create a dynamic value chain for each type that has a "UseValue" and a "NewValue" handler. FIG. 5 illustrates the structure of in-memory objects which are created by the dynamic value mechanism. Referring to FIG. 5, it is assumed that one of the TOCObjects 420 (FIG. 4) has a series of properties 502, 504, and so on (corresponding to 422 in FIG. 4). It is assumed further that property 502 has two values associated with it, indicated by value headers 506 and 508 (424 in FIG. 4). These values are of different types, as will be seen from the fact that different dynamic value chains are created for these values. Property 504 also has values associated with it, but these are shown only in the abbreviated form of an arrow 510. Associated with real value header 506 is a segment 512 of real value data, and associated with value header 508 are two segments 514 and 516 of real value data. If the values for the property 502 were not of types which require creation of dynamic values, then the actual data of the values would be stored in segments 512, 514 and 516. Since the type of these values call for dynamic value creation, however, the data stored in real value data segments 512, 514 and 516 may instead be transformed versions of the actual data and/or may contain only indirection information. The value header structure includes a pointer to the top dynamic value header 518 in a chain of dynamic value headers 518, 520 and 522. Each of the dynamic value headers 518, 520 and 522 have a format which is identical to the value header (also called a "real value header") 506, except that the field in real value header 506 which pointed to dynamic value header 518, is redefined in dynamic value header 518 to point to a set of dynamic value header extensions 524. The extensions 524 include an entry which points to the base value of the dynamic value header 518, which in the case of this chain, merely points to the second dynamic value header 520 on the chain. Dynamic value header 520 in turn points to its own dynamic value header extensions 526, which in turn points, in the base value field, to dynamic value header 522. Dynamic value header 522 also points to its dynamic value header extensions 528. But since dynamic value header 522 is at the bottom of the chain, its base value is the real value data stored in segment 512. Thus, the "base value" field of extensions 528 points back to the real value header 506. Recall that the purpose for creating a chain of dynamic value headers 518, 520 and 522 is to implement a complex value type which transparently handles data transformations and redirections. Each of the dynamic value headers 518, 520 and 522 corresponds to a respective one of the types on the tree defining the complex type of the value headed by real value header 506. Thus, each of the dynamic value headers 518, 520 and 522 maintains its own vector of value handlers to be used when a higher level caller desires to invoke a value operation. These dynamic value vectors are illustrated in FIG. 5 as 530, 532 and 534, pointed to respectively by extensions 524, 526 and 528. The dynamic value vectors 530, 532 and 534 contain a series of pointers to the respective value handlers to be called. The pointers are in predefined locations in the vector; for example, the third entry in each vector contains the pointer to the WriteValueData handler to be called for a value data write operation. The value header 508 in FIG. 5 is for a value whose type spawned only a single dynamic value header 536. Thus, the value header 508 points to dynamic value header 536, which in turn points to its extensions 538, which in turn points both to a dynamic value vector 540 and, for the base value, back to the value header 508. When a real value spawns dynamic values, a special dynamic value property 542 is created only to contain the dynamic value headers. Only the bottom most layer of each dynamic value chain (the layer whose base value is the "real" value) is on the dynamic property chain. All higher layers are not part of the dynamic property chain. The dynamic value property chain is used to simplify deletion of dynamic values, for example when the container is to be closed. Dynamic value headers never have value segment lists. No data is ever written to a dynamic value because these headers are removed when the value is released using a CMReleaseValue(). If there is any data, it must be associated with a "real" value--the real value associated with a dynamic value or some place else. In each value header there is a pointer (a union called "dynValueData" with alternative fields called "dynValue" and "extensions") that contains three possible values: 1. dynValueData is NULL for "real" value headers that don't have a corresponding dynamic value. 2. dynValueData.dynValue is a pointer to the top-most layer if it is a "real" value that does have a corresponding dynamic value header. 3. dynValueData.extensions is a pointer to the extensions if it is itself a dynamic value header. The value header's flags determine how to interpret the pointer. When a dynamic value is spawned by CMNewValue() or CMUseValue(), the pointer to the top-most dynamic value header is returned as the refNum. That means whenever the user passes it to an API value routine, it will check to see if the refNum is a dynamic value. If it is, it initiates the call to the corresponding value handler using the vector in the extensions. That may cause a search up the base value chain to look for the inherited value routine. In the limit, the original API value routine is used if no handler is supplied and the "real" value in the chain is reached. That's how data could get in there. FIG. 6 illustrates the same structure as FIG. 5 using a simplified notation. This notation will make it easier to describe how dynamic values are spawned and layers created. Here "0" is object, "P" is property, "VH" real value header, "DVP" the dynamic value property, and "DVH" a dynamic value. The value segments are omitted. As previously mentioned, when a CMNewValue() or CMUseValue() is almost done, a check is made on the value's type, and all of its base types (if any) to see if it has an associated registered metahandler. If it does, it is called with a "use value" operation type to see if a "use value" handler exists for the type. If it does, the dynamic value is spawned. Thus if CMNewValue() or CMUseValue() sees any (base) type that has an associated "use value" handler, it will spawn a dynamic value. The spawning is done essentially by calling the "use value" handler. It is expected to set up a refCon to pass among the value handlers and a pointer to another metahandler. These are returned to CMNewValue() or CMUseValue() which uses newDynamicValue() to do the actual creation of the dynamic value. The extensions are initialized, the metahandler pointer saved, and the refCon is also saved. The pointer to the created dynamic value header is what CMNewValue() or CMUseValue() returns to the user as the refNum. When the user attempts to do a value operation using this refNum, a check is made that the refNum is for a dynamic value. If it is, the corresponding handler routine will be called. The vector entries are set on first use of a value operation. It may mean searching up the base value chain, but once found, the handler address is saved in the top layer vector (associated with the refNum) so the search doesn't have to be done again. Note that if the search must be done up the base value chain, then the dynamic value refNum (pointer), in addition to the handler address, must be saved. This is very much like C++ classes, where inherited methods are called and the appropriate "this" must also be passed. The "this" in this case is the refNum. Previously there was described a layered type T which was registered in the Container Manager with its two base types Layer1 and Layer2 as follows:
______________________________________
layer1 = CMRegisterType(container, "Layer1");
layer2 = CMRegisterType(container, "Layer2");
t = CMRegisterType(container, "T");
CMAddBaseType(t, layer1);
CMAddBaseType(t, layer2);
______________________________________
Internally, the t object can be represented as shown in FIG. 7 (using the notation of FIG. 6). The value data segments are shown here with the data the segment will point to in the container. For the t object 702, the global name property 704 and value 706 are created, as usual, by calling CMRegisterType(). The CMAddBaseType() calls add the base types. These are recorded as the object IDs for each base type in the order created as separate value segments 708, 710 for a special "base type" property 712 belonging to the type object 702. The value segments 708, 710 store only the Object IDs of the base types; the global name of the base types are stored as values such as 706 in respective type objects similar to 702. As mentioned above, CMNewValue() or CMUseValue() spawn dynamic values if the original type or any of its base types have an associated "use value" handler. Assume that was done for T in the above example. What happens is that CMUseVALUE0 or CM'seValue() will look at its type object (t here) to see if the base type property is present. If it is, it will follow each type "down" to leaf types using a depth-first search. In the example, layer1 will be visited, then layer2, and finally the original type T itself. If the layer1 type object had base types of its own, they would be visited before using layer1 itself. Hence the depth-first search down to the leaf types. For each type processed, if it has a "use value" handler of its own, it will be called to get a refCon and value handler metahandler. These are passed to newDynamicValue() to create a dynamic value for the original "real" value. newDynamicValue() always returns its refNum that will be the dynamic value it created. The first layer will create the dynamic value property and put the dynamic value header on its value header list. All further calls to newDynamicValue() will pass the most recent refNum returned from it. newDynamicValue() then chains these off the first dynamic value header. This produces the desired layering result. The following C-language code defines TOCValue, the format of one of the TOCValue data segments 426 (FIG. 4) or 512, 514, 516 (FIG. 5):
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
union TOCValueBytes { /* Layout of value/Length fields; */
struct { /* value if not immediate(not explicitly
here): */
CM.sub.-- ULONG value; /* offset to value if not immediate */
CM.sub.-- ULONG valueLen; /* value length if not immediate */
} not Imm;
struct { /* value for a global name */
CM.sub.-- ULONG offset; /* offset to value in container */
struct GlobalName *globalNameSymbol;
/* ptr value for a global name (in memory) */
} globalName;
union { /* actual value if immediate placed here: */
CM.sub.-- UCHAR ucharsValue[2*sizeof(CM.sub.-- ULONG)];
/* value if immediate unsigned char(s) */
CM.sub.-- ULONG ulongValue;
/* value if immediate unsigned long */
CM.sub.-- USHORT ushortValue;
/* value if immediate unsigned short */
CM.sub.-- UCHAR ubyteValue;
/* value if immediate unsigned byte */
void *ptrValue; /* value if immediate pointer */
} imm;
};
typedef union TOCValueBytes TOCValueBytes, *TOCVatueBytesPtr;
struct TOCValue { /* Layout of a TOC type's value: */
ListLinks valueLinks; /* links to next/prev value (must be first)
*/
struct TOCValueHdr *theValueHdr;
/* ptr back to ValueHdr "owning" this value
*/
ContainerPtr container; /* ptr to "owning" container control block */
CMValueFlags flags; /* flags */
TOCValueBytes value; /* value and length or immediate value */
unsigned long LogicalOffset;
/* original (unedited) logical offset */
};
typedef struct TOCValue TOCValue, *TOCValuePtr;
enum ConstValueType { /* Data types used to copy data into
TOCValue's: */
Value.sub.-- NotImm, /* not immediate ==> value and valueLen */
Value.sub.-- GlobalName, /* global name ptr ==> in-memory str ptr */
Value.sub.-- Imm.sub.-- Chars,
/* immediate, chars ==> ucharsValue */
Value.sub.-- Imm.sub.-- Long,
/* immediate, long ==> ulongValue */
Value.sub.-- Imm.sub.-- Short,
/* immediate, short ==> ushortValue */
Value.sub.-- Imm.sub.-- Byte
/* immediate, byte ==> ushortValue */
};
typedef enum ConstValueType ConstValueType;
__________________________________________________________________________
The following C-language code defines the format of a Value Header (both real value headers and dynamic value headers).
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
struct TOCValueHdr {
/* Layout of a TOC property type: */
ListLinks valueHdrLinks;
/* Links to next/prev value hdr (must be first) */
struct TOCProperty *theProperty;
/* ptr to "owning" property */
ListHdr valueList; /* list of actual values */
CMObjectID
typeID; /* the value's type ID */
ContainerPtr
container; /* ptr to "owning" container control block */
unsigned long
size; /* total current size of the value data */
unsigned long
logicalSize;
/* original (unupdated) size of the value data */
unsigned short valueFlags;
/* flags indicating stuff about the value */
CMGeneration generation;
/* generation number */
unsigned long useCount;
/* count of nbr of times "used" */
CMRefCon valueRefCon;
/* user's value refCon */
TouchedListEntryPtr touch;
/* ptr to updating touched list entry */
union { /* this field depends on kind of value hdr: */
struct TOCValueHdr *dynValue;
/* ptr to dynamic value hdr or NULL */
struct DynValueHdrExt *extensions;
/* ptr to dynamic value hdr extensions */
} dynValueData; /* [extensions onty when it's a dynamic value] */
union { /* references recorded by this value */
TOCObjectPtr refDataObject;
/* associated ref object; NULL if no refs */
ListHdrPtr refShadowList;
/* or shadow list of the actual data */
} references; /* (refShadowList only in recording value) */
};
typedef struct TOCValueHdr TOCValueHdr, *TOCValueHdrPtr;
struct DynValueHdrExt {
/* Extensions to TOCValueHdr for a dynamic value: */
TOCValueHdrPtr baseValue;
/* ptr to base value of this dynamic value */
DynamicValueVector dynValueVector;
/* dynamic value handler vector */
CMMetaHandler metaHandler;
/* metahandler to get handler addresses*/
};
typedef struct DynValueHdrExt DynValueHdrExt, *DynValueHdrExtPtr;
/* Some of following valueFlags echo the flags field a TOCValue entry.
That is because a
CMValue "refNum" that an API user is given and in turn given back to us
is a pointer to a
TOCValueHdr. It is sometimes more convenient therefore to check the kind
of value we have
by looking at the header then "going out" to the value. In all but
continued values there
is only one TOCValue entry on the valueList anyway. So echoing is more
efficient then
always going after the tail or head (they're the of a valueList just to
see the flags and
the kind of value. */
#define ValueDeleted
0x0001U
/* valueFlags: 1 ==> deleted value */
#define ValueContinued
0x0002U
/* ==> continued */
#define ValueGlobal
0x0004U
/* ==> global name */
#define ValueImmediate
0x0008U
/* ==> immediate value */
#define ValueOffPropChain
0x0800U
/* ==> dynamic value off prop chain */
#define ValueDynamic
0x1000U
/* ==> dynamic value */
#define ValueUndeletable
0x2000U
/* ==> can't be deleted */
#define ValueProtected
0x4000U
/* ==> locked/protected value */
#define ValueDefined
0x8000U
/* ==> fully defined (in read only) */
/* ValueUndeleteable and ValueProtected are levels of protection bits.
*/
/* In order to make dealing with dynamic values easier, the following
macros are provided.
IsDynamicValue(v) is a more self-documented test to see if a TOCValueHdr
is indeed a
dynamic value, while DYNEXTENSIONS(v) allows simpler notational access to
a dynamic value
header's extension fields.
#define IsDynamicValue(v) ((((TOCValueHdrPtr)(v))->valueFlags &
ValueDynamic) != 0)
#define DYNEXTENSIONS(V)
/* to make access to extensions a "little" easier*.backs
lash.
(((TOCValueHdrPtr)(v))->dynValueData.extensions)
/* The dynamic value vectors are defined as follows */
struct DynamicValueVectorEntries {
/* Layout of a dynamic value vector entry: */
CMHandlerAddr handler;
/* the handler address */
CMValue thisValue;
/* the handler's value (C++ "this") */
Boolean active; /* true ==> handler is in calling chain */
};
typedef struct DynamicValueVectorEntries DynamicValueVectorEntries,
*DynamicValueVectorEntriesPtr;
struct DynamicValueVector {
DynamicValueVectorEntries cmGetValueSize;
DynamicValueVectorEntries cmReadValueData;
DynamicValueVectorEntries cmWriteValueData;
DynamicValueVectorEntries cmInsertValueData;
DynamicValueVectorEntries cmDeteteValueData;
DynamicValueVectorEntries cmGetValueInfo;
DynamicValueVectorEntries cmSetValueType;
DynamicValueVectorEntries cmSetValueGen;
DynamicValueVectorEntries cmReleaseValue;
};
typedef struct DynamicValueVector DynamicValueVector;
__________________________________________________________________________
When a handler is called, it is expected to do its operations on the "base value" of the value passed to it. It gets its base value using CMGetBaseValue (). However, we don't want to allow recursive use of the API for the same value. That would call the handler again and we would be in an infinite loop. Thus the active switch is provided to check for this so we can report an error. The dynamic value vector is initialized with each handler address thisValue set to NULL. On first use we use the metahandler which was returned from the "use value" handler (the metahandler address is saved in the value header extensions) to get the proper value handler address. It is saved in the handler field of the vector entry. Remember we may have to search up through a dynamic value chain to find an "inherited" value handler operation. Thus the handler used may correspond to a different dynamic value. We must therefore save the dynamic value refNum along with the handler address (in the thisValue). It is similar to the C++ "this" pointer for the value handler operation). Of course, in the simplest case, where the handler is provided for the original value, the thisValue will point to its own dynamic value header. At the other extreme no handlers are supplied for the operation and we end up using the "real" value that spawned the dynamic value(s). In that case the handler pointer in the vector entry remains NULL and the thisValue will be the "real" value refNum. With no handler we use the actual API routine to process the real value. As with standard handlers, to simplify this description, some macros are defined for calling the dynamic value handlers. These macros will require the following typedefs as casts to convert the generic handler typedef, HandlerAddr (the type used to store the addresses in the vector), to the actual function type:
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
CMSize (*TcmGetValueSize)(CMValue value);
typedef CMSize (*TcmReadValueData)(CMValue value, CMPtr buffer, CMCount
offset, CMSize
maxSize);
typedef void (*TcmWriteValueData)(CMValue value, CMPtr buffer, CMCount
offset, CMSize
size);
typedef void (*TcmInsertValueData)(CMValue value, CMPtr buffer, CMCount
offset, CMSize
size);
typedef void (*TcmDeleteValueData)(CMValue value, CMCount offset, CMSize
size);
typedef void (*TcmGetValueInfo)(CMValue value, CMContainer *container,
CMObject *object,
CMProperty *property, CMType *type, CMGeneration *generation);
typedef void (*TcmSetValueType)(CMValue value, CMType type);
typedef void (*TcmSetValueGen)(CMValue value, CMGeneration generation);
typedef void (*TcmReleaseValue)(CMValue);
__________________________________________________________________________
Here now are the macros used to do the calls using the vector.
__________________________________________________________________________
.COPYRGT. 1992 Apple Computer, Inc.
__________________________________________________________________________
#define CMDynGetValueSize(v)
(*(TcmGetValueSize)DYNEXTENSIONS(v)->dynValueVector.cmGetValueSize.handler
)((CMValue)(v))
#define CMDynReadValueData(v, b, x, m)
(*(TcmReadValueData)DYNEXTENSIONS(v)->dynValueVector.cmReadValueData.handl
er)((CMValue)(v),
(CMPtr)(b), (CMCount)(x), (CMSize)(m))
#define CMDynWriteValueData(v, b, x, n)
(*(TcmWriteValueData)DYNEXTENSIONS(v)->dynValueVector.cmWriteValueData.han
dler)((CMValue)(v
), (CMPtr)(b), (CMCount)(x), (CMSize)(n))
#define CMDynInsertValueData(v, b, x, n)
(*(TcmInsertValueData)DYNEXTENSIONS(v)->dynValueVector.cmInsertValueData.h
andler)((CMValue)
(v), (CMPtr)(b), (CMCount)(x), (CMSize)(n))
#define CMDynDeleteValueData(v, x, n)
(*(TcmDeleteValueData)DYNEXTENSIONS(v)->dynValueVector.cmDeleteValueData.h
andler)((CMValue)
(v), (CMCount)(x), (CMSize)(n))
#define CMDynGetValueInfo(v,c,obj,p,t, g)
(*(TcmGetValueInfo)DYNEXTENSIONS(v)->dynValueVector.cmGetValueInfo.handler
)((CMValue)(v),
(CMContainer*)(c), (CMObject*)(obj), (CMProperty*)(p), (CMType*)(t),
(CMGeneration*)(g))
#define CMDynSetValueType(v, t)
(*(TcmSetValueType)DYNEXTENSIONS(v)->dynValueVector.cmSetValueType.handler
)((CMValue)(v),
(CMType)(t))
#define CMDynSetValueGen(v, g)
(*(TcmSetValueGen)DYNEXTENSIONS(v)->dynValueVector.cmSetValueGen.handler)(
(CMValue)(v),
(CMGeneration)(g))
#define CMDynReleaseValue(v)
(*(TcmReleaseValue)DYNEXTENSIONS(v)->dynValueVector.cmReleaseValue.handler
)((CMValue)(v))
__________________________________________________________________________
As mentioned earlier, each corresponding API value operation must check to see if it has a dynamic value and call the corresponding handler which does the operation. It must get the proper address on first use. It must set switches to mark the handler as active. It must also set a switch to allow CMGetBaseValue () calls which are only allowed from dynamic value handlers. Thus the algorithm for calling a value handler looks something like this (ignoring all errors for the moment):
______________________________________
.COPYRGT. 1992 Apple Computer, Inc.
______________________________________
if (IsDynamicValue(v)) {
v = GetDynHandlerAddress(v, h, g);
if (IsDynamicValue(v)) {
SignalDynHandlerInUse(v, h);
AllowCMGetBaseValue(container);
Call the proper dynamic value handler with one of the
above macros definitions. The macro will pass the
appropriate value corresponding to a possibly in | ||||||
