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:
module M { interface Simple { void op(); } }
The Slice compiler generates the following definitions for use by the client:
// 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:
module M { interface A { ... } interface B { ... } interface C extends A, B { ... } }
The generated code for CPrx
reflects the inheritance hierarchy:
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 ObjectPrx
. ObjectPrx
provides a number of methods:
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:Slicemodule 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:
Swiftlet 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
Theice_isA
method determines whether the object denoted by the proxy supports a specific interface. The argument toice_isA
is a type ID. For example, to see whether a proxy of typeObjectPrx
denotes aPrinter
object, we can write:Swiftlet 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
Theice_ids
method returns an array of strings representing all of the type IDs that the object denoted by the proxy supports.
ice_id
Theice_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 typeBasePrx
, with a static type ID of::M::Base
, the return value ofice_id
might be::M::Base
, or it might something more derived, such as::M::Derived
.
ice_ping
Theice_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 asObjectNotExistException
orConnectTimeoutException
.
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. 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, checkedCast
, uncheckedCast
and ice_staticId
:
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:
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.
A 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:
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:
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:
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:
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.