Slicing Values and Exceptions

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

On this page:

Composition using Slices

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
Cstring s
Bfloat f
Aint i

Let's add the following interface to our discussion:

Slice
interface I
{
    B getObject();
}

Now suppose a client invokes getObject. Let's also suppose that the server actually returns an instance of class C, which is 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, therefore B is the least-derived type that the client can accept for this operation. At a high level, the Ice run time in the client behaves as follows:

  1. It discovers that the most-derived type of the returned instance is C and checks whether this type is known. These checks include language-specific mechanisms as well as the value factory API.
  2. If this type is known to the client, Ice instantiates the object, extracts the data members for each of its slices, and returns the object.
  3. If type C is not known to the client, which can occur when the client and server are using different versions of the Slice definitions, Ice discards the data members for slice C (also known as slicing the object) and tries again with the next slice.
  4. If type B is also not known to the client then we have a problem. First, from a logical standpoint, the client must know type B because 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, upon receipt of 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.

Slice Formats

Ice provides two on-the-wire formats for class and exception slices: the compact format and the sliced format. 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 must explicitly enable the sliced format as follows:

  • Set the Ice.Default.SlicedFormat property to a non-zero value to force the Ice run time to use the sliced format by default.
  • Annotate your Slice definitions with format 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 input parameters, output parameters, and exceptions 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 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.SlicedFormat property, be aware that this property only affects the sender of a value or exception. 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 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.

Optional Objects

A limitation of the compact format can prevent a receiver from successfully receiving a message. Suppose a client uses the following Slice definitions:

Slice
class UserInfo
{
    string name;
    optional(1) string organization;
}

Also suppose that the server is using a newer version of these definitions:

Slice
class GroupInfo // New type
{
    ...;
}
 
class UserInfo
{
    string name;
    optional(1) string organization;
    optional(2) GroupInfo group; // New member
}

The server's definitions introduce a new class, GroupInfo, and an optional data member that uses this new type.

Let's examine the case where a client receives a message containing a UserInfo instance whose group member is set to a non-nil value. Since the GroupInfo type is unknown to the client, it needs to be able to skip the data associated with the GroupInfo instance in order to process the rest of the message. The sliced format includes the information the client needs to skip instances of unknown types, but the compact format unfortunately does not include this information and the client will raise an exception.

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 receive it successfully.

Preserving Slices

The concept of slicing involves discarding the slices of unknown types when receiving an instance of a Slice class or exception. Here is a simple example:

Slice
class Base
{
    int b;
}

class Intermediate extends Base
{
    int i;
}

class Derived extends Intermediate
{
    int d;
}

interface Relay
{
    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 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 instances 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:

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

With this change, all instances of Base, and types derived from Base, can 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.

If a preserved value instance is sliced upon receipt, calling ice_getSlicedData on the value will return a SlicedData object that represents the complete state of the instance. User exceptions provides the same ice_getSlicedData method that returns the complete state of a sliced user exception.

The SlicedData class is described below:

namespace Ice
{
    struct SliceInfo
    {
        std::string typeId;
        int compactId;
        std::vector<Byte> bytes;
        std::vector<std::shared_ptr<Value>> instances;
        bool hasOptionalMembers;
        bool isLastSlice;
    };

    class SlicedData
    {
    public:
        SlicedData(const SliceInfoSeq&);
        const SliceInfoSeq slices;
        void clear();
    };
}
namespace Ice
{
    struct SliceInfo : public IceUtil::Shared
    {
        std::string typeId;
        int compactId;
        std::vector<Byte> bytes;
        std::vector<ValuePtr> instances;
        bool hasOptionalMembers;
        bool isLastSlice;
    };

    class SlicedData : public IceUtil::Shared
    {
    public:
        SlicedData(const SliceInfoSeq&);
        const SliceInfoSeq slices;
        void clear();
    };
}
C#
namespace Ice
{
    public class SliceInfo
    {
        public string typeId;
        public int compactId;
        public byte[] bytes;
        public Value[] instances;
        public bool hasOptionalMembers;
        public bool isLastSlice;
    }
 
    public class SlicedData
    {
        public SlicedData(SliceInfo[] slices);

        public SliceInfo[] slices;
    }
}

Java
package com.zeroc.Ice;

public class SliceInfo
{
    public String typeId;
    public int compactId;
    public byte[] bytes;
    public com.zeroc.Ice.Value[] instances;
    public boolean hasOptionalMembers;
    public boolean isLastSlice;
}
 
public class SlicedData
{
    public SlicedData(SliceInfo[] slices);

    public SliceInfo[] slices;
}
Java
package Ice;

public class SliceInfo
{
    public String typeId;
    public int compactId;
    public byte[] bytes;
    public Ice.Object[] instances;
    public boolean hasOptionalMembers;
    public boolean isLastSlice;
}

public class SlicedData
{
    public SlicedData(SliceInfo[] slices);

    public SliceInfo[] slices;
}
JavaScript
class SliceInfo
{
    constructor()
    {
        this.typeId = "";
        this.compactId = -1;
        this.bytes = [];
        this.instances = [];
        this.hasOptionalMembers = false;
        this.isLastSlice = false;
    }
}
 
class SlicedData
{
    constructor(slices)
    {
        this.slices = slices;
    }
}
MATLAB
classdef SliceInfo < handle
    properties
        typeId
        compactId
        bytes
        instances
        hasOptionalMembers
        isLastSlice
    end
end
 
classdef SlicedData < handle
    properties(SetAccess=private)
        slices
    end
    methods
        function obj = SlicedData(slices)
            ...
        end
    end
end
Objective-C
ICE_API @protocol ICESlicedData<NSObject>
//
// Clear the slices to break potential cyclic references.
//
-(void) clear;
@end
PHP
class SliceInfo
{
    public $typeId;
    public $compactId;
    public $bytes;
    public $instances;
    public $hasOptionalMembers;
    public $isLastSlice;
}
 
class SlicedData
{
    public $slices;
}
Python
class SliceInfo(object):
    def __init__(self):
        self.typeId = ""
        self.compactId = -1
        self.bytes = None
        self.instances = None
        self.hasOptionalMembers = False
        self.isLastSlice = False
 
class SlicedData(object):
    def __init__(self, slices):
        self.slices = slices
Ruby
class SliceInfo
    attr_accessor :typeId, :compactId, :bytes, :instances, :hasOptionalMembers, :isLastSlice
end
 
class SlicedData
    attr_accessor :slices   # array of SliceInfo
end

SliceInfo should be considered read-only even when the Ice API allows changing the fields of a SliceInfo. SlicedData and SliceInfo provide all of the information the run time requires to forward an instance; applications do not normally need to use them.

In C++11 and Objective-C, if the received value refers to a graph with cycles, the application should call clear on the SlicedData object associated with the value to break potential cycles.

 

Unknown Sliced Values

Suppose we modify our Relay example as shown below:

Slice
["preserve-slice"]
class Base
{
    int b;
}
 
class Intermediate extends Base
{
    int i;
}
 
class Derived extends Intermediate
{
    int d;
}
 
["format:sliced"]
interface Relay
{
    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 any value types known by the intermediary are marked with preserve-slice, and the transform operation uses the sliced format, this intermediary is capable of relaying values 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 Value, Ice uses an instance of UnknownSlicedValue to represent the instance:

namespace Ice
{
    class UnknownSlicedValue : public Value
    {
    public:
 
        std::string ice_id() const;
        std::shared_ptr<SlicedData> ice_getSlicedData() const;
 
        ...
    };
} 
namespace Ice
{
    class UnknownSlicedValue : public Object
    {
    public:
 
        const std::string& ice_id() const;
        SlicedDataPtr ice_getSlicedData() const;
 
        ...
    };
} 
C#
namespace Ice
{
    public sealed class UnknownSlicedValue : Value
    {
        public UnknownSlicedValue(string unknownTypeId);
        public override SlicedData ice_getSlicedData();
        public override string ice_id();
    }
}
Java
package com.zeroc.Ice;

public final class UnknownSlicedValue extends Value
{
    public UnknownSlicedValue(String unknownTypeId);
    public SlicedData ice_getSlicedData();
    public String ice_id();
}
Java
package Ice;

public final class UnknownSlicedValue extends ObjectImpl
{
    public UnknownSlicedValue(String unknownTypeId);
    public SlicedData ice_getSlicedData();
    public String ice_id();
}
JavaScript
class UnknownSlicedValue extends Ice.Value
{
    constructor(unknownTypeId)
    {
        ...
    }

    ice_getSlicedData()
    {
        ...
    }

    ice_id()
    {
        ...
    }
}

MATLAB
classdef UnknownSlicedValue < Ice.Value
    methods
        function obj = UnknownSlicedValue(unknownTypeId)
            ...
        end
        function r = ice_getSlicedData(obj)
            ...
        end
        function id = ice_id(obj)
            ...
        end
    end
end
Objective-C
ICE_API @interface ICEUnknownSlicedValue : ICEObject
{
@private
    NSString* unknownTypeId_;
    id<ICESlicedData> slicedData_;
}
@end
PHP
class UnknownSlicedValue extends Value
{
    public function ice_id()
    {
        ...
    }
 
    public function ice_getSlicedData()
    {
        ...
    }
}
Python
class UnknownSlicedValue(Value):
    def ice_id(self):
        ...
    def ice_getSlicedData(self):
        ...
Ruby
class UnknownSlicedValue < Value
    def ice_id
        ...
    end
    def ice_getSlicedData
        ...
    end
end

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

See Also