Slicing Values and Exceptions

9 min read

9 min read

9 min read

9 min read

9 min read

9 min read

9 min read

9 min read

9 min read

This page describes the concept of slicing, which is how the Ice runtime reacts when it receives an instance of an unknown class or exception.

Classes and exceptions are composed of slices, where each slice corresponds to a level in the type hierarchy and contains the data members defined at that level. Consider this example showing a class hierarchy:

Slice
class A
{
int i;
}
class B extends A
{
float f;
}
class C extends B
{
string s;
}

An instance of class C contains the following slices, listed in order from most-derived to least-derived:

SliceContents
C
string s
B
float f
A
int i

A receiver knows a type when its Slice loader creates an instance of this type from its type ID. A receiver that knows the most-derived type of an instance, here C, unmarshals all its slices. Otherwise, what the receiver does depends on the format the sender used to marshal the instance:

  • Compact format: the receiver must know the most-derived type of the instance. If it does not, Ice throws a MarshalException.
  • Sliced format: the receiver skips the slices of the types it does not know, also known as slicing the instance, and creates an instance of the most-derived type it knows. This instance must still be an instance of the declared type, such as the return type of an operation; otherwise, Ice throws a MarshalException. If the receiver knows none of the types of a class instance, Ice keeps it as an unknown sliced value, which only the declared type Value accepts.

These rules also apply to an exception, where the declared type is the operation's exception specification: after slicing, the exception must be one of the declared exceptions or derive from one; otherwise, Ice throws an UnknownUserException. If the receiver knows none of the exception's types, Ice throws a MarshalException.

Ice provides two on-the-wire formats for class and exception slices: the compact format and the sliced format. For class instances, Ice uses the compact format by default, which is more space-efficient on the wire but offers less flexibility on the receiving end.

An application that needs the slicing behavior we discussed in the previous section for class instances must explicitly enable the sliced format as follows:

  • Set the Ice.Default.SlicedFormat property to 1 to use the sliced format by default.
  • Annotate your Slice definitions with the format:sliced metadata 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:

Slice
interface Ledger
{
Account getAccount(string id); // Uses compact format
["format:sliced"]
Account importAccount(string source); // Uses sliced format
}

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:

Slice
["format:sliced"]
interface Ledger
{
["format:compact"]
Account getAccount(string id); // Uses compact format
Account importAccount(string source); // Uses sliced format
}

Here we specify that all operations in Ledger use the sliced format unless overridden at the operation level, which we do for getAccount.

The format affects the marshaling of input parameters, output parameters, and return value of an operation.

Consider this example:

Slice
exception IncompatibleAccount { ... }
interface Ledger
{
["format:compact"]
Account migrateAccount(Account oldAccount) throws IncompatibleAccount;
}

The metadata forces the client to use the compact format for the input parameter oldAccount, and forces the server to use the compact format for the return value.

If you decide to use the Ice.Default.SlicedFormat property, be aware that this property only affects the sender of a value. For example, if you enable this property in the client but not the server, then all values sent by the client use the sliced format by default, but all values 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.

With encoding 1.1, when Ice slices a class instance, it preserves the slices of unknown types. Here is a simple example:

Slice
class Base
{
int b;
}
class Intermediate extends Base
{
int i;
}
class Derived extends Intermediate
{
int d;
}
interface Relay
{
["format:sliced"]
Base transform(Base b);
}

The server implementing the Relay interface must know the type Base (because it is statically referenced in the interface definition), but may not know Intermediate or Derived. Suppose the implementation of transform involves forwarding the instance to another back-end server for processing and returning the transformed instance to the caller. In effect, the Relay server is an intermediary. If the Relay server does not know the types Intermediate and Derived, it slices an instance of Derived to Base and preserves the slices of Intermediate and Derived in encoded form. When the Relay server marshals this instance in the sliced format, Ice writes the preserved slices back, so the back-end server receives all the slices of the Derived instance.

When the Relay server marshals this instance in the compact format, Ice omits the preserved slices and writes only the Base slice.

If a class instance is sliced upon receipt, calling ice_getSlicedData on this instance will return a SlicedDataSlicedDataSlicedDataSlicedDataSlicedDataSlicedDataSlicedDataSlicedDataSlicedData object that holds the preserved slices.

Suppose we modify our Relay example as shown below:

Slice
class Base
{
int b;
}
class Intermediate extends Base
{
int i;
}
class Derived extends Intermediate
{
int d;
}
interface Relay
{
["format:sliced"]
Value transform(Value b);
}

The only difference here is the signature of the transform operation, which now uses the Value 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 the transform operation uses the sliced format, this intermediary is capable of relaying values of any type.

If the Ice runtime in the intermediary does not know any of the types in an object's inheritance hierarchy, and the formal type is Value, Ice uses an instance of UnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValueUnknownSlicedValue to represent the instance.

The implementation of transform receives an instance of UnknownSlicedValue and can use that object as its return value. If necessary, the implementation can determine the most-derived type of the instance by calling ice_id.