Swift Mapping for Interfaces

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 Protocols in Swift

On the client side, a Slice interface maps to an empty Swift protocol. A public extension of this protocol provides two methods for each Slice operation of your Slice interface.

Consider the following simple interface:

Slice
module M
{
    interface Simple
    {
        void op();
    }
}

The Slice compiler generates the following definitions for use by the client:

Swift
// in module M

public protocol SimplePrx: Ice.ObjectPrx {}

public extension SimplePrx {
    func op(context: Ice.Context? = nil) throws {
        // ...
    }
    
    func opAsync(context: Ice.Context? = nil, 
                 sentOn: DispatchQueue? = nil, 
                 sentFlags: DispatchWorkItemFlags? = nil,
                 sent: ((Bool) -> Void)? = nil,) -> PromiseKit.Promise<Void> {
        // ... 
    }
}

As you can see, the compiler generates a proxy protocol SimplePrx. In general, the generated name is <interface-name>Prx.

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 ObjectPrx. This reflects the fact that all Slice interfaces implicitly inherit from Ice::Object.

For each operation in the interface, the proxy protocol extension provides a method with the same name plus a method with the Async suffix. For the preceding example, we find that the operation op has been mapped to the methods op and opAsync. These methods have an optional context parameter that we examine in detail in Request Contexts.

Because all the <interface-name>Prx types are protocols, you cannot instantiate an object of such a type. Instead, proxy instances are always instantiated on behalf of the client by the Ice run time, so client code never has any need to instantiate a proxy directly. The proxies handed out by the Ice run time are always of type <interface-name>Prx; the concrete implementation of the interface is part of the Ice run time and does not concern application code.

Interface Inheritance in Swift

Inheritance relationships among Slice interfaces are maintained in the generated Swift protocols. For example:

Slice
module M
{
    interface A { ... }
    interface B { ... }
    interface C extends A, B { ... }
}

The generated code for CPrx reflects the inheritance hierarchy:

Swift
public protocol CPrx: APrx, BPrx {}

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.

The ObjectPrx Protocol

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

Swift
public protocol ObjectPrx {
    func ice_getIdentity() -> Identity
    // ...
}

public extension ObjectPrx {
    func ice_isA(id: String, context: Context? = nil) throws -> Bool {
        // ...
    }
    func ice_isAAsync(id: String, context: Context? = nil,
                      sentOn: DispatchQueue? = nil,
                      sentFlags: DispatchWorkItemFlags? = nil,
                      sent: ((Bool) -> Void)? = nil) -> Promise<Bool> {
        // ...
    }
    
    func ice_ids(context: Context? = nil) throws -> StringSeq {
        // ...
    }
    func ice_idsAsync(context: Context? = nil,
                      sentOn: DispatchQueue? = nil,
                      sentFlags: DispatchWorkItemFlags? = nil,
                      sent: ((Bool) -> Void)? = nil) -> Promise<StringSeq> {
        // ...
    }

    func ice_id(context: Context? = nil) throws -> String {
        // ...
    }
    func ice_idAsync(context: Context? = nil,
                     sentOn: DispatchQueue? = nil,
                     sentFlags: DispatchWorkItemFlags? = nil,
                     sent: ((Bool) -> Void)? = nil) -> Promise<String> {
        // ...
    }

    func ice_ping(context: Context? = nil) throws {
        // ...
    }
    func ice_pingAsync(context: Context? = nil,
                       sentOn: DispatchQueue? = nil,
                       sentFlags: DispatchWorkItemFlags? = nil,
                       sent: ((Bool) -> Void)? = nil) -> Promise<Void>{
        // ...
    }
    
    // ...
}

The methods behave as follows:

  • ice_getIdentity
    This method returns the identity of the object denoted by the proxy. The identity of an Ice object has the following Slice type:

    Slice
    module Ice
    {
        struct Identity
        {
            string name;
            string category;
        }
    }
    

    To see whether two proxies denote the same object, first obtain the identity for each object and then compare the identities:

    Swift
    let o1: Ice.ObjectPrx = ... // first proxy
    let o2: Ice.ObjectPrx = ... // second proxy
    let i1 = o1.ice_getIdentity()
    let i2 = o2.ice_getIdentity()
    
    if i1 == i2 {
        // o1 and o2 denote the same object
    } else {
        // o1 and o2 denote different objects
    }

    ice_getIdentity is always a local call: the identity is extracted from the proxy object.

  • ice_isA
    The ice_isA method determines whether the object denoted by the proxy supports a specific interface. The argument to ice_isA is a type ID. For example, to see whether a proxy of type ObjectPrx denotes a Printer object, we can write:

    Swift
    let o: Ice.ObjectPrx = ...
    if try o.ice_isA("::M::Printer") {
        // o denotes a M::Printer object
    } else {
        // o denotes some other type of object
    }
  • ice_ids
    The ice_ids method returns an array of strings representing all of the type IDs that the object denoted by the proxy supports.
  • ice_id
    The ice_id method returns the type ID of the object denoted by the proxy. Note that the type returned is the type of the actual object, which may be more derived than the static type of the proxy. For example, if we have a proxy of type BasePrx, with a static type ID of ::M::Base, the return value of ice_id might be ::M::Base, or it might something more derived, such as ::M::Derived.
  • ice_ping
    The ice_ping method provides a basic reachability test for the object. If the object can physically be contacted (that is, the object exists and its server is running and reachable), the call completes normally; otherwise, it throws an exception that indicates why the object could not be reached, such as ObjectNotExistException or ConnectTimeoutException.

The ice_isAice_idsice_id, and ice_ping methods are remote operations and therefore support an additional overloading that accepts a request context. There are many other methods in ObjectPrx, not shown here.

Proxy Helper Functions in Swift

For each proxy, the Slice compiler generate 3 helper functions in the same Swift module, checkedCastuncheckedCast and ice_staticId:

Swift
public protocol SimplePrx: Ice.ObjectPrx {}

public func checkedCast(prx: Ice.ObjectPrx,
                        type: SimplePrx.Protocol, 
                        facet: String? = nil, 
                        context: Ice.Context? = nil) throws -> SimplePrx? {
    // ...
}
public func uncheckedCast(prx: Ice.ObjectPrx, 
                          type: SimplePrx.Protocol, 
                          facet: String? = nil) -> SimplePrx {
    // ...
}

public func ice_staticId(_ type: SimplePrx.Protocol) -> String {
    // ...
}

Checked cast

A checked cast has the same function for proxies as as? in Swift. It allows you to assign a base proxy to a derived proxy. If the type of the base proxy's target object is compatible with the derived proxy's type, the assignment succeeds and, after the assignment, the derived proxy denotes the same remote object as the base proxy. Otherwise, if the type of the base proxy's target object is incompatible with the derived proxy's type, checkedCast returns nil. Here is an example to illustrate this:

Swift
let base: BasePrx = ... // Initialize base proxy
if let derived = try checkedCast(prx: base, type: DerivedPrx.self) {
    // base's target object has run-time type Derived,
    // use derived (a DerivedPrx)
} else {
    // Base has some other, unrelated type
}

The expression checkedCast(prx: base, type: DerivedPrx.self) tests whether base points at an object of type Derived (or an object with a type that is derived from Derived). If so, the cast succeeds and derived is set to point at the same remote Ice object as base. Otherwise, the cast fails and checkedCast returns nil.

checkedCast always results in a remote message, ice_isA, to the server. The message effectively asks the server "is the object denoted by this proxy of type Derived?".

When ice_isA returns true, checkedCast manufactures a new proxy instance that adopts the desired protocol and returns this instance.

Sending a remote message is necessary because, as a rule, there is no way for the client to find out what the actual run-time type of a remote Ice object is without confirmation from the server. (For example, the server may replace the implementation of the object for an existing proxy with a more derived one.) This means that you have to be prepared for a checkedCast to fail. For example, if the server is not running, you will receive a ConnectFailedException; if the server is running, but the object denoted by the proxy no longer exists, you will receive an ObjectNotExistException.

Unchecked cast

In some cases, it is known that a remote object supports a more derived interface than the static type of its proxy. For such cases, you can use an unchecked down-cast. An uncheckedCast provides a down-cast without consulting the server as to the actual run-time type of the object, for example:

Swift
let base: BasePrx = ...  // Initialize to point at a Derived remote Ice object
let derived = uncheckedCast(prx: base, type: DerivedPrx.self)
// Use derived...

You should use an uncheckedCast only if you are certain that target object indeed supports the more derived type: an uncheckedCast, as the name implies, is not checked in any way; it does not contact the object in the server and cannot fail. If you use the proxy resulting from an incorrect uncheckedCast to invoke an operation, the behavior is undefined. Most likely, you will receive an OperationNotExistException, but, depending on the circumstances, the Ice run time may also report an exception indicating that unmarshaling has failed, or even silently return garbage results.

Calling uncheckedCast on a proxy that is already of the desired proxy type returns immediately that proxy. Otherwise, uncheckedCast creates a new instance of the desired proxy type.

Despite its dangers, uncheckedCast is useful because it avoids the cost of sending a message to the server. And, particularly during initialization, it is common to receive a proxy of static type Ice.ObjectPrx, but with a known run-time type. In such cases, an uncheckedCast saves the overhead of sending a remote message.

ice_staticId

Another helper function generated for every interface 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".

Using Proxy Methods in Swift

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:

Swift
var proxy = try 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, the corresponding Swift method returns a proxy of the same type as the current proxy, therefore it is generally not necessary to down-cast after calling such a factory method. The example below demonstrates these semantics:

Swift
guard let base = try communicator.stringToProxy(...),
      var hello = try checkedCast(prx: base, type: HelloPrx.self) else {
   // base is not a Hello - handle this situation 
}
hello = hello.ice_invocationTimeout(10000) // Type of hello (HelloPrx) is preserved
try 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.

Proxy Comparison in Swift

Proxies can be compared for equality using the == operator:

Swift
public func == (lhs: ObjectPrx?, rhs: ObjectPrx?) -> Bool {
    // ...
}

public func != (lhs: ObjectPrx?, rhs: ObjectPrx?) -> Bool {
    return !(lhs == rhs)
}

Proxy comparison with == 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 invocation timeout and endpoint information, must be the same. In other words, comparison with == tests for proxy identity, not object identity. 

See Also