Operating Architecture
(How It Works)
Split Programming Design Philosophy
This particular design paradigm came about in response to two problems. The first problem was that of allowing multiple processes to all contribute to one OpenGL context for collective rasterization, presentation and interaction. Because an OpenGL context is local only to a single thread at a time means that only one thread of one process can hold the responsibility of actually managing the visual content. Through means of IPC (interprocess communication), client programs wishing to use the window server to run OpenGL commands can send messages with such requests. However, this requires too much overhead and the latency of current IPC methods became the second problem because the latency is not sufficiently low to support realtime human interfaces in the way that Vision handles them. The latency between the time that the event hits the window server to the time that the visual consequences of that event are observed was too great.
The solution to these two problems was one that was initially counter-intuitive but proved to work remarkably well. The solution was found to be the aforementioned separation of the portions of an application that deal with the user interface/presentation out from the main processing work of the program into separate processes. All objects, data and code having to do with the user interface are stored in dynamic library modules which the window server loads into its virtual address space on behalf of client processes that wish to have that code executed or interact with it. By itself, the window server upon bootup contains no code having to do with specific user interface constructs. Thus a client process may request that the window server load any number of dynamic libraries (hereafter referred to as "server modules" and sometimes just "modules") on its behalf as well as make requests that the window server instantiate objects and data from those server modules. Client processes can retrieve Objective-C object pointers of those objects instantiated in the window server and treat them just as if they were objects that exist their own virtual address space with the exception of direct member variable access. The server modules that the client process requests can be those in repositories meant for general use, third-party libraries or completely custom server modules designed only for one client program.
The general idea is that application developers write their programs in two portions. One portion is the workhorse part that needs extensive CPU cycles, specialized timing, specialized hardware support, the security/space of a complete virtual address space or many other things that applications may require other than the user interface. The other portion are server modules which contain all the code that makes up the constructs that users are going to interact with in order to use the program. Ideally these server modules would also leverage code in other server modules (such as those in a repository) so that application development is as efficient as possible. The design decisions behind what code goes into the server module and what code remains in the client process is very vital to the program's stability, security, speed and ease of development. Ideally any data that the client process creates that has a direct and immediate impact on the visual interface should be passed up to the corresponding objects (instantiated from code in the companion server module) residing in the window server through normal Objective-C messaging.
Some programs may lend themselves to an entire existence in the window server without any need to communicate with the client process. Most of the time these are smaller, more graphically oriented programs. One example of this kind of a program is a Rubik's cube. Because all code and data having to do with maintaining the state and interaction of a Rubik's cube directly and immediately impacts its presentation and interface, it is appropriate that it remain almost entirely within a server module. An example of a fairly even code volume split would be a packet sniffer. All of the networking code and callbacks remain in the client process. The client process sends messages to its counterpart objects in the window server indicating that new packets have arrived and that they need visualization. The objects on the server then react accordingly to the messages by creating other objects, changing values or other appropriate code.
This separation of code allows all the client programs access to the same OpenGL context which solves the first problem. As for the second problem, because in most cases there is no messaging overhead (between event hit and visual reaction) that could be susceptible to the operating system scheduler and system load, the latency becomes negligible. Once events hit the window server process, the consequences are evaluated and executed within the next run loop thus making the latency substantially lower.
Development Paradigms
There are two main ways to run Vision. It can be run as a traditional API library that applications simply link to with all client and server activity occurring within the same process (this style of a traditional API library is the only configuration that Vision has been publicly released for at this time). It can also be run as a window server/client program style system where functionality is segregated into different processes and there is interprocess communication.
Window Server/Client Program |
Application Library |
As shown in the diagrams above there are two types of files that these portions act upon. Server modules are dynamic libraries which hold code that is to be run within the memoryspace of the server in order to expand and extend functionality. Construct tree files are saved portions of the GUI state which can be built upon and used on behalf of client programs to save time instantiating Construct trees manually by communication with the server.
Window Server/Client
Program |
Application Library |
|
Operating System
Event To
Visual
Change Latency |
Very Low |
Very Low |
Server/Client
Communication
Latency |
Limited to IPC or
Network |
Almost Zero |
Security Model | Self
Run Sandbox |
Operating System |
Multiple Clients |
Yes |
No |
Operating System Window Server Capable | Yes |
No |
Direct Memory Access
To
Server
Objects |
No |
Yes |
3rd Party Library
Access
In
Server Process |
No |
Yes |
The window server/client program paradigm (hereafter referred to as the "window server" paradigm) is used when Vision is being run as a window server process on an operating system (either the primary window server or one that runs alongside or within the primary window server). The application library paradigm is used to create standalone programs that run within the primary window server. The two different styles for developing and using Vision are meant to be as similar as possible to facilitate easy portability of programs between both paradigms. The differences in the table note some places to be wary of. Server/client communication latency can be an issue in the window server paradigm if messages are passed from the client side to objects that reside in the server too often, whearas in the application library paradigm there is no penalty from messaging at all. Thus the segregation of function into server modules and client code becomes a very important design decision. The security model of the window server is more restrictive than that of the application library and so low-level operations and features such as Objective-C categories which are allowed in the application library are not allowed in the window server. Because direct memory access to server objects is banned in the window server paradigm all access from the client must happen through Objective-C messaging. This restriction does not exist in the application library however. An application library program may link to and take advantage of 3rd party libraries but server modules may not perform such linking and thus in turn the window server also may not. The client program in the window server paradigm however may link to 3rd party libraries.
Role Of Server Modules And The Repository
Server modules are vital in the Vision operating architecture and their role is very important. Great care should be taken towards their design. The application library paradigm does not require that any code be loaded in at runtime and all relevant design code may included at compile time if so desired. However, there is great power in keeping generally used design code outside in server modules. Advantages include the leverage of publicly distributed server modules which would ideally be open source, robust and feature rich. These modules would most likely provide commonly requested functionality thus alleviating the programmer from having to re-invent the wheel by creating code that has already been written, scrutinized and debugged. It also encourages programmers to follow modular code design, reuse existing code and write more general/flexible code.
Server modules can reside
anywhere as long as the code requesting it is aware of where it
resides. There is however
a directory that is reserved as a repository for server modules which
can be
made available to all programs for use. This directory is called
the Repository and is located at
/usr/local/Vision/Repository. Application library programs at
runtime by default change their repository path to
MyApp.app/Contents/Resources/.
When modules are loaded from files and into the memoryspace of the server, their constructors and destructors ensure that the Objective-C runtime is properly prepared and that all necessary symbols are resolved before any code within the server module is executed. Because multiple modules may be dependent on the same server module, instead of loading it multiple times it increments its reference count and keeps track of which clients are dependent on which server modules. When the clients disconnect from the server all the resources that were requested and allocated on their behalf are released. Only when a server module's reference count becomes zero is it actually moved out of the process space.
Construct Tree Save Files
When Construct
trees become complex it is very
cumbersome to try and maintain their construction purely by managing
the source code that instantiates it. Thus there arises a need
for
storage and retrieval of Construct trees for reuse, clean coding
policies and ease of creation/modification. There are facilities
in
the base classes and in the server that allow for easy archival and
retrieval of Construct trees. There are also methods for more
specific archival behavior through subclass message overriding.
When Construct trees are loaded from files they can be used in either a
palette/template fashion where one construct tree is used as a
prototype from
with which to copy many other instances from or they can simply be
loaded
and put to work as they are. There is an editor
program called VisionEditor that specializes in visually
manipulating
construct trees and managing their archival into construct tree
files.
Communication Between Server and Client
Because the server and client portions are present in the same process space in the application library paradigm, communication does not need any additional facilitation. However, in the window server paradigm communication is carried out by language-level distributed objects. It was decided to forego Apple's implementation of transparent distributed objects in favor of developing one specifically tailored to Vision because of certain restrictions placed on usage and bugs that are present in Apple's implementation.
Communication in the split process situation is handled by transparent Objective-C messaging. What this means is that even though the actual object that the client is attempting to message resides in the server process, it can still accomplish the message pass by instead sending the message to a proxy (and vice versa in the reverse direction). As far as the client program is concerned the object that it is messaging is not a proxy but is for all messaging purposes, the actual target object (in the client's source code it is actually typed as such as well). From its perspective it cannot tell what objects reside in its virtual address space and which ones reside in the server's. When a program sends a message to an object that it thinks is of a specific type but is actually stored as a proxy what happens is described below:
If the
receiving object is of
the VProxy type then the Objective-C
runtime
will fail to find the method implementation for the selector and will
forward it
using
the distributed objects functionality in the VProxy object. This
functionality occurs in two methods:
methodSignatureForSelector()
is called to obtain the call stack
structure (signature) for the method
- It first trys the explicitly assigned/cached signatures to see if it is there.
- Then it trys to see if the signature is within the current Objective-C runtime.
- If all else fails then
it asks the receiving process for it.
- It runs through all the arguments, push proxies, selectors and data for translation if necessary.
- It then creates the argument frame.
- Then it creates the IPC message and sends.
- It receives the return
value (if any)
and deals with data that has been pushed.
requestCallback() gets the IPC message and services it accordingly. If it is a method call then it follows these steps:
- It performs proxy and other translations as needed.
- It then uses objc_msgSendv to actually send the message to the real object.
- Finally it packages up the return value and sends it back to the calling process.
When proxies or actual objects are passed as parameters in messages that undergo this process they are translated into values that correspond to their counterparts in the other process. Scalar parameter values however such as ints, floats, arrays and others are simply copied over. If the corresponding counterpart object does not exist then it is created on the fly. When the server and the client initially connect there is an object that is passed over which is then used by the client to initially send messages to the server and consequentially receive more remote objects which it can then send messages to. The number of proxies in the processes then gets larger and larger as more messages are passed and more objects passed and returned. Upon client disconnect all proxies and associated resources on both sides are released.
This document explains how Vision works internally and why it was designed the way it is. You do not necessarily need to read this document if you want to use Vision under the Application Library paradigm (described below) but it can shed some light on the motivations for certain decisions that were made in Vision's design.
The operation of Vision is divided up into two different kinds of actions: actions that occur in the server and actions that occur in the client. The server holds all of the state for the GUI (graphical user interface) and has the responsibility of rendering it to the screen, communicating with the client side and loading resources on behalf of the client side. Clients communicate with the server about their needs and perform specified application functionality. As described below in the section Development Paradigms there are currently two ways to organize these parts. The segregation of actions into those two distinct portions works well under both of them, however the Window Server/Client Program paradigm requires it and thus the motivation for the separation described below will be in terms of that paradigm.