Table of Contents Previous Next
Logo
Dynamic Ice : 35.3 Dynamic Invocation and Dispatch
Copyright © 2003-2010 ZeroC, Inc.

35.3 Dynamic Invocation and Dispatch

Ice applications generally use the static invocation model, in which the application invokes a Slice operation by calling a member function on a generated proxy class. In the server, the static dispatch model behaves similarly: the request is dispatched to the servant as a statically-typed call to a member function. Under­neath this statically-typed facade, the Ice run times in the client and server are exchanging sequences of bytes representing the encoded request arguments and results. These interactions are illustrated in Figure 35.1.
Figure 35.1. Interactions in a static invocation.
1. The client initiates a call to the Slice operation add by calling the member function add on a proxy.
2. The generated proxy class marshals the arguments into a sequence of bytes and transmits them to the server.
3. In the server, the generated servant class unmarshals the arguments and calls add on the subclass.
4. The servant marshals the results and returns them to the client.
5. Finally, the client’s proxy unmarshals the results and returns them to the caller.
The application is blissfully unaware of this low-level machinery, and in the majority of cases that is a distinct advantage. In some situations, however, an application can leverage this machinery to accomplish tasks that are not possible in a statically-typed environment. Ice provides the dynamic invocation and dispatch models for these situations, allowing applications to send and receive requests as encoded sequences of bytes instead of statically-typed arguments.
The dynamic invocation and dispatch models offer several unique advantages to Ice services that forward requests from senders to receivers, such as Glacier2 (Chapter 42) and IceStorm (Chapter 44). For these services, the request arguments are an opaque byte sequence that can be forwarded without the need to unmarshal and remarshal the arguments. Not only is this significantly more efficient than a statically-typed implementation, it also allows intermediaries such as Glacier2 and IceStorm to be ignorant of the Slice types in use by senders and receivers.
Another use case for the dynamic invocation and dispatch models is scripting language integration. The Ice extensions for Python, PHP, and Ruby invoke Slice operations using the dynamic invocation model; the request arguments are encoded using the streaming interfaces from Section 35.2.
It may be difficult to resist the temptation of using a feature like dynamic invo­cation or dispatch, but we recommend that you carefully consider the risks and complexities of such a decision. For example, an application that uses the streaming interface to manually encode and decode request arguments has a high risk of failure if the argument signature of an operation changes. In contrast, this risk is greatly reduced in the static invocation and dispatch models because errors in a strongly-typed language are found early, during compilation. Therefore, we caution you against using this capability except where its advantages significantly outweigh the risks.

35.3.1 Dynamic Invocation using ice_invoke

Dynamic invocation is performed using the proxy member function ice_invoke, defined in the proxy base class ObjectPrx. If we were to define the function in Slice, it would look like this:
sequence<byte> ByteSeq;

bool ice_invoke(
    string operation,
    Ice::OperationMode mode,
    ByteSeq inParams,
    out ByteSeq outParams
);
The first argument is the name of the Slice operation1. The second argument is an enumerator from the Slice type Ice::OperationMode; the possible values are Normal and Idempotent. The third argument, inParams, represents the encoded in parameters of the operation.
A return value of true indicates a successful invocation, in which case the marshaled form of the operation’s results (if any) is provided in outParams. A return value of false signals the occurrence of a user exception whose marshaled form is provided in outParams. The caller must also be prepared to catch local exceptions, which are thrown directly.
Note that the Ice run time currently does not support the use of collocation optimization in dynamic invocations. Attempting to call ice_invoke on a proxy that is configured to use collocation optimization raises CollocationOpti­mizationException. See Section 32.21 for more information on this optimiza­tion and instructions for disabling it.

35.3.2 Dynamic Dispatch using Blobject

A server enables dynamic dispatch by creating a subclass of Blobject (the name is derived from blob, meaning a blob of bytes). The Slice equivalent of Blobject is shown below:
sequence<byte> ByteSeq;

interface Blobject {
    bool ice_invoke(ByteSeq inParams, out ByteSeq outParams);
};
The inParams argument supplies the encoded in parameters. The contents of the outParams argument depends on the outcome of the invocation: if the operation succeeded, ice_invoke must return true and place the encoded results in outParams; if a user exception occurred, ice_invoke must return false, in which case outParams contains the encoded exception. The operation may also raise local exceptions such as OperationNotExistException.
The language mappings add a trailing argument of type Ice::Current to ice_invoke, and this provides the implementation with the name of the operation being dispatched. See Section 32.6 for more information on Ice::Current.
Because Blobject derives from Object, an instance is a regular Ice object just like instances of the classes generated for user-defined Slice interfaces. The primary difference is that all operation invocations on a Blobject instance are dispatched through the ice_invoke member function.
If a Blobject subclass intends to decode the in parameters (and not simply forward the request to another object), then the implementation obviously must know the signatures of all operations it supports. How a Blobject subclass determines its type information is an implementation detail that is beyond the scope of this book.
Note that a Blobject servant is also useful if you want to create a message forwarding service, such as Glacier2 (see Chapter 42). In this case, there is no need to decode any parameters; instead, the implementation simply forwards each request unchanged to a new destination. You can register a Blobject servant as a default servant (see Section 32.8) to easily achieve this.

35.3.3 C++ Mapping

This section describes the C++ mapping for the ice_invoke proxy function and the Blobject class.

ice_invoke

The mapping for ice_invoke is shown below:
bool ice_invoke(
    const std::string& operation,
    Ice::OperationMode mode,
    const std::vector< Ice::Byte >& inParams,
    std::vector< Ice::Byte >& outParams
);
Another overloading of ice_invoke (not shown) adds a trailing argument of type Ice::Context (see Section 32.12).
As an example, the code below demonstrates how to invoke the operation op, which takes no in parameters:
Ice::ObjectPrx proxy = ...
try {
    std::vector<Ice::Byte> inParams, outParams;
    if (proxy>ice_invoke("op", Ice::Normal, inParams,
                          outParams)) {
        // Handle success
    } else {
        // Handle user exception
    }
} catch (const Ice::LocalException& ex) {
    // Handle exception
}

Using Streams with ice_invoke

The streaming interface described in Section 35.2 provides the tools an applica­tion needs to dynamically invoke operations with arguments of any Slice type. Consider the following Slice definition:
module Calc {
    exception Overflow {
        int x;
        int y;
    };
    interface Compute {
        idempotent int add(int x, int y)
            throws Overflow;
    };
};
Now let’s write a client that dynamically invokes the add operation:
Ice::ObjectPrx proxy = ...
try {
    std::vector< Ice::Byte > inParams, outParams;

    Ice::OutputStreamPtr out =
        Ice::createOutputStream(communicator);
    out>writeInt(100); // x
    out>writeInt(1);  // y
    out>finished(inParams);

    if (proxy>ice_invoke("add", Ice::Idempotent, inParams,
                          outParams)) {
        // Handle success
        Ice::InputStreamPtr in =
            Ice::createInputStream(communicator, outParams);
        int result = in>readInt();
        assert(result == 99);
    } else {
        // Handle user exception
    }
} catch (const Ice::LocalException& ex) {
    // Handle exception
}
We neglected to handle the case of a user exception in this example, so let’s imple­ment that now. We assume that we have compiled our program with the Slice-generated code, therefore we can call throwException on the input stream and catch Overflow directly2:
    if (proxy>ice_invoke("add", Ice::Idempotent, inParams,
                          outParams)) {
        // Handle success
        // ...
    } else {
        // Handle user exception
        Ice::InputStreamPtr in =
            Ice::createInputStream(communicator, outParams);
        try {
            in>throwException();
        } catch (const Calc::Overflow& ex) {
            cout << "overflow while adding " << ex.x
                 << " and " << ex.y << endl;
        } catch (const Ice::UserException& ex) {
            // Handle unexpected user exception
        }
    }
As a defensive measure, the code traps Ice::UserException. This could be raised if the Slice definition of add is modified to include another user exception but this segment of code did not get updated accordingly.

Subclassing Blobject

Implementing the dynamic dispatch model requires writing a subclass of Ice::Blobject. We continue using the Compute interface from page 1249 to demonstrate a Blobject implementation:
class ComputeI : public Ice::Blobject {
public:
    virtual bool ice_invoke(
        const std::vector<Ice::Byte>& inParams,
        std::vector<Ice::Byte>& outParams,
        const Ice::Current& current);
};
An instance of ComputeI is an Ice object because Blobject derives from Object, therefore an instance can be added to an object adapter like any other servant (see Chapter 32 for more information on object adapters).
For the purposes of this discussion, the implementation of ice_invoke handles only the add operation and raises OperationNotExistException for all other operations. In a real implementation, the servant must also be prepared to receive invocations of the following operations:
• string ice_id()
Returns the Slice type id of the servant’s most-derived type.
• StringSeq ice_ids()
Returns a sequence of strings representing all of the Slice interfaces supported by the servant, including "::Ice::Object".
• bool ice_isA(string id)
Returns true if the servant supports the interface denoted by the given Slice type id, or false otherwise. This operation is invoked by the proxy function checkedCast.
• void ice_ping()
Verifies that the object denoted by the identity and facet contained in Ice::Current is reachable.
With that in mind, here is our simplified version of ice_invoke:
bool ComputeI::ice_invoke(
    const std::vector<Ice::Byte>& inParams,
    std::vector<Ice::Byte>& outParams,
    const Ice::Current& current)
{
    if (current.operation == "add") {
        Ice::CommunicatorPtr communicator =
            current.adapter>getCommunicator();
        Ice::InputStreamPtr in =
            Ice::createInputStream(communicator, inParams);
        int x = in>readInt();
        int y = in>readInt();
        Ice::OutputStreamPtr out =
            Ice::createOutputStream(communicator);
        if (checkOverflow(x, y)) {
            Calc::Overflow ex;
            ex.x = x;
            ex.y = y;
            out>writeException(ex);
            out>finished(outParams);
            return false;
        } else {
            out>writeInt(x + y);
            out>finished(outParams);
            return true;
        }
    } else {
        Ice::OperationNotExistException ex(__FILE__, __LINE__);
        ex.id = current.id;
        ex.facet = current.facet;
        ex.operation = current.operation;
        throw ex;
    }
}
If an overflow is detected, the code “raises” the Calc::Overflow user exception by calling writeException on the output stream and returning false, other­wise the return value is encoded and the function returns true.

Array Mapping

Ice for C++ supports an alternative mapping for sequence input parameters that avoids the overhead of extra copying. Since the ice_invoke functions treat the encoded input parameters as a value of type sequence<byte>, the dynamic invo­cation and dispatch facility includes interfaces that use the array mapping for the input parameter blob.
Ice provides two overloaded versions of the proxy function ice_invoke that use the array mapping. The version that omits the trailing Ice::Context argument is shown below:
bool ice_invoke(
    const std::string& operation,
    Ice::OperationMode mode,
    const std::pair< const Ice::Byte*, const Ice::Byte* >& in,
    std::vector< Ice::Byte >& out
);
A Blobject servant uses the array mapping by deriving its implementation class from Ice::BlobjectArray and overriding its ice_invoke function:
class BlobjectArray {
public:
    virtual bool ice_invoke(
        const std::pair<const Ice::Byte*, const Ice::Byte*>& in,
        std::vector<Ice::Byte>& out,
        const Ice::Current& current) = 0;
};
See Section 6.7.4 for more information on the array mapping.

35.3.4 Java Mapping

This section describes the Java mapping for the ice_invoke proxy function and the Blobject class.

ice_invoke

The mapping for ice_invoke is shown below:
boolean ice_invoke(
    String operation,
    Ice.OperationMode mode,
    byte[] inParams,
    Ice.ByteSeqHolder outParams
);
Another overloading of ice_invoke (not shown) adds a trailing argument of type Ice.Context (see Section 32.12).
As an example, the code below demonstrates how to invoke the operation op, which takes no in parameters:
Ice.ObjectPrx proxy = ...
try {
    Ice.ByteSeqHolder outParams = new Ice.ByteSeqHolder();
    if (proxy.ice_invoke("op", Ice.OperationMode.Normal, null,
                         outParams)) {
        // Handle success
    } else {
        // Handle user exception
    }
} catch (Ice.LocalException ex) {
    // Handle exception
}

Using Streams with ice_invoke

The streaming interface described in Section 35.2 provides the tools an applica­tion needs to dynamically invoke operations with arguments of any Slice type. Consider the following Slice definition:
module Calc {
    exception Overflow {
        int x;
        int y;
    };
    interface Compute {
        idempotent int add(int x, int y)
            throws Overflow;
    };
};
Now let’s write a client that dynamically invokes the add operation:
Ice.ObjectPrx proxy = ...
try {
    Ice.OutputStream out =
        Ice.Util.createOutputStream(communicator);
    out.writeInt(100); // x
    out.writeInt(1);  // y
    byte[] inParams = out.finished();
    Ice.ByteSeqHolder outParams = new Ice.ByteSeqHolder();
    if (proxy.ice_invoke("add", Ice.OperationMode.Idempotent,
                         inParams, outParams)) {
        // Handle success
        Ice.InputStream in =
            Ice.Util.createInputStream(communicator,
                                       outParams.value);
        int result = in.readInt();
        assert(result == 99);
    } else {
        // Handle user exception
    }
} catch (Ice.LocalException ex) {
    // Handle exception
}
We neglected to handle the case of a user exception in this example, so let’s imple­ment that now. We assume that we have compiled our program with the Slice-generated code, therefore we can call throwException on the input stream and catch Overflow directly3:
    if (proxy.ice_invoke("add", Ice.OperationMode.Idempotent,
                         inParams, outParams)) {
        // Handle success
        // ...
    } else {
        // Handle user exception
        Ice.InputStream in =
            Ice.Util.createInputStream(communicator,
                                       outParams.value);
        try {
            in.throwException();
        } catch (Calc.Overflow ex) {
            System.out.println("overflow while adding " + ex.x +
                               " and " + ex.y);
        } catch (Ice.UserException ex) {
            // Handle unexpected user exception
        }
    }
As a defensive measure, the code traps Ice.UserException. This could be raised if the Slice definition of add is modified to include another user exception but this segment of code did not get updated accordingly.

Subclassing Blobject

Implementing the dynamic dispatch model requires writing a subclass of Ice.Blobject. We continue using the Compute interface from page 1254 to demonstrate a Blobject implementation:
public class ComputeI extends Ice.Blobject {
    public boolean ice_invoke(
        byte[] inParams,
        Ice.ByteSeqHolder outParams,
        Ice.Current current)
    {
        // ...
    }
}
An instance of ComputeI is an Ice object because Blobject derives from Object, therefore an instance can be added to an object adapter like any other servant (see Chapter 32 for more information on object adapters).
For the purposes of this discussion, the implementation of ice_invoke handles only the add operation and raises OperationNotExistException for all other operations. In a real implementation, the servant must also be prepared to receive invocations of the following operations:
• string ice_id()
Returns the Slice type id of the servant’s most-derived type.
• StringSeq ice_ids()
Returns a sequence of strings representing all of the Slice interfaces supported by the servant, including "::Ice::Object".
• bool ice_isA(string id)
Returns true if the servant supports the interface denoted by the given Slice type id, or false otherwise. This operation is invoked by the proxy function checkedCast.
• void ice_ping()
Verifies that the object denoted by the identity and facet contained in Ice.Current is reachable.
With that in mind, here is our simplified version of ice_invoke:
    public boolean ice_invoke(
        byte[] inParams,
        Ice.ByteSeqHolder outParams,
        Ice.Current current)
    {
        if (current.operation.equals("add")) {
            Ice.Communicator communicator =
                current.adapter.getCommunicator();
            Ice.InputStream in =
                Ice.Util.createInputStream(communicator,
                                           inParams);
            int x = in.readInt();
            int y = in.readInt();
            Ice.OutputStream out =
                Ice.Util.createOutputStream(communicator);
            try {
                if (checkOverflow(x, y)) {
                    Calc.Overflow ex = new Calc.Overflow();
                    ex.x = x;
                    ex.y = y;
                    out.writeException(ex);
                    outParams.value = out.finished();
                    return false;
                } else {
                    out.writeInt(x + y);
                    outParams.value = out.finished();
                    return true;
                }
            } finally {
                out.destroy();
            }
        } else {
            Ice.OperationNotExistException ex =
                new Ice.OperationNotExistException();
            ex.id = current.id;
            ex.facet = current.facet;
            ex.operation = current.operation;
            throw ex;
        }
    }
If an overflow is detected, the code “raises” the Calc::Overflow user exception by calling writeException on the output stream and returning false, other­wise the return value is encoded and the function returns true.

35.3.5 C# Mapping

This section describes the C# mapping for the ice_invoke proxy function and the Blobject class.

ice_invoke

The mapping for ice_invoke is shown below:
namespace Ice
{
    public interface ObjectPrx
    {
        bool ice_invoke(string operation,
                        OperationMode mode,
                        byte[] inParams,
                        out byte[] outParams);
        // ...
    }
}
Another overloading of ice_invoke (not shown) adds a trailing argument of type Ice.Context (see Section 32.12).
As an example, the code below demonstrates how to invoke the operation op, which takes no in parameters:
Ice.ObjectPrx proxy = ...
try {
    byte[] outParams;
    if (proxy.ice_invoke("op", Ice.OperationMode.Normal, null,
                         outParams)) {
        // Handle success
    } else {
        // Handle user exception
    }
} catch (Ice.LocalException ex) {
    // Handle exception
}

Using Streams with ice_invoke

The streaming interface described in Section 35.2 provides the tools an applica­tion needs to dynamically invoke operations with arguments of any Slice type. Consider the following Slice definition:
module Calc {
    exception Overflow {
        int x;
        int y;
    };
    interface Compute {
        idempotent int add(int x, int y)
            throws Overflow;
    };
};
Now let’s write a client that dynamically invokes the add operation:
Ice.ObjectPrx proxy = ...
try {
    Ice.OutputStream outStream =
        Ice.Util.createOutputStream(communicator);
    outStream.writeInt(100); // x
    outStream.writeInt(1);  // y
    byte[] inParams = outStream.finished();
    byte[] outParams;
    if (proxy.ice_invoke("add", Ice.OperationMode.Idempotent,
                         inParams, out outParams)) {
        // Handle success
        Ice.InputStream inStream =
            Ice.Util.createInputStream(communicator, outParams);
        int result = inStream.readInt();
        System.Diagnostics.Debug.Assert(result == 99);
    } else {
        // Handle user exception
    }
} catch (Ice.LocalException ex) {
    // Handle exception
}
We neglected to handle the case of a user exception in this example, so let’s imple­ment that now. We assume that we have compiled our program with the Slice-generated code, therefore we can call throwException on the input stream and catch Overflow directly4:
    if (proxy.ice_invoke("add", Ice.OperationMode.Idempotent,
                         inParams, out outParams)) {
        // Handle success
        ...
    } else {
        // Handle user exception
        Ice.InputStream inStream =
            Ice.Util.createInputStream(communicator, outParams);
        try {
            inStream.throwException();
        } catch (Calc.Overflow ex) {
            System.Console.WriteLine("overflow while adding " +
                                     ex.x + " and " + ex.y);
        } catch (Ice.UserException) {
            // Handle unexpected user exception
        }
    }
As a defensive measure, the code traps Ice.UserException. This could be raised if the Slice definition of add is modified to include another user exception but this segment of code did not get updated accordingly.

Subclassing Blobject

Implementing the dynamic dispatch model requires writing a subclass of Ice.Blobject. We continue using the Compute interface from page 1258 to demonstrate a Blobject implementation:
public class ComputeI : Ice.Blobject {
    public bool ice_invoke(
                    byte[] inParams,
                    out byte[] outParams,
                    Ice.Current current);
    {
        ...
    }
}
An instance of ComputeI is an Ice object because Blobject derives from Object, therefore an instance can be added to an object adapter like any other servant (see Chapter 32 for more information on object adapters).
For the purposes of this discussion, the implementation of ice_invoke handles only the add operation and raises OperationNotExistException for all other operations. In a real implementation, the servant must also be prepared to receive invocations of the following operations:
• string ice_id()
Returns the Slice type id of the servant’s most-derived type.
• StringSeq ice_ids()
Returns a sequence of strings representing all of the Slice interfaces supported by the servant, including "::Ice::Object".
• bool ice_isA(string id)
Returns true if the servant supports the interface denoted by the given Slice type id, or false otherwise. This operation is invoked by the proxy function checkedCast.
• void ice_ping()
Verifies that the object denoted by the identity and facet contained in Ice.Current is reachable.
With that in mind, here is our simplified version of ice_invoke:
    public bool ice_invoke(
                    byte[] inParams,
                    out byte[] outParams,
                    Ice.Current current);
    {
        if (current.operation.Equals("add")) {
            Ice.Communicator communicator =
                current.adapter.getCommunicator();
            Ice.InputStream inStream =
                Ice.Util.createInputStream(communicator,
                                           inParams);
            int x = inStream.readInt();
            int y = inStream.readInt();
            Ice.OutputStream outStream =
                Ice.Util.createOutputStream(communicator);
            try {
                if (checkOverflow(x, y)) {
                    Calc.Overflow ex = new Calc.Overflow();
                    ex.x = x;
                    ex.y = y;
                    outStream.StreamwriteException(ex);
                    outParams = outStream.finished();
                    return false;
                } else {
                    outStream.writeInt(x + y);
                    outParams = outStream.finished();
                    return true;
                }
            } finally {
                outStream.destroy();
            }
        } else {
            Ice.OperationNotExistException ex =
                new Ice.OperationNotExistException();
            ex.id = current.id;
            ex.facet = current.facet;
            ex.operation = current.operation;
            throw ex;
        }
    }
If an overflow is detected, the code “raises” the Calc::Overflow user exception by calling writeException on the output stream and returning false, other­wise the return value is encoded and the function returns true.

1
This is the Slice name of the operation, not the name as it might be mapped to any particular language. For example, the string "while" is the name of the Slice operation while, and not "_cpp_while" (C++) or "_while" (Java).

2
This is obviously a contrived example: if the Slice-generated code is available, why bother using dynamic dispatch? In the absence of Slice-generated code, the caller would need to manually unmarshal the user exception, which is outside the scope of this book.

3
This is obviously a contrived example: if the Slice-generated code is available, why bother using dynamic dispatch? In the absence of Slice-generated code, the caller would need to manually unmarshal the user exception, which is outside the scope of this book.

4
This is obviously a contrived example: if the Slice-generated code is available, why bother using dynamic dispatch? In the absence of Slice-generated code, the caller would need to manually unmarshal the user exception, which is outside the scope of this book.


Table of Contents Previous Next
Logo