From a CLI task to a run (main) loop

In the previous post, we explored the process of creating a simple Swift Command Line Tool with a one-time execution. It starts up, processes a task, then it closes. This is a useful and very common pattern - but applications wouldn’t be very useful if they always closed after executing a task. Any application with a Graphical User Interface runs until it’s told to stop or until it crashes - and the same applies to some server-side applications. We can change the way this application behaves simply by adding a simple event loop (also known as a main loop or run loop). In essence, the mechanism remains the same, regardless of the underlying frameworks (SwiftUI, UIKit, or technologies outside of Apple’s infrastructure):

  1. Your application performs some setup activities, either in a serial, synchronous manner - or asynchronously, concurrently

  2. Once the setup process is complete, the final instruction in the application’s entry point creates an infinite loop, with some very clear (and mandatory) exit/return/continue conditions. To see this in action, simply replace the content of the cli-example main.swift file with the snippet below:

print("Enter some text and I will repeat it, or enter 'q' to quit\n")

while true {
        print("what should I repeat?: ", terminator: "")
        
        guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) else {
            continue
        }
        
        // Check if user wants to quit
        if input.lowercased() == "q" {
            print("Goodbye!")
            break
        }
        
        // Skip empty input
        if input.isEmpty {
            print("Please enter some text or 'q' to quit\n")
            continue
        }
        
        print("Input was: \(input)")

}
print("Enter some text and I will repeat it, or enter 'q' to quit\n")

while true {
        print("what should I repeat?: ", terminator: "")
        
        guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) else {
            continue
        }
        
        // Check if user wants to quit
        if input.lowercased() == "q" {
            print("Goodbye!")
            break
        }
        
        // Skip empty input
        if input.isEmpty {
            print("Please enter some text or 'q' to quit\n")
            continue
        }
        
        print("Input was: \(input)")

}

If you would run the new executable, you would essentially have a process with an event loop ( in an overly simplified manner, this is mimics very well how more complex applications work).

$\> swift run cli-example
Building for debugging...
[7/7] Applying cli-example
Build of product 'cli-example' complete! (0.81s)
Enter some text and I will repeat it, or enter 'q' to quit

what should I repeat?: Something
Input was: Something
what should I repeat?: Something else
Input was: Something else
what should I repeat?: 
Please enter some text or 'q' to quit

what should I repeat?: q
Goodbye!


A Swift Program - from source code to executable file

When developing applications for Apple’s ecosystem, you would spend a good portion of your time writing Swift source code files. Sometimes, you may need to use Objective-C, C or C++, but even then, you would mostly write source code. While writing Swift source code, you use components (libraries) included in the Swift versions of Apple’s numerous frameworks - one of which being SwiftUI - and you ensure your code respects the requirements of Apple’s frameworks, into which your code would need to integrate.

Since Swift is a compiled language, when your program’s code is ready, you need to compile it. So far, we have seen how a simple Swift program is created by writing text into files - and then how Swift’s toolset transforms that text into another set of files, such as object files, dynamic library files and, importantly, the executable file. We have explored the main mechanisms you can use to describe an application, using concepts (symbols and abstractions) you find in the Swift programming language. We have then looked at the build process and how it translates those source files into binary object files, then bundles everything together into one, Mach-O executable file you can use to execute the program.

Between the source code files and the final executable files, the LLVM toolset (Compiler, Assembler, Linker) performs various other, intermediary tasks:

  • Perform Lexical and Syntactic Analysis (this is where compilation due to syntax errors would fail);

  • Perform Semantic Analysis (for type checking);

  • Generate intermediary LLVM IR files (LLVM Intermediate Representation files, which are target-independent files written in the LLVM Assembly Language)

  • Generate platform-specific assembly language files

  • Generate the Binary Object files, from the platform specific assembly language files

  • Bundle everything into an executable file, usable by the Operating System’s kernel ( in this particular case, theMach-O executable file for the XNU kernel )

More complex projects (such as an application with a GUI) would also include many other components, which are used while the application is running (from icons to pre-rendered sprites to interface builder files)

The diagram below represents a simplified overview of the numerous transformations your Swift Code will go through.

 

A Swift Program - from the Human Domain to the CPU Domain

 

Once your program is bundled into a Mach-O executable file, it can be started by the Operating System.

Next
Next

Exploring Swift - Creating a simple CLI tool