Server-Side Swift Mapping for Interfaces

The server-side mapping for interfaces provides an up-call API for the Ice run time: by implementing instance methods in a servant class, you provide the hook that gets the thread of control from the Ice server-side run time into your application code.

On this page:

Skeleton Types in Swift

On the client side, Slice interfaces map to proxy protocols. On the server side, interfaces map to skeleton protocols and request dispatcher structs. A skeleton protocol specifies an instance method for each operation on the corresponding Slice interface. For example, consider our Slice definition for the Node interface:

Slice
module Filesystem
{
    interface Node
    {
        idempotent string name();
    }
    // ...
}

The Slice compiler generates the following definitions for this interface:

Swift
// Skeleton protocol
public protocol Node {
    func name(current: Ice.Current) throws -> String
}

// Dispatcher struct
public struct NodeDisp: Ice.Disp {
    public let servant: Node

    public init(_ servant: Node) { ... }
	...
}

As for the client side, Slice modules are mapped to Slice modules with the same name, so the skeleton protocol and request dispatcher struct are part of Filesystem Swift module. Swift module references are generated only for references to types outside the current module, to allow you to compile the definitions for a single Slice module directly into your application without creating a Swift module.

Request Dispatcher Structs

The generated request dispatcher structs are unique to the Swift language mapping. A request dispatcher struct is a helper that holds a servant. You use such a struct to add your servant to an object adapter: while in other language mapping you add servants directly to your object adapter, in Swift you add a request dispatcher that holds your servant.

Object Protocol

Object (the implicit base interface of all Slice interfaces) is mapped to the Ice.Object protocol in Swift:

Swift
public protocol Object {
    func ice_isA(id: String, current: Current) throws -> Bool
    func ice_ping(current: Current) throws
    func ice_ids(current: Current) throws -> [String]
    func ice_id(current: Current) throws -> String
}

The methods of Object behave as follows:

  • ice_isA
    This method returns true if the target object implements the given type ID, and false otherwise.
  • ice_ping
    ice_ping provides a basic reachability test for the servant.
  • ice_ids
    This method returns a string sequence representing all of the type IDs implemented by this servant, including ::Ice::Object.
  • ice_id
    This method returns the type ID of the most-derived interface implemented by this servant.

Servant Classes in Swift

In order to provide an implementation for an Ice object, you must create a servant class or struct that adopts the corresponding skeleton protocol. For example, to create a servant for the Node interface, you could write:

Swift
class NodeI: Node {
    private let name: String
    
    init(_ name: String) {
		self.name = name
    }

    func name(current _: Ice.Current) {
        return name
    }
}

By convention, servant classes or struct have the name of their interface with an I-suffix, so the servant class for the Node interface is called NodeI. (This is a convention only: as far as the Ice run time is concerned, you can choose any name you prefer for your servants.)

As far as Ice is concerned, the NodeI class must implement only a single method: the method name specified in the skeleton protocol. You can add other methods and properties as you see fit to support your implementation. For example, in the preceding definition, we added a name stored property.

Using Servant Structs in Swift

You can implement your Swift servant with a Swift class or a struct that adopts the corresponding skeleton protocol. Structs are suitable for stateless servants, or servants with read-only properties that are cheap to copy. For all other servants, you should use classes.

The Slice to Swift compiler does not provide any way to add mutating to the methods of generated skeleton protocols.

Normal and idempotent Operations in Swift

Whether an operation is an ordinary operation or an idempotent operation has no influence on the way the operation is mapped. To illustrate this, consider the following interface:

Slice
interface Example
{
    void normalOp();
    idempotent void idempotentOp();
    idempotent string readonlyOp();
}

The methods for the mapped protocol look like this:

Swift
func normalOp(current: Ice.Current)
func idempotentOp(current: Ice.Current)
func readonlyOp(current: Ice.Current) -> String

Note that the signatures of the methods are unaffected by the idempotent qualifier.

Implementing Object's Operations in Swift

The skeleton protocols generated by the Slice to Swift compiler, like Node shown earlier on this page, do not inherit from Ice.Object. This means that when you implement a skeleton protocol, you don't need to implement the methods from Ice.Object–but you can if you like.

If you wish to provide your own implementation for Object's operations (ice_idice_idsice_isAice_ping), make your servant implementation adopt the Ice.Object protocol.  For example:

Swift
// Adopts both Node and Object, and provides a custom implementation of Object's operations
class NodeI: Node, Ice.Object {
   ...
}

If your servant class or struct does not adopt the Ice.Object protocol,  the dispatcher struct dispatches to a default implementation of these operations. This default implementation is type-dependent: the default implementation for Node's ice_id, ice_ids and ice_isA use the type-ids for Node, and most of the time this default implementation is all you need.

You can also inherit from this default implementation to customize the implementation of just one method of Ice.Object. For example:

Swift
class NodeI: Ice.ObjectI<NodeTraits>, Node {
   ...
   override func ice_ping(current _: Ice.Current) {
       print("received ice_ping")
   }
}

nameTraits, where name represents the name of your interface or class, is a struct generated by the Slice to Swift compiler that adopts the Ice.SliceTraits protocol:

Swift
public protocol SliceTraits {
    static var staticIds: [String] { get }
    static var staticId: String { get }
}

Ice.ObjectI is an open class that uses your interface's traits to implement Ice.Object methods:

Swift
open class ObjectI<T: SliceTraits>: Object {
    public init() {}

    open func ice_id(current _: Current) throws -> String {
        return T.staticId
    }

    open func ice_ids(current _: Current) throws -> [String] {
        return T.staticIds
    }

    open func ice_isA(id: String, current _: Current) throws -> Bool {
        return T.staticIds.contains(id)
    }

    open func ice_ping(current _: Current) throws {
        // Do nothing
    }
}

The methods of ObjectI are marked throws even though they don't throw anything. This is just to allow you to override them with methods that throw exceptions.


See Also