Extensible applications built with Elk are hybrid in that they consist of code written in a mixture of languages--code written in the application's implementation language (C or C++) and code written in the extension language (Scheme). An application of this kind is usually composed of two layers, a low-level C/C++ layer that provides the basic, performance-critical functionality of the application, and on top of that a higher-level layer which is written in Scheme and interpreted at runtime.
The Scheme-language portion of an Elk-based application may range from just a few dozen lines of Scheme code (if a simple form of customization is sufficient) to fifty percent of the application or more (if a high degree of extensibility is required). As Scheme code is interpreted at runtime by an interpreter embedded in the application, users can customize and modify the application's Scheme layer or add and test their own Scheme procedures; recompilation, access to the C/C++ source, or knowledge of the implementation language are not required. Therefore, an application can achieve highest extensibility by restricting its low-level part to just a small core of time-critical C/C++ code.
To enable extensions to ``work on'' an application's internal data structures and state, the application core exports a set of new, application-specific Scheme data types and primitives operating on them to the Scheme layer. These types and primitives can be thought of as a ``wrapper'' around some of the C/C++ types and functions used by the application's core. For example, the core of an Elk-based newsreader program would export first-class Scheme types representing newsgroups, subscriptions, and news articles; these types would encapsulate the corresponding low-level C ``structs'' or C++ classes. In addition, it would export a number of Scheme primitives to operate on these types--to create members of them (e.g. by reading a news article from disk), to present them to the user through the application's user-interface, etc. Each of these primitives would recur on one or more corresponding C or C++ functions implementing the functionality in an efficient way.
Another job of the low-level C/C++ layer of an application is to hide platform-specific or system-specific details by providing suitable abstractions, so that the Scheme part can be kept portable and simple. For example, in case of the newsreader program, extension writers should not have to care about whether the news articles are stored in a local file system or retrieved from a network server, or about the idiosyncrasies of the system's networking facilities. Most of these system-specific details can be better dealt with in a language oriented towards systems programming, such as C, than in Scheme.
To decide whether to make a function part of the low-level part of an application or to write it in the extension language, you may ask yourself the following questions:
If the answer to this question is yes, put the function into the C/C++ core. For example, in case of the newsreader application, a primitive to search all articles in a given newsgroup for a pattern is certainly performance-critical and would therefore be written in the implementation language, while a function to ask the user to select an item from a list of newsgroups is not time-critical and could be written Scheme.
For example, a function that needs to allocate and open a UNIX pseudo-tty or to establish a network connection needs to care about numerous system-specific details and different kinds of operating system facilities and will therefore be written in C/C++ rather than in Scheme.
A function that parses and tokenizes a string can be expressed more naturally (that is, in a significantly more concise and efficient way) in a language such as C than in Scheme. On the other hand, functions to construct trees of news articles, to traverse them, and to apply a function to each node are obvious candidates for writing them in a Lisp-like language (Scheme).
If it is likely that the application's users will want to customize or augment a function or even replace it with their own versions, write it in the extension language. If, for some reason, this is impossible or not practicable, at least provide suitable ``hooks'' that enable users to influence the function's operation from within Scheme code.
In addition to the Scheme interpreter component, Elk consists of a number of Scheme extensions. These extensions are not specific to any kind application and are therefore reusable. They provide the ``glue'' between Scheme and a number of external libraries, in particular the X11 libraries and the UNIX C library (exceptions are the record extension and the bitstring extension which provide a functionality of their own). The purpose of these extensions is to make the functionality of the external libraries (for example, the UNIX system calls) available to Scheme as Scheme data types and primitives operating on them.
While the Scheme extensions are useful for writing freestanding Scheme programs (e.g. for rapid prototyping of X11-based Scheme programs), their main job is to help building applications that need to interface to external libraries on the extension language level. The X11 extensions, for instance, are intended to be used by applications with a graphical user interface based on the X window system. By linking the X11 extensions (in addition to the Scheme interpreter) with an Elk-based application, the application's user interface can be written entirely in Scheme and will therefore be inherently customizable and extensible. As the Scheme extensions are reusable and can be shared between applications, extension language code can be written in a portable manner.
As far as the C/C++ programmer's interface to Elk (that is, the subject of this manual) is concerned, there is not really a technical difference between Scheme extensions on the one hand (such as the X11 extensions), and Elk-based, extensible applications on the other hand. Both are composed of an efficient, low-level C/C++ core and, above that, a higher-level layer written in Scheme. In both cases, the C/C++ layer exports a set of Scheme types and primitives to the Scheme layer (that is, to the Scheme programmer) and thus needs to interact with the Scheme interpreter. Because of this analogy, the rest of the manual will mostly drop the distinction between applications and extensions and concentrate on the interface between C/C++ and Elk.
The only noteworthy difference between applications and extensions is that the former tend to have their own main() function that gains control on startup, while Scheme extensions do not have a main() entry point--they are usually loaded into the interpreter (or application) during runtime. This distinction will become important in the next chapter, when the different ways of joining Elk and C/C++ code will be discussed.