Example of a File System Server in Swift

This page presents the source code for a Swift server that implements our file system and communicates with the client we wrote earlier. The code is fully functional.

Note that the server is remarkably free of code that relates to distribution: most of the server code is simply application logic that would be present just the same for a non-distributed version. Again, this is one of the major advantages of Ice: distribution concerns are kept away from the application code so that you can concentrate on developing application logic instead of networking infrastructure.

On this page:

Implementing a File System Server in Swift

We have now seen enough of the server-side Swift mapping to implement a server for our file system. (You may find it useful to review these Slice definitions before studying the source code.)

Our server is composed of a single source file, main.swift.

Server Main Program in Swift

Our server main program (in main.swift) consists of a run function that creates and destroys an Ice communicator, creates an object adapter and instantiates our file system objects:

Swift
import Foundation
import Ice
func run() -> Int32 {
    do {
        let communicator = try Ice.initialize(CommandLine.arguments)
        defer {
            communicator.destroy()
        }

        //
        // Create an object adapter.
        //
        let adapter = try communicator.createObjectAdapterWithEndpoints(name: "SimpleFilesystem",
                                                                        endpoints: "default -h localhost -p 10000")

        //
        // Create the root directory (with name "/" and no parent)
        //
        let root = DirectoryI(name: "/", parent: nil)
        try root.activate(adapter: adapter)

        //
        // Create a file called "README" in the root directory
        //
        var file = FileI(name: "README", parent: root)
        file.write(text: ["This file system contains a collection of poetry."],
                   current: Ice.Current())
        try file.activate(adapter: adapter)

        //
        // Create a directory called "Coleridge" in the root directory
        //
        let coleridge = DirectoryI(name: "Coleridge", parent: root)
        try coleridge.activate(adapter: adapter)

        //
        // Create a file called "Kubla_Khan" in the Coleridge directory
        //
        file = FileI(name: "Kubla_Khan", parent: coleridge)
        file.write(text: ["In Xanadu did Kubla Khan",
                          "A stately pleasure-dome decree:",
                          "Where Alph, the sacred river, ran",
                          "Through caverns measureless to man",
                          "Down to a sunless sea."],
                   current: Ice.Current())
        try file.activate(adapter: adapter)

        //
        // All objects are created, allow client requests now
        //
        try adapter.activate()

        //
        // Wait until we are done
        //
        communicator.waitForShutdown()
        return 0
    } catch {
        print("Error: \(error)\n")
        return 1
    }
}

exit(run())

 Much of this code is boilerplate: we create a communicator, then an object adapter, and, towards the end, activate the object adapter and call waitForShutdown.

The interesting part of the code follows the adapter creation: here, the server instantiates a few nodes for our file system to create the structure shown below:


A small file system.

As we will see shortly, the servants for our directories and files are of type DirectoryI and FileI, respectively. The initializer for either type of servant accepts two parameters, the name of the directory or file to be created and a reference to the servant for the parent directory. (For the root directory, which has no parent, we pass a nil parent.) Thus, the statement

Swift
let root = DirectoryI(name: "/", parent: nil)

creates the root directory, with the name "/" and no parent directory.

Here is the code that establishes the structure in the above illustration:

Swift
        let root = DirectoryI(name: "/", parent: nil)
        try root.activate(adapter: adapter)

        var file = FileI(name: "README", parent: root)
        file.write(text: ["This file system contains a collection of poetry."],
                   current: Ice.Current())
        try file.activate(adapter: adapter)

        let coleridge = DirectoryI(name: "Coleridge", parent: root)
        try coleridge.activate(adapter: adapter)

        file = FileI(name: "Kubla_Khan", parent: coleridge)
        file.write(text: ["In Xanadu did Kubla Khan",
                          "A stately pleasure-dome decree:",
                          "Where Alph, the sacred river, ran",
                          "Through caverns measureless to man",
                          "Down to a sunless sea."],
                  current: Ice.Current())
        try file.activate(adapter: adapter)

We first create the root directory and a file README within the root directory. (Note that we pass a reference to the root directory as the parent when we create the new node of type FileI.)

After creating each servant, the code calls activate on the servant. (We will see the definition of this method shortly.) activate adds the servant to the ASM (Adapter Servant Map).

The next step is to fill the file with text:

Swift
        file.write(text: ["This file system contains a collection of poetry."],
                   current: Ice.Current())

Recall that Slice sequences by default map to Swift arrays. The Slice type Lines is simply an array of strings; we add a line of text to our README file by initializing the text array to contain one element.

This statement is interesting: the server code invokes an operation on one of its own servants. Because the call happens via a reference to the servant (of type FileI) and not via a proxy (of type FilePrx), the Ice run time does not know that this call is even taking place — such a direct call into a servant is not mediated by the Ice run time in any way and is dispatched as an ordinary Swift function call. We need to provide an Ice.Current instance for such direct call–for example, an empty Current.

In similar fashion, the remainder of the code creates a subdirectory called Coleridge and, within that directory, a file called Kubla_Khan to complete the structure in the illustration listed above.

NodeI Servant Class in Swift

Our NodeI servant class has the following basic structure:

Swift
class NodeI: Node {
    private let name: String
    private let parent: DirectoryI?

    init(name: String, parent: DirectoryI?) {
        self.name = name
        self.parent = parent
    }

    ...
}

The class has two properties:

  • name
    This property stores the name of the file incarnated by the servant.
  • parent
    This property stores the reference to the servant for the node's parent directory.

The name and parent properties are initialized by the init method.

NodeI also provides the implementation for the Slice operation name and two helper methods, activate and makeDisp:

Swift
    func name(current _: Ice.Current) -> String {
        return name
    }

    func activate(adapter: Ice.ObjectAdapter) throws {
        let id = Ice.Identity(name: parent == nil ? "RootDir" : UUID().uuidString,
                              category: "")
        let prx = try adapter.add(servant: makeDisp(), id: id)
        parent?.addChild(child: uncheckedCast(prx: prx, type: NodePrx.self))
    }

    func makeDisp() -> Ice.Disp {
        fatalError("Abstract method")
    }

activate generates an identity for the node: "RootDir" when parent is null, and a random ID otherwise. activate then creates a dispatcher for self and adds it to the object adapter. And finally, when the node has a parent directory, activate registers a proxy to self with the parent directory.

The makeDisp helper method creates a dispatcher struct for self (self being a servant).  For Node, we implement it with fatalError because we only want to create File and Directory servants, and never plain Node servants.

FileI Servant Class in Swift

Our FileI servant class simply reuses the NodeI implementation, implements read and write, and overrides the makeDisp helper method:

Swift
class FileI: NodeI, File {
    private var lines: [String] = []

    // Slice File::read() operation
    func read(current _: Ice.Current) -> [String] {
        return lines
    }

    // Slice File::write() operation
    func write(text: [String], current _: Ice.Current) {
        lines = text
    }

    // Create a dispatcher for servant `self`
    override func makeDisp() -> Ice.Disp {
        return FileDisp(self)
    }
}

DirectoryI Servant Class in Swift

The DirectoryI class reuses NodeI, implements list and addChild, and overrides the helper method makeDisp:

Swift
class DirectoryI: NodeI, Directory {
    private var contents: [NodePrx?] = []

    func list(current _: Ice.Current) -> [NodePrx?] {
        return contents
    }

    func addChild(child: NodePrx) {
        contents.append(child)
    }

    override func makeDisp() -> Ice.Disp {
        return DirectoryDisp(self)
    }
}

See Also