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. Underneath 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 32.1.
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 39) and IceStorm (
Chapter 41). 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 32.2.
It may be difficult to resist the temptation of using a feature like dynamic invocation 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.
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
CollocationOptimizationException. See
Section 28.21 for more information on this optimization and instructions for disabling it.
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 28.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. For example, the Ice extension for PHP (see
Chapter 24) uses the Slice parser library to parse Slice files at run time; the Slice description of the operation drives the decoding process. How a
Blobject subclass determines its type information is an implementation detail that is beyond the scope of this book.
This section describes the C++ mapping for the ice_invoke proxy function and the
Blobject class.
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 28.11).
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
}
The streaming interface described in Section 32.2 provides the tools an application 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;
};
};
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 implement 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 directly
2:
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.
Implementing the dynamic dispatch model requires writing a subclass of Ice::Blobject. We continue using the
Compute interface from
page 1075 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 28 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:
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.
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, otherwise the return value is encoded and the function returns
true.
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 invocation 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
);
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.
This section describes the Java mapping for the ice_invoke proxy function and the
Blobject class.
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 28.11).
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
}
The streaming interface described in Section 32.2 provides the tools an application 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;
};
};
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 implement 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 directly
3:
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.
Implementing the dynamic dispatch model requires writing a subclass of Ice.Blobject. We continue using the
Compute interface from
page 1080 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 28 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:
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.
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, otherwise the return value is encoded and the function returns
true.
This section describes the C# mapping for the ice_invoke proxy function and the
Blobject class.
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 28.11).
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
}
The streaming interface described in Section 32.2 provides the tools an application 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;
};
};
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.NonMutating,
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 implement 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 directly
4:
if (proxy.ice_invoke("add", Ice.OperationMode.NonMutating,
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.
Implementing the dynamic dispatch model requires writing a subclass of Ice.Blobject. We continue using the
Compute interface from
page 1084 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 28 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:
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.
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, otherwise the return value is encoded and the function returns
true.