Application Architecture Overview

Introduction

The general architecture of Leapfrog can be defined as a stack of abstraction layers built on top of the XMPP core used by the  Psi Jabber Client. The application is split in two big modules that functionally complement each other: the "Core" and the "GUI". The following figure illustrates this stack:

The "Core" module sits on the lower half of the stack and is responsible for managing all the classes borrowed from the Psi project (and, therefore, the connection to the server) and the classes responsible for taking care of SAPO proprietary protocols. The "Core" contains C++/Qt code.

The "GUI" module is on the top half of the stack. It contains Controller and View classes that as a whole take care of the GUI of the application, as well as an Objective-C Model Layer that provides a high-level abstraction over everything that lies beneath while adding support for some essential Cocoa technologies, like Cocoa Bindings, Notifications, among others. It contains Objective-C/C++ code, with some bits of C/C++ in the lower layers.

The "Core" and the "GUI" modules run in a fairly independent manner. Each one works in its own thread, running its own event loop. A Cocoa event loop is the center of the "GUI", while a Qt event loop takes care of things in the "Core". These two modules communicate with each other by passing messages through the Lilypad Bridge.

The Lilypad Bridge is basically a two-way message-passing interface between the two modules. It doesn't belong to any of the modules in particular, as its code is more or less evenly split between the two. The "GUI" can send action commands down to the "Core", and the "Core" can send notifications describing the current state of affairs up to the "GUI". This is all completely asynchronous. However, since the messages actually resemble remote method invocations, some of the messages sent by the "GUI" aren't really action commands but rather information requests. In some very specific cases, the messages can have return values defined, which means that when the "GUI" asks the "Core" for some info, the "GUI" thread blocks waiting for the value returned in the reply, resulting in something that is more akin to a regular synchronous method invocation.

The synchronous bridge mode can certainly cause hangs in the user interface, and it should be used carefully and sparingly. In the current implementation, it's being used only in situations where we're sure that the reply from the "Core" will be very quick and doesn't involve communication with the server or interaction with other asynchronous processes. Due to the possible dangers of this mode, it is only used in situations where great simplification can be achieved by having a synchronous API.

In the remainder of this document, each sub-section header refers directly to a block depicted in the global architecture diagram shown above. Because the bridge lies between the two main modules — i.e., it belongs to both of them, and at the same time to neither of them — it will be discussed below in its own section.


The Leapfrog Core

Psi Core

Files:

core/psi-core

This is exactly as taken from Psi, except for some minor changes that had to be made in order to support some SAPO protocols that need to customize the presence stanzas or that need to get some data out of the presence stanzas received from other contacts. Also, the image (iconsets) and sound resources of Psi were removed to save space.

Psi Helper Classes

Files:

core/psi-helpers

These classes were next to the Psi GUI code. They were copied from core/psi-core and are being maintained independently from the Psi core due to some massive modifications that some of them have gone through. The avatar management code, for example, has had several bugs fixed and support for the sapo:photo protocol was added. That's why we're not considering them to be part of the hardcore "Psi Core", and that's why we chose to manage them in a way that is completely independent of the core and immune to possible changes included in future core upgrades.

They are very useful to us as they take care of some lower level details of some standard Jabber protocols.

This layer comprises classes that take care of managing:

  • avatars and their local cache;
  • the low-level details of file transfers while providing us with a somewhat higher-level API;
  • vCards and their local cache.

SAPO Protocols

Files:

core/sapo

These classes implement several SAPO proprietary protocols on top of the "Psi Core". The following protocols are implemented:

  • sapo:agents
  • sapo:audible
  • sapo:debug
  • sapo:liveupdate
  • sapo:photo
  • sapo:remote:options
  • sapo:sms
  • server-vars

The Lilypad Bridge

C++/Qt/"Core" side

Files:

core/tools/appmain
core/*.{h,cpp}

The C++/Qt side of the bridge receives messages from the "GUI" — action messages or information requests — and it sends notification messages back to the "GUI". The notification messages emitted by the "Core" module through the bridge effectively allow the "GUI" module to have its internal state completely synchronized with the current state of the Psi Core, the SAPO Protocols classes and Psi Helper classes, without ever having to know about the existence of these entities and how to manage them. The "Core" side of the bridge thus provides a thin layer with a slightly higher level of abstraction over the workings of all the classes that together make our "Core".

ObjC/Cocoa/"GUI" side

Files:

lilypad/Sources/leapfrog_platform.h
lilypad/Sources/LPPlatform.h
lilypad/Sources/LFPlatformBridge.*
lilypad/Sources/LFAppController.*
lilypad/Sources/LFBoolean.*
lilypad/Sources/LFObjectController.*

The "LFObjectAdditions.*" and "LFBoolean.*" files implement the encoding and decoding of objects that must be performed on the arguments and returned values of messages passed through the bridge. These are only of interest if you want to tinker with the inner workings of the bridge, otherwise they can be largely ignored.

The LFAppController class isn't supposed to be instantiated, as it only provides a series of class methods that very closely mirror the API provided by the core on the other side of the bridge. It effectively hides the complexity of running the bridge from the rest of the Objective-C code. When coding above this layer, in order to invoke a method in the "Core" module (going through the bridge) one simply has to invoke the corresponding Objective-C class method implemented by LFAppController.

The remaining files contain the actual implementation of the Objective-C side of the bridge, i.e., they contain code that manages the synchronization of message passing, coding and decoding of values, and dispatching of notifications received from the "Core" to layers in a higher level of abstraction.

Final Remarks About the Bridge

When passing messages and notifications through the bridge, integer ID numbers are used to refer to most objects. Contacts, roster groups, chat sessions, file transfers, among others, are all assigned an integer ID number when they are created in the "Core" side of the bridge. This ID is known by both sides of the bridge, and can then be passed back and forth through the bridge to refer to a particular object when invoking methods and notifications. However, the ID obviously represents two different objects, each one on its own side of the bridge, but with both representing exactly the same thing. There is clearly some duplication here and some room for improvement, which could be attained by streamlining the side of the bridge that sits on top of the "Core".


The Leapfrog GUI

Objective-C Model Layer

Although the bridge already provides a fairly high-level procedural API to the functionality provided by all the classes that we grouped under our "Core", that API is nevertheless nothing more than a collection of functions that can be invoked and a collection of notifications that can be received. A Cocoa application deserves (and needs) more. In order to be able to leverage most of the Cocoa technologies that enable the rapid development of dynamic GUIs — like Cocoa Bindings — we need to have an object model representing the state of the entities involved in the inner workings of the app. This way, we'll be able to directly bind GUI elements to actions or attributes of the model, and everything will be kept consistent and in-synch automatically by the frameworks.

Our Objective-C model is very high-level and most of the time deals with concepts directly accessible through and represented by the GUI. The complexities of dealing with the API provided by the bridge and keeping track of the current state of the core are all hidden from the users of the model layer.

In order to be able to keep the model layer as synchronized with the state of the "Core" as possible, the GUI controllers should never directly access the bridge API directly, bypassing the model. If this happens, the objects in the model may become out of synch with the current internal state of the lower layers of the app. All the functionality that can and should be used directly at the user-interface level is already provided by the model.

Cocoa Controllers and Views

This layer comprises all the custom window controllers, view controllers, views, cells, and many more, that traditionally define how a Cocoa GUI looks like and interacts with the user. The central application controller is a single instance of the LPUIController class.

The controllers are tied to the Objective-C Model Layer through several Cocoa technologies, such as Cocoa Bindings, Key-Value Coding/Observing, Notifications, among others.

Attachments