Documentation

Introduction

The Abstract Namespace
Base Types
What is an Application, Anyway?

Conventions






Introduction

This page explains how to develop with Abstract, and it also describes how it works. It is broken up into other pages since Abstract has different parts, each of which are large on their own.

The problem with most frameworks is that, by nature, their source code is large and complicated, and their documentation is often limited to using the framework to port applications. If you want to understand how the framework works, the process can be daunting. This isn't true of all frameworks (Amulet is a good example), and hopefully it won't be true of Abstract. As luck would have it, I like writing documentation, and I like things to be understandable because I too get easily confused.

Since this is a work in progress, I insert questionable things such as to-do items inside square brackets. They help separate what I'm sure about from what I'm unsure about. Important text is colored in red.

In the future, the documentation for using Abstract and its internal workings will be more cleanly separated. The stuff about particular platform implementations will be factored towards their own place, because the whole point is that someone writing an Abstract app shouldn't have to worry about specific platforms.



The Abstract Namespace

All types defined by Abstract are declared within a namespace called “Abstract”. [todo: global objects should reside there too. ]





Base Types

Abstract is basically a collection of various data types (usually classes) that are used to define an application. The base types are subclassed and extended to do what your application is supposed to do. So far, the base types include:

String and character (for international text)

CCommand (command objects like New, Open, Undo, etc.)

CViewpane (handles windows/dialogs/widgets, does buffering, placement, coord transforms, subcontent protocol, etc.)

CApp (application object and startup logic)

Filesystem types (files, directories, file and folder names, etc.)



What is an Application, Anyway?

It helps to ask ourselves just what an application is.

Fundamentally, an application is not logically any different from a function. Both take arguments, both may have subroutines, and both may return a value to the calling function. To the OS, an application is nothing more than a function called “main”. Even the OS itself is nothing more than a function whose entrypoint is called by a bootstrap loader.

There's no technical reason why an OS couldn't just do everything itself. The demarcation between what is “in” the OS as opposed to what lies “elsewhere” is completely immaterial to the computer -- it's all just software loaded into memory. Any application making calls into the OS already has much of its functionality in the OS -- it could not run apart from it. In this sense, applications are merely “OS extensions” or “OS plug-ins”. They let the user temporarily extend and customize the OS to perform some particular function. Those functions which nearly all users perform (such as text editing or web browsing), not surprisingly, tend to wind up being provided by the OS vendor and even tightly coupled into the traditional OS software. Applications per se tend to exist only because there are too many different kinds of specialized tasks for the OS to perform. But if computers were powerful enough, and an OS vendor resourceful enough, it would be possible to have the OS simply do everything. Today's situation is clearly some distance along that path since the early days of personal computing.

Let's ask what a function is, since applications are simply large functions. A function implements the classic input-process-output transformation of data. The data in question might be structured in a very complex fashion and the inputs and outputs equally complex, but that simple idea is what it boils down to.

The focus onto this simple idea is what a good abstraction model provides. It lets developers stay close to the essentials of what the application does and what the application is all about. It makes it easier to see what is secondary and belonging to other domains.



Other Types

The classes described below represent the various objects that applications can readily instance and use. Through inheritence and/or aggregation, other types can be defined. Simple scalar types such as ints, floats, raw pointers, etc. are intrinsic to C++. The use of the Abstract character and string classes is recommended in order to gain Unicode support.

The Abstract bitmap class is not meant to replace all bitmap usage, particularly where pixel-level manipulation and display performance is concerned. Such code is usually #ifdef platform at the application level.

Some classes should be private to the runtime engines, such as GUI elements. Otherwise, an application can restrict itself to working only in a GUI environment.

Using the memory classes even in release builds can be handy, since some methods such as memcpy can be automatically mapped to the fastest available memory copying technique on the host computer.

[todo:

Signal Manager (registers message types between broadcasters/listeners)

Preference Manager (for both global and user-specific app settings)

Resource Manager (refcounts read-only bitmaps, etc.)

Data Translation Manager

Report Creator (handles printing, PDF generation, etc.)

Datasource State Manager (journals commands for multiple Undo/Redo)

Command Macro Manager (lets command sequences be arbitrarily stored/replayed)

External Modules Manager (handles plug-ins)

Memory Manager (offers memory tracking in debug builds, etc.)

Help Visualizer (might use regular HTML)

Media Manager (sound, video, etc.)



Memory blocks

Numbers

Generic numbers

Integers

int8, uint8

int16, uint16

int32, uint32

Floating-point numbers

float32, float64

Normalized numbers?

Counters and cardinal quantities

size_t

Complex numbers

Random numbers

Monetary amounts

Choices

Choice sets

Temporal Types

Dates

Times

Tick counts

Stopwatches

Geometric Types

Points

Angles

Vectors

Normals

Matrices

Quaternions

Events and Signals

Messages

Commands

Device and timer events

Threads and Mutexes

Data sources

Documents

Data validators

Edits

Toolbars

Broadcasters

Listeners

Bitmaps

Pixel Images

Icons

Icon maps

Masks

Heightfields

Bumpmaps

UV Maps

Surface normal maps

Displacement maps

Vector Graphic Types

Colors

Color models

Grayscale
RGB
CMYK
CIELab
HSV
HSL

Fonts

Font glyphs

Splines

Meshes

Coordinate Transformers

Exceptions



Numbers

For practical purposes, there are limits to abstraction. In a perfect world, one would never do something as crude as saying:

int i;

when one needed an integer but rather,

number i(1.0, -50, 100);

with the three construction arguments indicating precision, minimum value and maximum value. The computer would then instantiate the optimum physical representation for the number, using no more bits than necessary, and enforce the min/max rules automatically during mutation.

But this isn't practical, of course, because the overhead is often unnecessary, and current computers can't efficiently exploit a number that takes fewer than a whole multiple of eight bits to represent. It is also easier to type 'int' or 'float'. However, such an abstraction is useful as a generic or variant number type, which can be implemented in C++. Knowing the allowed range of an integer could also provide useful storage efficiencies for large arrays of such numbers. In C, however, one can also use the bitdepth operator during integer variable declaration.

So, bottom line, Abstract allows the use of both native and generic number types.

Number types are intriguing because it is possible (if one were pedantic enough) to make number classes to cover every use within an application. Instead of passing an integer to a function such as ComputeDateFromToday, for example, one could instead pass a DateDelta type, to make it absolutely clear that the function used the integer to mean a difference between dates. While this might make function declarations vastly more readable without having to include meaningful variable names, and increase type safety, it would also mean having to declare lots of classes, and at a certain point, too much abstraction is no longer a good thing -- it simply becomes more trouble than it's worth (e.g., the learning curve of the API becomes more of a bottleneck than the inefficiencies arising from poor code reuse). The truth lies somewhere in between and the trick, as always, is knowing where to draw the line.



Filesystem Entities

Volumes, directories, and files are among some of the best-known entities in computing, if only because they have been present from nearly the beginning. Computing certainly wouldn't be much fun if one could not persist data across multiple program invocations or exchange data between programs. Files also allow datasets much larger than available memory to be manipulated.

Abstraction can be self-defeating here, because files represent a classic latency performance problem. Just about every language has commands to open and close files, and these operations must contain data transfer operations (reading and writing). One could, for example, easily make a function that mimics C stdlib's fread without requiring a handle to an open file, like this:

    int fileread(const char* filename, long offset, void* block, size_t blocksize)
    {
        FILE* f = fopen(filename, “rb”);
        if(f == NULL)
            return 0;
        fseek(f, offset, SEEK_SET);
        int n = fread(block, 1, blocksize, f);
        fclose(f);
        return n;
    }
 

But using it all the time would be an exercise in performance wastage, because with each call the filename must be translated to a filesystem entry and physical disk block locations must be computed from seek offsets. That information can be cached, but that's exactly what the FILE structure does, and explicitly opening a file lets the filesystem know when to start and stop caching. The same is true for any resource where data acquisition benefits from maintaining some session metadata across get/set calls.

Acquiring and releasing handles to resources is a chore and definitely an abstraction target, but to achieve the existing level of performance, an abstract system must cache the data somehow. Opening a file is also a convenient way to establish sharing permissions and to indicate the intended nature of the upcoming I/O (e.g., reading vs. writing) where this information helps the I/O system improve performance. Even people use spoken language in a stateful manner: one does not repeatedly refer to the same object by its name, but rather by using the word “it” after the formal identification has been done once or twice. It is simply assumed that the conversants are aware to what “it” refers.



Edits

Edits are (and usually user-issued) data mutations. They are usually deterministic, i.e., if they are re-issued onto a data object that has been put into the same state at the time the command was initially issued, then the data will be transformed to the matching changed state. An example of a non-deterministic command is the opening of a file, because the document state will be whatever is in the file which could be anything. Unless the file's contents are stored with the command object, there is no way to be certain what state will ensue from loading the file.

Edits normally have a one-to-one relationship with mutator commands. An “Edit, Cut” command, for example, usually creates a matching “Cut” edit. Other commands are less obvious, such as holding down an arrow key to move an object some distance. While the user may perceive the events between pressing the key and releasing it as constituting a single edit, the application may prefer to treat each key repetition as a unique edit.

By explicitly defining datasource mutations as edits, Abstract can provide Undo/Redo and macro features for free.

Edits must be doable and optionally may also be undoable. If an edit is undoable, it will be asked to undo itself when an Undo command is issued. Otherwise, Abstract will revert the datasource to its initial state and play back all recorded edits forward in time until the datasource is regressed one step back. If undoable edits don't need to store large amounts of data, they should try to be undoable.



Broadcasters and Listeners

Anyone who has developed large object-oriented systems invariably comes to appreciate the need to abstract the relationship between objects. In Abstract, for example, one does not explicitly invalidate a viewpane after modifying visible data. This only makes the data-modifying code closely tied to the viewpane. Instead, the data being modified issues (or is instructed to issue) a “I've been changed” message to subscribing listeners. Since the viewpanes have subscribed, they automatically invalidate themselves. Thus, the number and type of different possible viewpanes visualizing the data are of no concern to either the data or to the data modifier.

Not all classes and types are broadcasters/listeners. Doing so would add overhead to simple types which are often allocated in great numbers.



Datasources

The core essence of every application is data. Even “Hello, world!” has data: the message itself. A videogame has data: the position of the player and the opponents, their runtime states, etc.

Datasource objects act as the things that the application is about. They represent the information that the user needs to see and/or transform. A big datasource can represent a document. A small one can represent a piece of text or geometry copied to the clipboard. A datasource can have a transformation history managed by the Undo manager. A datasource can be persisted to/from disk. A datasource can be described with metadata. Like structures, a datasource can hierarchically contain child datasources. A datasource can also be a command target.



Data Translation

A neat idea from the Be OS is translation, which helps programs import and export data by translating external data formats to/from their own. The moment someone starts persisting data, someone else wants to convert it. Standardizing translation services makes it easy for applications to add import/export facilities.



CAppStartupOptions

Contains the command-line options used to launch the program. Abstract apps do not need to concern themselves with how the options are provided or formatted (e.g., on a command line as a string). For standard main() entrypoints, CAppStartupOptions converts char* argument descriptions to Unicode-based CString objects. Under Windows, this object may also contain other details.



Runtime Environment

When an Abstract application starts, the runtime engines basically must initialize and emulate an idealized computing environment. The application is treated as a procedural data object that the engines make visible to the user and mediate events for. Knowledge of, and interaction with, the actual underlying OS is kept to a minimum.

The application normally begins by instancing an application object and calling AbstractMain from within the C/C++ main function or from WinMain on Windows. From there on, Abstract manages the event loop.

Abstract starts by signaling the application to initialize. The application normally defines its initial set of commands and datasources. If a data source is instanced, Abstract will request it to define viewpanes and command sets related to it. Otherwise, Abstract loops until the user loads or instances a data source.

If datasource viewpanes are defined, Abstract makes them visible and starts mediating their related events. The application's datasource objects have their necessary accessor/mutator methods invoked. Whenever a set of data edits occurs within one command handling, the related viewpanes are invalidated. Interactive edits, of course, can request immediate viewpane updates.

Runtime information can be automatically logged to text files to assist developers, tech support, or knowledgeable end users. Datasources with supporting readable metadata descriptors can be used by Abstract to document events in a sophisticated manner.

When a termination command or signal is raised, Abstract destructs the application object, releases its own resources and exits to the OS.



An Example Application

It's tempting to describe the traditional “Hello, world!” example in Abstract, but that would be incomplete since such an app only presents information. So instead, we'll talk about a simple data editing program. Ideally, we want a program that demonstrates Abstract's full abilities by using dialogs, preferences, complex viewpane groupings, command changing, etc.

Our program, CSimpleApp, lets multiple documents and multiple views per document be used. Each document datasource contains an integer. The integer can be changed by clicking and dragging on a viewpane related to the document.

The use of constant char* declarations does not imply that C-style strings are used. Instead, such declarations simply use convenience string argument variants of methods that take CStrings, or the CString constructor itself autoconverts C-strings. Message IDs are safe to define as 8-bit characters, but command names presented in user interfaces need to be defined in proper Unicode form. When creating a command set called “File”, for example, the C-string is merely an internal identifier. This identifier is then used to locate the actual readable form for UI presentation.

Some of the classes used in this example are described above.

An example application called smallapp has already been implemented. It doesn't currently do much, but it does show the basics in actual compilable form.



Conventions

This part applies mostly to source code.

I use four spaces per tab stop. Not physical spaces; that's just how wide the tab stop setting should be in your source code editor in order to have things line up properly.

I tend to wrap long lines at around 40 or 50 characters. I find it easier to read large text but using large fonts on a typical screen makes for less characters per line. But I haven't found the shorter length uncomfortable at all. If anything, it actually makes it easier because I don't lose track of which line I'm reading as I try to find the beginning of a successive line. It's the same reason newspaper columns are narrow.

[ todo: might be easier on the hands to use lowercase syntax, e.g., abstract::viewpane instead of Abstract::Viewpane. That would also mesh with the stdlib naming convention. Long multipart names become harder to read, however, and using underscores defeats the purpose of avoiding the Shift key. Dropping the 'C' prefix from class names also sounds worthy -- why distinguish some types as classes? Ideally types are just “types” and that's that. ]

[ An odd namespace thing cropped up with enum types. gcc on OS X doesn't like within-class enum decls that work on MSVC. So to enjoy the convenience of being able to categorize an enum label within a context (e.g., HomeStyle::FixerUpper instead of HomeStyle_FixerUpper), because it often happens that enum labels are shared across contexts, we make the context a namespace and the enum typedef simply the stub word 'Type'. Now I can easily use a shared label like “_default” to mean the default value in an enum list, and the context name indicates which enum list is being referred to. To decl a var of a particular enum type, one just does something like “HomeStyle::Type var”. ]






Copyright 2003-2004 Daylon Graphics Ltd.
Abstract is a trademark of Daylon Graphics Ltd.
Author(s): Ray Gardener (rayg@daylongraphics.com)
Location: http://www.daylongraphics.com/other/abstract/docs.htm
Last updated: March 6, 2004