The mapping of Slice interfaces revolves around the idea that, to invoke a remote operation, you call a member function on a local class instance that is a proxy for the remote object. This makes the mapping easy and intuitive to use because making a remote procedure call is no different from making a local procedure call (apart from error semantics).

On this page:

Proxy Classes in Ruby

On the client side, a Slice interface maps to a Ruby class with methods that correspond to the operations on those interfaces. Consider the following simple interface:

interface Simple
{
    void op();
}

The Ruby mapping generates the following definition for use by the client:

class SimplePrx < Ice::ObjectPrx
    def op(context=nil)
        # ...
    end
 
    def SimplePrx.ice_staticId()
        # ...
    end
    # ...
end

In the client's address space, an instance of SimplePrx is the local ambassador for a remote instance of the Simple interface in a server and is known as a proxy instance. All the details about the server-side object, such as its address, what protocol to use, and its object identity are encapsulated in that instance.

Note that SimplePrx inherits from Ice::ObjectPrx. This reflects the fact that all Ice interfaces implicitly inherit from Ice::Object.

For each operation in the interface, the proxy class has a method of the same name. In the preceding example, we find that the operation op has been mapped to the method op. Note that op accepts an optional trailing parameter context representing the operation context. This parameter is a Ruby hash value for use by the Ice run time to store information about how to deliver a request. You normally do not need to use it. (We examine the context parameter in detail in Request Contexts. The parameter is also used by IceStorm.)

Proxy instances are always created on behalf of the client by the Ice run time, so client code never has any need to instantiate a proxy directly.

A value of nil denotes the null proxy. The null proxy is a dedicated value that indicates that a proxy points "nowhere" (denotes no object).

Another method defined by every proxy class is ice_staticId, which returns the type ID string corresponding to the interface. As an example, for the Slice interface Simple in module M, the string returned by ice_staticId is "::M::Simple".

Interface Inheritance in Ruby

Inheritance relationships among Slice interfaces are maintained in the generated Ruby classes. For example:

interface A { ... }
interface B { ... }
interface C extends A, B { ... }

The generated code for CPrx uses mixins to include the operations of APrx and BPrx:

module CPrx_mixin
    include APrx_mixin
    include BPrx_mixin
end

class CPrx < ::Ice::ObjectPrx
    include ::Ice::Proxy_mixin
    include CPrx_mixin
end

Given a proxy for C, a client can invoke any operation defined for interface C, as well as any operation inherited from C's base interfaces.

Ice::ObjectPrx Class in Ruby

All Ice objects have Object as the ultimate ancestor type, so all proxies inherit from Ice::ObjectPrx. ObjectPrx provides a number of methods:

class ObjectPrx
    def eql?(proxy)
    def ice_getIdentity
    def ice_isA(id)
    def ice_ids
    def ice_id
    def ice_ping
    # ...
end

The methods behave as follows:

The ice_isA, ice_ids, ice_id, and ice_ping methods are remote operations and therefore support an additional overloading that accepts a request context. Also note that there are other methods in ObjectPrx, not shown here. These methods provide different ways to dispatch a call and also provide access to an object's facets.

Casting Proxies in Ruby

The Ruby mapping for a proxy also generates two class methods:

class SimplePrx < Ice::ObjectPrx
    # ...

    def SimplePrx.checkedCast(proxy, facet='', context={})

    def SimplePrx.uncheckedCast(proxy, facet='')
end

The method names checkedCast and uncheckedCast are reserved for use in proxies. If a Slice interface defines an operation with either of those names, the mapping escapes the name in the generated proxy by prepending an underscore. For example, an interface that defines an operation named checkedCast is mapped to a proxy with a method named _checkedCast.

For checkedCast, if the passed proxy is for an object of type Simple, or a proxy for an object with a type derived from Simple, the cast returns a reference to a proxy of type SimplePrx; otherwise, if the passed proxy denotes an object of a different type (or if the passed proxy is nil), the cast returns nil.

Given a proxy of any type, you can use a checkedCast to determine whether the corresponding object supports a given type, for example:

obj = ...       # Get a proxy from somewhere...

simple = SimplePrx::checkedCast(obj)
if simple
    # Object supports the Simple interface...
else
    # Object is not of type Simple...
end

Note that a checkedCast contacts the server. This is necessary because only the server implementation has definite knowledge of the type of an object. As a result, a checkedCast may throw a ConnectTimeoutException or an ObjectNotExistException.

In contrast, an uncheckedCast does not contact the server and unconditionally returns a proxy of the requested type. However, if you do use an uncheckedCast, you must be certain that the proxy really does support the type you are casting to; otherwise, if you get it wrong, you will most likely get a run-time exception when you invoke an operation on the proxy. The most likely error for such a type mismatch is OperationNotExistException. However, other exceptions, such as a marshaling exception are possible as well. And, if the object happens to have an operation with the correct name, but different parameter types, no exception may be reported at all and you simply end up sending the invocation to an object of the wrong type; that object may do rather nonsensical things. To illustrate this, consider the following two interfaces:

interface Process
{
    void launch(int stackSize, int dataSize);
}

// ...

interface Rocket
{
    void launch(float xCoord, float yCoord);
}

Suppose you expect to receive a proxy for a Process object and use an uncheckedCast to down-cast the proxy:

obj = ...                                # Get proxy...
process = ProcessPrx::uncheckedCast(obj) # No worries...
process.launch(40, 60)                   # Oops...

If the proxy you received actually denotes a Rocket object, the error will go undetected by the Ice run time: because int and float have the same size and because the Ice protocol does not tag data with its type on the wire, the implementation of Rocket::launch will simply misinterpret the passed integers as floating-point numbers.

In fairness, this example is somewhat contrived. For such a mistake to go unnoticed at run time, both objects must have an operation with the same name and, in addition, the run-time arguments passed to the operation must have a total marshaled size that matches the number of bytes that are expected by the unmarshaling code on the server side. In practice, this is extremely rare and an incorrect uncheckedCast typically results in a run-time exception.

Using Proxy Methods in Ruby

The base proxy class ObjectPrx supports a variety of methods for customizing a proxy. Since proxies are immutable, each of these "factory methods" returns a copy of the original proxy that contains the desired modification. For example, you can obtain a proxy configured with a ten second invocation timeout as shown below:

proxy = communicator.stringToProxy(...)
proxy = proxy.ice_invocationTimeout(10000)

A factory method returns a new proxy object if the requested modification differs from the current proxy, otherwise it returns the current proxy. With few exceptions, factory methods return a proxy of the same type as the current proxy, therefore it is generally not necessary to repeat a down-cast after using a factory method. The example below demonstrates these semantics:

base = communicator.stringToProxy(...)
hello = Demo::HelloPrx::checkedCast(base)
hello = hello.ice_invocationTimeout(10000) # Type is not discarded
hello.sayHello()

The only exceptions are the factory methods ice_facet and ice_identity. Calls to either of these methods may produce a proxy for an object of an unrelated type, therefore they return a base proxy that you must subsequently down-cast to an appropriate type.

Object Identity and Proxy Comparison in Ruby

Proxy objects support comparison using the comparison operators ==, !=, and <=>, as well as the eql? method. Note that proxy comparison uses all of the information in a proxy for the comparison. This means that not only the object identity must match for a comparison to succeed, but other details inside the proxy, such as the protocol and endpoint information, must be the same. In other words, comparison tests for proxy identity, not object identity. A common mistake is to write code along the following lines:

p1 = ...        # Get a proxy...
p2 = ...        # Get another proxy...

if p1 != p2
    # p1 and p2 denote different objects       # WRONG!
else
    # p1 and p2 denote the same object         # Correct
end

Even though p1 and p2 differ, they may denote the same Ice object. This can happen because, for example, both p1 and p2 embed the same object identity, but each uses a different protocol to contact the target object. Similarly, the protocols may be the same, but denote different endpoints (because a single Ice object can be contacted via several different transport endpoints). In other words, if two proxies compare equal, we know that the two proxies denote the same object (because they are identical in all respects); however, if two proxies compare unequal, we know absolutely nothing: the proxies may or may not denote the same object.

To compare the object identities of two proxies, you can use helper functions in the Ice module:

def proxyIdentityCompare(lhs, rhs)
def proxyIdentityAndFacetCompare(lhs, rhs)

proxyIdentityCompare allows you to correctly compare proxies for identity:

p1 = ...        # Get a proxy...
p2 = ...        # Get another proxy...

if Ice.proxyIdentityCompare(p1, p2) != 0
    # p1 and p2 denote different objects       # Correct
else
    # p1 and p2 denote the same object         # Correct
end

The function returns 0 if the identities are equal, -1 if p1 is less than p2, and 1 if p1 is greater than p2. (The comparison uses name as the major sort key and category as the minor sort key.)

The proxyIdentityAndFacetCompare function behaves similarly, but compares both the identity and the facet name.

See Also