Method for mapping types stored in a model in an object-oriented repository to language constructs for A C binding for the repository5889992Abstract The method of the present invention is useful in a computer system having a user interface, a CPU, a memory, at least one disk drive, and an object-oriented repository, a program operating in the computer system for accessing the object-oriented repository. The program executes a method for mapping types in a model stored in the repository to language constructs for a C binding to the repository. The method first processes each type in the model, then the program processes each data type in the model. Function declarations and C to C++ wrapper functions are generated for each type and data type. Claims What is claimed is: Description A portion of the disclosure of this patent document contains material that is subject to copyright protection. The copyright owner has no objection to the facsimile reproduction by anyone of the patent disclosure, as it appears in the Patent and Trademark Office patent files or records, but otherwise reserves all copyright rights whatsoever.
______________________________________
typedef int udt.sub.-- boolean
#define TRUE 1
#define FALSE 0
typedef unsigned char udt.sub.-- char;
typedef long udt.sub.-- integer;
typedef double udt.sub.-- real;
typedef char * udt.sub.-- string;
typedef const char * udt.sub.-- cstring
______________________________________
The UREP data type name is not used for the typedef name, because it is still necessary to provide access to objects of these types using the UREP data type name. For example, the format operation may need to be called on instance of a data type. When the data type has a subtype defined in the model, then the same typedef is used for that type. For example, UrepId is a subtype of UrepString. In the C binding, the udt.sub.-- string typedef would be used. The underlying C++ implementation will handle the length restriction. The actual definition of the typedefs may change on different platforms for the C data types so that the memory layout for the data type is equivalent on all platforms. Model Types The C typedef hierarchy for UREP is extended by typedefs for the types in a model. The naming rules of C++ Binding determine the name of the type. The type name is also used as a prefix for many of the generated C binding names. Whenever <type name> is used in this document, it is referring to the name created by following the naming rules. The name of the initializer function for the object is init<type name>. The function must be called to initialize the object for the C Binding before other UREP functions are called using the object as a parameter. These functions are equivalent to the C++ constructors defined in the C++ API. The prefix of "construct" was not used because it might cause confusion with the construct operation defined for the persistent types. Each persistent type defined in the model has three object initializer functions generated for it. The initializer functions initialize an object that is not linked to any instance of a persistent object. The C function declarations for the first two functions are in the following format:
______________________________________
<type name>
initd<type name>();
<type name>
init<type name>
(UREP urep
);
______________________________________
The first initializer function uses the globalurep instance to initialize the object, while the second function uses the UREP instance passed as a parameter for the same purpose. The third initializer function has an object parameter for an object of the same type. The format for the C declaration is:
______________________________________
<type name>
copy<type name>
(<type name> copyObject
,udt.sub.-- boolean restricted
);
______________________________________
The return value of this function is a copy of the object copyobject. If copyobject was linked to a persistent object in the repository, then the new object is linked to the same object. If restricted is TRUE, then the returned object is restricted to type <type name>. This is useful in those cases where an operation is overridden in one or more subtypes, but the tool needs to call the operation on type <type name>, not on any of its subtypes. For example, the UREP type UrepMetaClass will have the following initializer functions generated for it:
______________________________________
UrepMetaClass
initdUrepMetaClass();
UrepMetaClass
initUrepMetaClass
(UREP urep
);
UrepMetaClass
copyUrepMetaClass
(UrepMetaClass copyObject
,udt.sub.-- boolean restricted);
______________________________________
For example, to create an object of type UrepNamedVersionedObject: UrepNamedVersionedobject NVO=initUrepNamedVersionedobject(globalUrep); In this example, "globalurep" is the global instance of the UREP type, and was used to open the repository prior to this code. At this point in the code, the variable NVO is not actually linked to any object in the repository. Any attempt to use an operation defined for the UrepNamedVersionedObject type using the NVO object will return an unlinked object error. Transient types have two or three object initializer functions defined for each type depending on whether the object represents a basic C data type or not. There is a base initializer function, a copy initializer function, and an initializer function that accepts a basic C data type to provide a conversion to a UREP data type. The latter is equivalent to the type conversion operator for the basic C type. None of the initializer functions require an instance of the UREP type as a parameter, because these are transient types. If the initializer function is successful, an instance of the corresponding transient type is created and returned. If the function is not successful, an error is pushed on the globalurep instance. The returned C object is marked internally as invalid and will cause an error if it is used as a parameter to a function. The C function declaration of the base initializer is as follows:
______________________________________
<type name>
init<type name>();
______________________________________
The result is that an instance of <type name> is constructed and returned. The copy initializer function takes an object of the same type. The C function declaration is as follows:
______________________________________
<type name>
copy<type name>
(<type name> copyObject);
______________________________________
The return value of this function is a copy of the object copyobject. For object types that represent the C types, the third initializer function accepts an argument of that C type. The C declaration of the function is as follows:
______________________________________
<type name>
convert<type name>
(<C type> value);
______________________________________
The return value is an object that is linked to an instance of <type name> that was created in the function, with the value. For example, the UrepInteger type has the following initializer functions defined for it:
______________________________________
UrepInteger
initUrepInteger();
UrepInteger
copyUrepInteger
(UrepInteger copyObject);
UrepInteger
convertUrepInteger
(udt.sub.-- integer value);
______________________________________
Enumerations The structure for the declaration of the enumerated type (items appearing between "› !" are optional) is as follows:
______________________________________
typedef enum
<enumerated type name>.sub.-- <mnemonic 1> ›= <value>!,
<enumerated type name>.sub.-- <mnemonic 2> ›= <value>!,
. . .
. . .
<enumerated type name>.sub.-- <mnemonic n> ›= <value>!
} <enumerated type name>;
______________________________________
The <enumerated type name>.sub.-- <mnemonic> for C mnemonic names ensures uniqueness of the names. For example, the declaration for UrepCardinality would be:
______________________________________
typedef enum
UrepCardinality.sub.-- sv = 0,
UrepCardinality.sub.-- mv = 1
} UrepCardinality;
______________________________________
The notification mnemonics for the error and warning messages for a model are also declared in a similar fashion. The enum name is <model prefix>.sub.-- Notification. The mnemonic names are then formed as described above. Properties For each property of a type there are several functions generated depending on the type and cardinality of the property. For properties whose type has a corresponding C data type, the <type> will be the C data type rather than object type. An additional get and set function is generated that accepts an object. The list of functions generated is for a property whose collection type is UrepArray. The functions are different for the different collection types. For example, the accessor and mutator function declarations for the maxVariantWidth property of the UrepSystemProfile type are:
______________________________________
udt.sub.-- integer
UrepSystemProfile.sub.-- get.sub.-- maxVariantWidth
(UrepSystemProfile myself);
void
UrepSystemProfile.sub.-- set.sub.-- maxVariantWidth
(UrepSystemProfile myself
,udt.sub.-- integer value);
______________________________________
The C function declarations for the top property of the UrepCompositeContext type are:
______________________________________
UrepVersionedObject
UrepCompositeContext.sub.-- get.sub.-- top
(UrepCompositeContext myself);
void
UrepCompositeContext.sub.-- set.sub.-- top
(UrepCompositeContext myself
,UrepVersionedObject value);
udt.sub.-- boolean
UrepCompositeContext.sub.-- isNull.sub.-- top
(UrepCompositeContext myself);
void
UrepCompositeContext.sub.-- flush.sub.-- top
(UrepCompositeContext myself);
______________________________________
The C function declarations for the topObjectFor property of the UrepversionedObject type are:
______________________________________
UrepArray.sub.-- UrepCompositeContext
UrepVersionedObject.sub.-- get.sub.-- topObjectFor
(UrepVersionedObject myself);
udt.sub.-- boolean
UrepVersionedObject.sub.-- contains.sub.-- topObjectFor
(UrepVersionedObject myself
,UrepCompositeContext object);
udt.sub.-- integer
UrepVersionedObject.sub.-- size.sub.-- topObjectFor
(UrepVersionedObject myself);
void
UrepVersionedObject.sub.-- set.sub.-- topObjectFor
(UrepVersionedObject myself
,UrepArray.sub.-- UrepCompositeContext value);
void
UrepVersionedObject.sub.-- add.sub.-- topObjectFor
(UrepVersionedObject myself
,UrepCompositeContext value)
void
UrepVersionedObject.sub.-- remove.sub.-- topObjectFor
(UrepVersionedcObject myself
,UrepCompositeContext object);
udt.sub.-- boolean
UrepVersionedObject.sub.-- isNull.sub.-- topObjectFor
(UrepVersionedObject myself);
void
UrepVersionedObject.sub.-- flush.sub.-- topObjectFor
(UrepVersionedObject myself);
______________________________________
A property can have several different constraints: access mode, class, and index. A property can have an access mode of read write, read only, or no access. Table 1 shows whether the accessor or mutator functions are generated for each access mode.
TABLE 1
______________________________________
Access and Mutator Function Generation
Access Mode Accessors Mutators
______________________________________
read write generated generated
read only generated not generated
no access not generated
not generated
______________________________________
Accessor functions also include the get, isNull, size, and contains functions. Mutator functions include flush, remove, add, as well as set. Changing the access mode to read only or no access should be done with care, because the properties cannot be changed or accessed, respectively, in C functions that implement operations for the type. Properties for a type can have the class constraint. In this case, the property has a single value for all instances of the type. The accessor and mutator functions for a property with the class constraint are the same as other accessor and mutator functions, except that the first parameter, myself, is replaced by an object of type UREP. The following shows the format for an accessor function for a property with the class constraint:
______________________________________
<property type> <type name>.sub.-- get.sub.-- <property name>(UREP
urep);
______________________________________
The following is the format for a mutator function for a property with the class constraint:
______________________________________
void <type name>.sub.-- set.sub.-- <property name>
(UREP urep
,<property type> value
);
______________________________________
A single-valued property of a type can also have the index constraint. This constraint indicates that the property is used as an index to retrieve instances of the type. In addition to the accessor and mutator functions a find function is generated. This function takes the form:
______________________________________
<type name> <type name>.sub.-- find.sub.-- <property name>
(<property type> value);
______________________________________
For instance, in the RSM, the id property of UrepNamespace has the index constraint. The generated find function is: UrepNamespace UrepNamespace.sub.-- find.sub.-- id(udt.sub.-- cstring id); Operations Operations can be defined for the types in a model. The format for a function declaration in C corresponding to an operation is:
______________________________________
<return type> <type name>.sub.-- <operation name>
(<type name> myself
›, <parameters> !
);
______________________________________
The additional parameter myself is always inserted at the front of the parameter list for each operation that is an operation for a type, with the exception of operations which have the class constraint. This parameter is the object for which the operation is called. Parameter names and types are the same as those defined in the model for the operation. For example, the reserve operation on UrepVersionedObject is declared as:
______________________________________
UrepVersionedObject
UrepVersionedObject.sub.-- reserve
(UrepVersionedObject myself
,udt.sub.-- cstring variant
,udt.sub.-- boolean reserveComponent
);
______________________________________
Table 2 describes for the different types how they are passed as parameters and returned as result values for operations.
TABLE 2
______________________________________
Parameters and Return Types.
Input/Output
Type Return Input Parameter
Output Param
______________________________________
udt.sub.-- boolean,
value (that is,
value pointer (that.
udt.sub.-- char,
<type>) is, <type>*)
udt.sub.-- integer,
udt.sub.-- real,
enumerated types
UrepObject (and
value value pointer
subtypes)
udt.sub.-- string,
pointer (already
pointer pointer
udt.sub.-- cstring
a pointer)
______________________________________
For enumerated types, the C enum type name will be used as the formal parameter type, rather than a data type such as udt.sub.-- integer. If the return type of the operation is a UREP data type that has a C equivalent, or one of the parameters is of that type (or both) an additional function declaration is generated. This function specifies a <data type> in place of the parameter(s) and a <data type> in place of the return value that were specified as C data types in the normal operation declaration. The C format for this function declaration is:
______________________________________
<data type>
<type name>.sub.-- <operation name>.sub.-- obj
(<type name> myself
›, <parameters> !
);
______________________________________
For example, the accessor and mutator functions for the maxvariantWidth property of the UrepSystemProfile type also has these function declarations:
______________________________________
UrepInteger
UrepSystemProfile.sub.-- get.sub.-- maxVariantWidth.sub.-- obj
(UrepSystemProfile myself);
void
UrepSystemProfile.sub.-- set.sub.-- maxVariantWidth.sub.-- obj
(UrepSystemProfile myself
,UrepInteger value
);
______________________________________
Only one such function is generated for each operation with a parameter or return value of the appropriate type. All permutations of the operation for the combinations of objects and values are not generated. Operations declared with a class constraint do not have the myself parameter. The format for the corresponding C function declaration is:
______________________________________
<return type>
<type name>.sub.-- <operation name>
(UREP urep
›, <parameters> !
);
______________________________________
Operations can have an access mode constraint with the values of public access, protected access, and private access. Only those operations with an access mode constraint of public access will have C function declarations. Changing the access mode of an operation to protected or private access means that it is not available in the C binding, because of the limitations of the C language. The constructor operations used in the C++ Binding are not generated in the C binding. The object initializer functions discussed earlier take the place of these operations. In order to remove a persistent object from the repository, the destruct operation of the UrepPersistentObject type must be called because a destruct operation is not generated for each type. The destructobject function used to delete the memory used by the C object, does not remove the linked object from the repository. The following is the C function declaration for the destruct operation for UrepPersistentObject:
______________________________________
void
UrepPersistentObject.sub.-- destruct
(UrepPersistentobject myself
,UrepReferenceProcessing freeMode
);
______________________________________
The following example shows how to destruct an instance of UrepNamedVersionedObject. NVO is an object variable that is linked to an instance of UrepNamedVersionedObject.
______________________________________
UrepPersistentObject.sub.-- destruct(NVO,
UrepReferenceProcessing.sub.-- objectOnly);
______________________________________
For transient object types, there is no defined destruct operation. Instead, the destructobject function defined earlier destroys the object and any memory associated with it, as well as any internal memory associated with the object. It should be noted that it is important to call the destructobject function for all objects (whether linked to persistent, transient, or not linked at all) so that memory in the C program is released properly. This does not occur automatically. Referring now to FIG. 5, a flow chart of the steps for generating C binding is illustrated. The process begins with a start bubble 40, followed by an inquiry as to whether or not there are more types in model (diamond 41). If the answer to this inquiry is yes, then another inquiry is made as to whether or not the type is valid for C binding (diamond 42). That is, this inquiry determines if the type is publicly accessible. If the answer to this inquiry is no, then a return is made back to the diamond 41 processing the next type. On the other hand, if the answer to this inquiry is yes, then the process shown in FIG. 6 and described hereinafter is called (block 43). This process generates the function declarations for types. Next, another process is called to generate C to C++ wrapper functions (block 44). This process is shown in FIG. 7 and described further hereinafter. Finally, a return is made back to the diamond 41 to process the next type. If the answer to the inquiry in the diamond 41 is no, then yet another inquiry is made as to whether or not there are more data types in model (diamond 45). If the answer to this inquiry is yes, then still another inquiry is made as to whether or not the current type is valid for C binding (diamond 46). This inquiry determines if the current type can be translated to a data type in the C language. If the answer to this inquiry is no then a return is made back to the diamond 45 for the next data type. On the other hand, if the type is valid then a process for generating C function declarations for data types is called (block 47). This process is shown in FIG. 10 and described further hereinafter. Next, a process for generating C to C++ wrapper functions for data types is called (block 48). This process is illustrated in FIG. 11 and described further hereinafter. Finally, a return is made back to the diamond 45 to process the next data type. If there are no more data types in model, then the process is stopped (bubble 49). Referring now to FIG. 6, a flow chart of the process alluded to above for generating function declarations for types is shown. The process begins with a start bubble 50 followed by a process step of generating object initializer function declarations (block 51). Next, an inquiry is made as to whether or not there are more properties for types (diamond 52). If the answer to this inquiry is yes, then another inquiry is made as to whether or not the property is valid for C binding (diamond 53). That is, this inquiry determines if the property is publicly available. If the answer to this inquiry is no, then a return is made back to the diamond 52 to process the next property for type. On the other hand if the answer to this inquiry is yes, then a process step of generating accessor function declarations is performed (block 54). Following this process step, yet another inquiry is made as to whether or not property is not read-only (diamond 55). If the answer to this inquiry is no, then a return is made back to the diamond 52. On the other hand, if the property is not read-only, then a process step of generating mutator function declarations is performed (block 56). Finally, a return is made back to the diamond 52 for processing the next property for the type. If there are no more properties for type, then yet another inquiry is made as to whether or not there are more operations for type to process (diamond 57). If the answer to this inquiry is yes, then still another inquiry is made as to whether or not the operation is valid for C binding. If the answer to this inquiry is no, then a return is made back to the diamond 57. On the other hand if the answer to this inquiry is yes, then a process step of resolving operation name is performed (block 59). Next, an operation declaration is generated, block 60, and a return is made back to the inquiry 57 to determine if there are more operations for type. If there are no more operations for type, a process step of generating special function declarations is performed (block 61). Finally, the process returns to block 44 (FIG. 5), as depicted by a bubble 62. Referring now to FIG. 7, a flow chart of the process for generating C to C++ wrapper functions is illustrated. The process begins with a start bubble 64, followed by a process step of generating object initializer functions (block 65). Next, an inquiry is made as to whether or not there are more properties for the type (diamond 66). If the answer to this inquiry is yes, then another inquiry is made as to whether or not property is valid for C binding (diamond 67). This inquiry determines whether or not the property for type is publicly accessible. If the answer to this inquiry is no, then a return is made back to the diamond 66. On the other hand, if the answer to this inquiry is yes, then a process is called for generating C to C++ wrapper code for accessor functions (block 68). This process is illustrated in FIG. 8 and described further hereinafter. Following this, an inquiry is made as to whether or not property is not read-only (diamond 69). If the answer to this inquiry is no, then a return is made back to the diamond 66. On the other hand, if the answer is yes then the process for generating C to C++ wrapper code for mutator functions is called (block 70). This process is shown in FIG. 8 and described further hereinafter. Finally, a return is made back to the diamond 66 to process the next property for the type. If there are no more properties for the type, then another inquiry is made as to whether or not there are more operations for the type (diamond 71). If the answer to this inquiry is yes, then yet another inquiry is made as to whether or not the operation is valid for C binding (i.e., is the operation publicly accessible) (diamond 72). If the answer to this inquiry is no then a return is made back to the diamond 71 to process the next operation for type. On the other hand, if the answer to this inquiry is yes, then a process step of resolving operation name is performed (block 73). That is, a determination is made if the operation is overloaded for this type. If it is, then a numeric suffix is created which is appended to the function name in order to prevent duplicate function names. Next, a process for generating the C to C++ wrapper function for an operation is called (block 74). This process is shown in FIG. 9, which is described further hereinafter. Finally, a return is made back to the diamond 71 to process the next operation for type. If there are no more operations for type, then a process step of generating wrapper functions for special operations for the type is performed (block 75). Finally, the process returns to diamond 41 (FIG. 5), as depicted by a bubble 76. Referring now to FIG. 8, a process for generating C to C++ wrapper functions for accessors and mutators is illustrated. The process begins with a start bubble 78 followed by a process step of generating a function heading (block 79). Next, another process of generating the code for checking the myself parameter is performed (block 80). Following this, an inquiry is made as to whether or not a mutator function is being generated, and the property type is an object (diamond 81). If the answer to this inquiry is yes, then a process step of generating a check for the object parameter is performed (block 82). On the other hand, if the answer to the inquiry at diamond 81 is no (or once the process step 82 is complete), another process step of generating code for a return if an error was found (block 83). Next, a process step of generating code to call the C++ accessor or mutator (block 84) is performed. Following this, an inquiry is made to determine if the function being generated is an accessor (diamond 85). If the answer to this inquiry is yes, then a process step of generating code to return the value is performed (block 86). If the function is not an accessor (or on completion of the process step in block 86), the process returns to either diamond 69 or 66, depending upon whether this property was called by the step in block 68 or 70, respectively. This return step is depicted by a bubble 87. Referring now to FIG. 9A, which is the first of a two-part flow chart, the process for generating C to C++ wrapper functions for an operation is illustrated. The process begins with a start bubble 90 followed by a process step for generating a function heading with the return type and parameter list (block 91). That is, the C++ function is generated, which includes the return type, function name and a list of parameters enclosed in parentheses. Next, an inquiry is made as to whether or not this is a class operation (diamond 92). If the answer to this inquiry is yes, then a process step of checking for the UREP object is performed (block 93). That is, for class operations code is generated to check the value passed for the UREP parameter to insure that the C++ object it represents is of the correct type. On the other hand, if the answer to this inquiry is no, then another process step of generating a check for the myself parameter is performed (block 94). That is, code is generated to check the value of the myself parameter to insure that the C++ object it represents is the correct type for the operation. Once the process steps 93 or 94 have been completed, an inquiry is made as to whether or not there are more parameters (diamond 95). If the answer to this inquiry is yes, then a process step of declaring local variable of correct type for the parameter is performed (block 96). Next, another inquiry is made as to whether or not the parameter is a data type, i.e., is it real, integer, Boolean, string, etc. (diamond 97). If the answer to this inquiry is yes, then a process step of generating an assignment of the parameter to the local variable is performed (block 98). Once this step has been performed, a return is made back to the diamond to process more parameters. On the other hand, if the answer to this inquiry is no, then yet another inquiry is made as to whether or not the parameter is optional (diamond 99). If the answer to this inquiry is yes, then another process step of generating a check for null object because the parameter is optional (block 100). Following these steps, another process step of generating a call to a function to check the object that the parameter represents is performed (block 101). Once this step is complete, yet another process step is performed to generate code to store the object the parameter represents in a local variable. Next, a return is made back to the diamond 95 to process the next parameter. If the parameter is not optional (diamond 99), the process step of generating a check for a null object (block 100) is skipped and the process step of generating a call to check the object is then performed (block 101). Finally, if there are no more parameters a branch is taken (via connector "A") to an inquiry as to whether or not the operation has a return value (diamond 103, FIG. 9B). Referring now to FIG. 9B, the second half of the process for generating C to C++ wrapper function for an operation is illustrated. If the operation has a return value (diamond 103), then code is generated to return a null object or value if an error is found when the parameters are checked (block 104). On the other hand, if the operation does not have a return value, then code is generated to return if an error occurs when the parameters are checked (block 105). Next, an inquiry is made as to whether or not the operation returns an object, or has an output or an input/output parameter (diamond 106). If the answer to this inquiry is yes, then a local variable is generated to hold a return value (block 107). Next, code is generated to call the C++ operation and capture the return value in the local variable (block 108). Following these steps, an inquiry is made as to whether or not the operation has output or input/output parameters (diamond 109). If the answer to this inquiry is yes, then code is generated to transfer values of output or input/output parameters from the C++ objects to the C parameters (block 110). On completion of this step, or if the operation does not have output or input/output parameters, code is generated to store the return value (block 111). Next, a return is generated and the process returns to the diamond 71 (FIG. 7), as depicted by a bubble 113. If the operation did not return an object or has parameters that do (diamond 106), then code is generated to call the C++ operation in a return statement (block 114) followed by a return to the diamond 71 (FIG. 7). Referring now to FIG. 10, the process for generating function declarations for data types is illustrated. The process begins with a start bubble 118, followed by an inquiry (diamond 119) as to whether or not the type needs values (i.e., limits for strings and integers, enumeration for symbolic types, etc.). If the answer to this inquiry is yes, then values for the type as C enums are generated (block 120). Once this step is complete, or if the type does not need values, another inquiry is made as to whether or not the type is not symbolic (diamond 121). If the answer to this inquiry is yes, then object initializer function declarations are generated (block 122). Next, conversion function declarations are generated (block 123). Following these steps, yet another inquiry is made as to whether or not type is symbolic (diamond 124). If the answer to this inquiry is yes, then the format and conversion function declarations are generated (block 125). If the type is not symbolic, the class id and model id function declarations are generated (block 126). After completion of this step, or upon completion of the process step 125, the process returns to block 48 (FIG. 5), as depicted by a bubble 127. Referring now to FIG. 11, the process for generating C to C++ wrapper functions for data types is illustrated. The process begins with a start bubble 130, followed by an inquiry as to whether or not the data type is not symbolic (diamond 131). If the data is not symbolic, then object initializer functions are generated (block 132). Next, conversion functions are generated (block 133). Following these steps, or if the data type is symbolic (diamond 131), another inquiry is made as to whether or not type is symbolic (diamond 134). If the answer to this inquiry is yes, then format and conversion functions for the symbolic type are generated (block 135). If the type is not symbolic, then class id and model id functions are generated (block 136). Once this step is complete, or following completion of the step in process block 135, the process returns to diamond 45 (FIG. 5), as depicted by a bubble 137. Although the invention has been described with reference to a specific embodiment, this description is not meant to be construed in a limiting sense. Various modifications of the disclosed embodiment as well as alternative embodiments of the invention will become apparent to one skilled in the art upon reference to the description of the invention. It is therefore contemplated that the appended claims will cover any such modifications of embodiments that fall within the true scope of the invention.
|
Same subclass Same class Consider this |
||||||||||
