On this page:
Distinguishing Objects and Servants
The term Ice object refers to a conceptual entity that is incarnated by a programming language object called a servant. An Ice object typically implements a Slice interface, but could implement a Slice class with operations instead.
In addition, applications can transfer instances of Slice classes by value, using classes as just another type in the data model, along with other Slice types such as structures and sequences. We refer to this usage as transferring objects by value, to distinguish it from the formal concept of Ice objects.
As a refresher on Slice syntax, consider the following definitions:
As its name implies, the operation
getByValue returns a
ClassWithOperations object by value, whereas the
getByProxy operation returns a proxy to a remote Ice object that implements
ClassWithOperations. Although their syntax is quite similar, the semantics of these two operations are very different. Calling
getByValue, a client receives a local instance of
ClassWithOperations on which it can access the
cost member and call the
updateCost method. With
getByProxy, a client can use the returned proxy to call
updateCost as a remote invocation but cannot access the
cost member, which is local state for the remote servant.
Design Considerations for Exceptions
Using exceptions to report error conditions is a generally accepted idiom, given its wide support in programming languages and its convenience in simplifying a developer's error-handling responsibilities. The alternatives, such as using numeric error codes, can be unwieldy in practice and lack the flexibility that a well-designed exception hierarchy provides. Slice does not impose any particular error-handling style on developers, but if you choose to use Slice exceptions, you should understand their advantages and limitations.
Slice exception types support single inheritance, which allows you to compose hierarchies of related error conditions. Furthermore, exceptions may have any number of data members, meaning your errors can convey as little or as much detail as you like. One limitation of exceptions is that they are treated differently than other Slice data types: exceptions cannot be passed as parameters, and cannot be used as data members. The sole purpose of an exception type is to appear in the
throws clause of an operation.
As an Ice developer, you are not expected to know the intricacies of how Ice transfers exceptions from server to client. However, it can be very useful for you to understand that exceptions are composed of slices, that multiple encoding formats are available, and that passing exceptions through intermediaries may require additional effort.
Design Considerations for Objects
The motivation for using objects by value is not as simple as for exceptions. If you wish to use the exception idiom, you have no choice but to use the Slice exception model. However, an application is not forced to use objects by value. Other user-defined Slice types, such as sequences, dictionaries, and structures, may satisfy the data model requirements of many applications. Classes and structures may seem similar at first glance because they can both be used as data containers, but there are also some significant differences between them. Be sure you understand the architectural implications of classes before using them in your application. Granted, if your data model requires the use of inheritance, polymorphism, or self-referential capabilities, you really have no other choice but to use classes.
One aspect of using objects by value that must not be overlooked is overhead, both in terms of the processing effort required to encode and decode them, as well as the space consumed in messages that use them. In Ice 3.4 and earlier, objects by value were relatively expensive, likely causing many developers to use structures when classes might have been the more convenient choice. In Ice 3.5 and later, the new compact format makes objects by value much more competitive with structures. Ice 3.5 and later also make it possible to forward objects of unknown types for situations where flexibility takes precedence over compactness.
Composition using Slices
The data encoding rules for classes and exceptions rely on the concept of slices in which each slice corresponds to a level in the type hierarchy and contains the encoded data members defined at that level. The low-level details of the encoding rules are not relevant here, but having a basic understanding of how Ice marshals and unmarshals classes and exceptions can help you with your design decisions. Consider this example:
The encoded form of an instance of class
C is summarized below:
Notice that the instance is encoded in order from most-derived to least-derived. For the sake of simplicity, we have omitted many of the encoding details. For instance, we have not shown that the initial (most-derived) slice includes a type ID string such as
"::MyModule::C" that identifies the object's type.
Although we used a class hierarchy in the example above, the behavior for exceptions is very similar.
Let's add the following interface to our discussion:
Now suppose a client invokes
getObject. Let's also suppose that the servant actually returns an instance of class
C, which is perfectly legal given that
C derives from
B. The Ice run time in the client knows that the formal return type of the
getObject operation is
B and unmarshals the object as follows:
- Extract the type ID of the initial slice (
- If this type is known to the client, instantiate the object, decode the state for each of its slices, and return the object.
- If type
Cis not known to the client, which can occur when client and server are using different versions of the Slice definitions, the Ice run time discards the encoded data for slice
C(also known as slicing the object), reads the type ID of the next slice, and repeats step 2.
- If type
Bis also not known to the client then we have a problem. First, from a logical standpoint, the client must know type
Bbecause it is the statically-declared return type of the operation that the client just invoked. Second, we cannot slice this object any further because it would no longer be compatible with the formal signature of the operation; the returned object must at least be an instance of
B, so we could not return an instance of
A. In either case, the Ice run time would raise an exception.
Generally speaking, to unmarshal an instance of a class or exception, the Ice run time discards the slices of unknown types until it finds a type that it recognizes, exhausts all slices, or can no longer satisfy the formal type signature of the operation. This slicing feature allows the receiver, whose Slice definitions may be limited or outdated, to continue to function properly even when it does not recognize the most-derived type.
With this knowledge, we can now discuss new features introduced in Ice 3.5 that affect the run time's encoding rules for classes and exceptions. Namely, a choice of encoding formats and the ability to preserve the slices of unknown types. First, however, you should understand the need for object factories.
The Factory Requirement
For Slice classes that define data members but no operations, receiving an object by value is straightforward: the Ice run time examines the type ID encoded with the object, translates that (in a language-specific manner) into the corresponding generated class, and creates an instance of that class to represent the object. This happens without any help from the application. However, things get more complicated when a class defines operations. By definition, such a class is abstract, therefore Ice cannot allocate an instance of the generated abstract class. Only the application knows which derived class implements an abstract Slice class, so Ice requires the application to supply factories for translating type IDs into their programming language equivalent objects.
Ice does not require or support user-supplied factories for exceptions because exceptions cannot define operations.
The encoding for classes does not inform the receiver whether an object is abstract. As Ice processes each slice of an object, it first attempts to locate the corresponding generated class. If Ice cannot find the class, or finds the class but determines that it is abstract, the run time looks for a factory registered by the application for that type ID. If no factory is found to create the object, Ice may discard the slice as described above.
Here is some sample code to demonstrate the implementation and registration of a factory:
Our factory is only intended to create instances of the
ClassWithOperations type we defined earlier, which is the reason for the assertion. In your own application, you can partition the factory responsibilities however you wish. For example, you can define a separate factory class for each Slice class, or use a single factory class that knows how to create multiple types. Whatever you decide, you must register a factory for each abstract class that the program has the potential to receive, and the registrations must occur before any remote invocations are made that might return one of these types.
You can also register a factory for a concrete Slice class if you wish to substitute a derived type, or simply because you want more control over the allocation of that type.
Version 1.0 of the Ice encoding, which is used by all versions of Ice through 3.4, supports a single format for encoding the slices of classes and exceptions. The primary design goal for this format was the ability to support slicing at the expense of a somewhat larger encoding. The fact that the encoding embeds the type IDs of each type in an instance's type hierarchy means the size of the encoded data is affected by the symbol names and nesting depth of the Slice definitions. Consider this example:
The encoding for an instance of
SubcontractorAccount includes the following type ID strings:
As you can see, there is a non-trivial amount of overhead for passing objects by value. Note however that the encoding includes a lookup table for type IDs to minimize this overhead as much as possible, meaning each unique type ID is only encoded once per message. While sending a single instance of
SubcontractorAccount might be relatively expensive, sending a collection of
SubcontractorAccount objects amortizes this cost over a number of instances. We can contrive situations where a message contains many instances of unrelated types, in which case the encoded type IDs would have a greater impact on the size of the resulting message, but those scenarios are less common.
The slice format for classes and exceptions is nearly identical. The main difference is that, for an exception, there is no need for a type ID lookup table because there can never be more than one instance of an exception in a message.
With version 1.1 of the encoding introduced in Ice 3.5, you now have two slice formats to choose from: compact and sliced.
The compact format in version 1.1 of the encoding sacrifices the slicing feature of classes and exceptions to reduce the encoded size of these types. The format still encodes the type ID of the most-derived type, and continues to use a lookup table to prevent duplication of type IDs in a message, but omits the type IDs for any intermediate types. Version 1.1 of the encoding also eliminates the need to encode the type ID for the base
Object type of class instances and, together with other optimizations, makes the encoded size of classes in the compact format much more competitive with that of structures.
Ice 3.5 and later use the compact format by default. Existing applications that rely on the slicing feature may begin receiving
NoObjectFactoryException errors after migrating to Ice 3.5 or later, an indication that the run time does not recognize the most-derived type and cannot slice to a less-derived type because the compact format omits the necessary type information. These applications must use the sliced format instead, which behaves similarly to the format in version 1.0 of the encoding.
Using our previous example, an instance of
SubcontractorAccount still includes the type ID string
"::TopLevelModule::IntermediateModule::SubcontractorAccount" in the compact format, but omits the type ID strings for
Object. Clearly we could reduce this overhead even further by using shorter symbol names and eliminating the nested module, or we could omit the type ID string altogether by using a compact type ID.
The sliced format in version 1.1 of the encoding retains the slicing behavior of version 1.0: a receiver discards the slices of unknown types until it finds a type that it recognizes, exhausts all available slices, or slices so far that the result would violate the formal signature of the operation. The sliced format still encodes the type IDs of the most-derived type as well as all intermediate types. However, unlike the 1.0 encoding, Ice no longer encodes the type ID for the base
Object type in class instances such that, along with other improvements in the 1.1 encoding, the sliced format is still more efficient than the 1.0 encoding.
Choosing a Format
Ice uses the compact format by default, so an application that needs slicing behavior must explicitly enable it as follows:
- Set the
Ice.Default.SlicedFormatproperty to a non-zero value to force the Ice run time to use the sliced format by default.
- Annotate your Slice definitions with
formatmetadata to selectively enable the sliced format for certain operations or interfaces.
For example, suppose an application can safely use the compact format most of the time, but still needs slicing in a few situations. In this case the application can use metadata to enable the sliced format where necessary:
The semantics implied by these definitions state that the caller of
getAccount assumes it will know every type that that might be returned, but the same cannot be said for
importAccount. By enabling the sliced format here, we allow the client to "slice off" what it does not recognize, even if that means the client is left with only an instance of
Account and not an instance of some derived type.
Now let's examine the opposite case: use the sliced format by default, and the compact format only in certain cases:
Here we specify that all operations in
Ledger use the sliced format unless overridden at the operation level, which we do for
Keep the following points in mind when using the
- The chosen format only controls how Ice encodes a value and has no effect on the decoding process.
The format affects the input parameters, output parameters, and exceptions of an operation. Consider this example:
The metadata forces the client to use the compact format when marshaling the input parameter
oldAccount, and forces the server to use the compact format when marshaling the return value or the exception. For a given operation, it is not possible to use one format for the parameters and a different format for the exceptions.
- If you decide to use the
Ice.Default.SlicedFormatproperty, be aware that this property only affects the sender of a class or exception. For example, if you enable this property in the client but not the server, then all objects sent by the client use the sliced format by default, but all objects and exceptions returned by the server use the compact format by default.
By offering two alternative formats, Ice gives you a great deal of flexibility in designing your applications. The compact format is ideal for applications that place a greater emphasis on efficiency, while the sliced format is helpful when clients and servers evolve independently.
Declaring a data member of type
class to be optional does not eliminate the Ice run time's requirement for type information. Suppose a client uses the following Slice definitions:
Also suppose that the server is using a newer version of the definitions:
When receiving an instance of
UserInfo, the Ice run time in the client ignores the
group member because it does not recognize its tag, but Ice must still be able to unmarshal (or skip) the
GroupInfo instance data in the message. If the server used the compact format for its reply, the client will receive an exception while attempting to decode the message. On the other hand, this scenario would succeed when using the sliced format because the format embeds enough information for the receiver to skip instances of unknown types.
The lesson here is that adding a new class type to your application can have unexpected repercussions, especially when using the compact format. Remember that the sender can change formats without necessarily needing to update the receiver. For example, after adding the
group member to
UserInfo, the server could begin using the sliced format for all operations that return
UserInfo to ensure that any existing clients would be able to ignore this member.
The concept of slicing involves discarding the slices of unknown types when decoding an instance of a Slice class or exception. Here is a simple example:
The server implementing the
Relay object must know the type
Base (because it is statically referenced in the interface definition), but may not know
Derived. Suppose the implementation of
transform involves forwarding the object to another back-end server for processing and returning the transformed object to the caller. In effect, the
Relay server is an intermediary. If the
Relay server does not know the types
Derived, it will slice an instance to
Base and discard the data members of any more-derived types, which is clearly not the intended result because the back-end server does know those types. The only way the
Relay server could successfully forward these objects is by knowing all possible derived types, which makes the application more difficult to evolve over time because the intermediary must be updated each time a new derived type is added.
To address this limitation, Ice supports a new metadata directive that allows an instance of a Slice class or exception to be forwarded with all of its slices intact, even if the intermediary does not know one or more of the instance's derived types. The new directive,
preserve-slice, is shown below:
With this change, all instances of
Base, and types derived from
Base, are encoded in a format that allows them to be forwarded intact. Also notice the addition of the
format:sliced metadata on the
Relay interface, which ensures that its operations use the sliced format and not the default compact format.
Unknown Sliced Objects
The combination of preserved slices and the sliced format means Ice applications now have a subtle new capability that was not available prior to Ice 3.5. Suppose we modify our
Relay example as shown below:
The only difference here is the signature of the
transform operation, which now uses the
Object type. Technically, it is not necessary for the intermediary server to know any of the class types that might be relayed via this new definition of
transform because the formal types in its signature do not impose any requirements. As long as any object types known by the intermediary are marked with
preserve-slice, and the
transform operation uses the sliced format, this intermediary is capable of relaying objects of any type.
If the Ice run time in the intermediary does not know any of the types in an object's inheritance hierarchy, and the formal type is
Object, Ice uses an instance of
UnknownSlicedObject to represent the object:
The implementation of
transform receives an instance of
UnknownSlicedObject and can use that object as its return value. If necessary, the implementation can determine the most-derived type of the object by calling
getUnknownTypeId. The actual encoded state of the object is encapsulated by the
Applications do not normally need to use these types, but they provide all of the information the run time requires to forward an instance.
The definition of
SliceInfo are very similar in the other language mappings and are not shown here.