In contrast to other extension language implementations (e.g. Tcl), Elk does not provide its functionality in the form of a library that is statically linked into an application to be extended. Instead, the object modules comprising the application and all required library extensions are dynamically linked with and loaded into the running Scheme interpreter. To accomplish this, the load primitive of Elk has been extended to load not only files containing Scheme code, but also object files -- compiled extensions written in C or C++. Dynamic loading enables applications to load less frequently used modules into the running program only on demand; such an application is initially smaller than the equivalent statically linked application (where all modules must be combined into one large executable file).
Dynamic loading of object files is often used together with the dump primitive that creates an executable file from the running interpreter, similar to unexec of GNU Emacs or dumplisp in some Lisp systems. The dump primitive of Elk differs from existing, similar mechanisms in that the newly created executable, when called, starts at the point where dump was called in the original invocation (as opposed to the program's main entry point). Here the return value of dump is ``true'', while in the original invocation it returns ``false'' -- not unlike the UNIX fork system call.
To generate a new instance of an application one would typically
invoke the Scheme interpreter, load all object modules and all Scheme
code required initially, perform all initializations that can survive a
dump, and finally dump an image of the running interpreter
containing all the loaded code into a new executable on disk.
The use of dump avoids time-consuming activities such as
loading of object files and other initializations on each startup.
The dumped executable, when started, resumes after the call to
dump; at this point one would perform the remaining,
environment-dependent initializations and finally invoke the
application's ``main program'' (e.g. enter the X toolkit's event
processing main loop).
Listing 1 shows a (slightly simplified) Scheme program that
generates and starts a new instance of an application.
;;; Load initially required object files and Scheme files of ;;; application and dump image into executable file. ;;; Dumped file enters application's main loop on startup. (load 'main.o) ; initial object modules (load 'edit.o) (load 'x11.o) ; (a library extension) ... (load 'ui.scm) ; initial Scheme files (load 'custom.scm) (load 'x11.scm) ... (initialize-application) (if (dump 'a.out) (begin ; dumped a.out starts execution here (initialize-depending-on-environment) (main-loop-of-application) (exit))) ;; Original invocation gets here when dump is finished. We're done.
Note: Filenames can be given as symbols (besides the usual string
A more meaningful name than a.out would probably be chosen in practice.
On systems that do not support dynamic linking and loading of object files (such as older versions of UNIX System V) or where dump cannot be implemented, the interpreter kernel and the application and library extensions are linked statically and combined into one executable.
In any event, in an application using Elk, the control initially rests in the Scheme interpreter. The interpreter acts as the ``main program'' of the application; it is the interpreter's main() function which is invoked on startup of the program. Therefore the first code to execute in an application is Scheme code; this Scheme code provides the shell functionality of the application (hence it is called shell code). The shell code may perform a few simple tasks, for instance, load a user-provided initialization file containing customization code for the application and then enter the application's main loop, or it may be as complex as in ISOTEXT, where the entire X-based user interface is written in Scheme.
The application, as it is linked with the extension language interpreter, has full access to all external functions and variables of the interpreter kernel. The interpreter, on the other hand, does not have any knowledge of the contents of dynamically linked and loaded object modules; all it sees of an object file being loaded is the file's symbol table. To obtain ``hooks'' into a newly loaded extension, the interpreter searches the symbol table of each object file being loaded for functions whose names start with the prefix ``elk_init_'' (extension initialization functions) and invokes these functions as they are encountered. Likewise, to support extensions written in C++, any C++ static constructors found in the symbol table are called. When linked statically with its extensions, the interpreter must scan its own symbol table on startup to find and invoke the initialization functions. (Similar support is available for calling extension finalization functions and C++ static destructors on termination.)
Besides initializing private data of the modules being loaded, these initialization functions register with the interpreter the Scheme primitives and Scheme data types implemented by the extensions. To enable extensions to register new primitive procedures and types, the interpreter kernel exports two functions: Define_Primitive() to register a new Scheme primitive and Define_Type() to register a new Scheme data type. Both functions take pointers to C functions as arguments that implement the new primitive or the basic access functions of the type (such as the print function and the equality predicates).
A simple example for a library extension is presented in Appendix A.