11.  Defining New Scheme Types  

      A new, disjoint Scheme type is registered with Elk by calling the function Define_Type(), similar to Define_Primitive() for new primitives. Making a new type known to Elk involves passing it information about the underlying C/C++ representation of the type and a number of C or C++ functions that are ``called back'' by the interpreter in various situations to pass control to the code that implements the type. The prototype of Define_Type() is:

int Define_Type(int zero, const char *name,
	int (*size)(Object), int const_size,
	int (*eqv)(Object, Object),
	int (*equal)(Object, Object),
	int (*print)(Object, Object, int, int, int),
	int (*visit)(Object*, int (*)(Object*)));
The arguments to Define_Primitive() are in detail:
The first argument must be zero (in early versions of Elk it could be used to request a fixed, predefined type number for the new type);
The name of the new type.
size, const_size
The size of the corresponding C type (usually a struct) in bytes, given as one of two, mutually-exclusive arguments: size, a pointer to a function called by the interpreter to determine the size of an object (for types whose individual members are of different sizes, such as the vector type); and const_size, the size as a constant (for all other types). A null-pointer is given for const_size if size is to be used instead.
eqv, equal
Pointers to (callback) functions that are invoked by the interpreter whenever the Scheme predicate equal?, or eqv? respectively, is applied to members of the newly defined type. As an application-defined type is opaque from the interpreter's point of view, the equality predicates have to be supplied by the application or extension. Each of these (boolean) functions is passed two objects of the new type as arguments when called back.
A pointer to a function that is used by the interpreter to print a member of this type. When calling the print function, the interpreter passes as arguments the Scheme object to be printed, a Scheme port to which the output is to be sent, a flag indicating whether output is to be rendered in human-readable form (display Scheme primitive) or machine-readable, read-write-invariance preserving form (write), and finally the current remainders of the maximum print depth and print length. The return value of this function is not used (the type is int for historical reasons).
A pointer to a ``visit'' function called by the garbage collector when tracing the set of all currently accessible objects. This function is only required if other Scheme objects are reachable from objects of the newly defined type (a null pointer can be given otherwise). It is invoked with two arguments: a pointer to the object being visited by the garbage collector, and a pointer to another function to be called once with the address of each object accessible through the original object. For example, the implementation of pairs would supply a visit function that invokes its second argument twice--once with the address of the car of the original object, and once with the address of the cdr.

      The return value of Define_Type() is a small, unique integer identifying the type; it is usually stored in a ``T_*'' (or ``t_*'') variable following the convention used for the built-in types.

      In the current version of Elk, Define_Type() cannot be used to define new ``pointer-less'' types resembling built-in types such as fixnum or boolean.

      The first component of the C structure implementing a user-defined Scheme type must be an Object; its space is used by the garbage collector to store a special tag indicating that the object has been forwarded. If you are defining a type that has several components one of which is an Object, just move the Object to the front of the struct declaration. Otherwise insert an additional Object component.

      The Scheme primitive that instantiates a new type can request heap space for the new object by calling the function Alloc_Object():

Object Alloc_Object(int size, int type, int const_flag);
The arguments to Alloc_Object() are the size of the object in bytes (usually obtained by applying sizeof to the underlying struct), the type of which the new object is a member (i.e. the return value of Define_Type()), and a flag indicating whether the newly created object is to be made read-only. The return value is a fully initialized Object.

11.1.  Example for a User-Defined Scheme Type  

      Figure @(ndbm1) shows the skeleton of an extension that provides a simple Scheme interface to the UNIX ndbm library; it can be loaded dynamically into the Scheme interpreter, or into an Elk-based application that needs access to a simple database from within the extension language. Please refer to your system's documentation if you are not familiar with ndbm. The extension defines a new, first-class Scheme type dbm-file corresponding to the DBM type defined by the C library. Again, note the naming convention to use lower-case for new identifiers (in contrast to the predefined ones).

#include <scheme.h>
#include <ndbm.h>

int t_dbm;

struct s_dbm {
	Object unused;
	DBM *dbm;
	char alive;   /* 0: has been closed, else 1 */

#define DBMF(obj) ((struct s_dbm *)POINTER(obj))

int dbm_equal(Object a, Object b) {
	return DBMF(a)->alive && DBMF(b)->alive && DBMF(a)->dbm == DBMF(b)->dbm;

int dbm_print(Object d, Object port, int raw, int length, int depth) {
	Printf(port, "#[dbm-file %lu]", DBMF(d)->dbm);
	return 0;

Object p_is_dbm(Object d) {
	return TYPE(d) == t_dbm ? True : False;

void elk_init_dbm(void) {
	t_dbm = Define_Type(0, "dbm-file", 0, sizeof(struct s_dbm),
		dbm_equal, dbm_equal, dbm_print, 0);

	Define_Primitive(p_is_dbm,    "dbm-file?", 1, 1, EVAL);
	Define_Primitive(p_dbm_open,  "dbm-open",  2, 3, VARARGS);
	Define_Primitive(p_dbm_close, "dbm-close", 1, 1, EVAL);
Figure 5: Skeleton of a UNIX ndbm extension

      The code shown in Figure @(ndbm1) declares a variable t_dbm to hold the return value of Define_Primitive(), and the C structure s_dbm that represents the new type. The structure is composed of the required initial Object, the DBM pointer returned by the C library function dbm_open(), and a flag indicating whether the database pointed to by this object has already been closed (in this case the flag is cleared). As a dbm-file Scheme object can still be passed to primitives after the DBM handle has been closed by a call to dbm_close(), the alive flag had to be added to avoid further use of a ``stale'' object: the ``dbm'' primitives include an initial check for the flag and raise an error if it is zero.

      The macro DBMF is used to cast the pointer field of an Object of type t_dbm to a pointer to the correct structure type. dbm_equal() implements both the eqv? and the equal? predicates; it returns true if the Objects compared point to an open database and contain identical DBM pointers. The print function just prints the numeric value of the DBM pointer; this could be improved by printing the name of the database file instead, which must then be included in each Scheme object. The primitive p_is_dbm() provides the usual type predicate. Finally, an extension initialization function is supplied to enable dynamic loading of the compiled code; it registers the new type and three primitives operating on it. Note that a visit function (the final argument to Define_Type()) is not required here, as the new type does not include any components of type Object that the garbage collector must know of--the required initial Object is not used here and therefore can be neglected. The type constructor primitive dbm-open and the primitive dbm-close are shown in Figure @(ndbm2).


Object p_dbm_open(int argc, Object *argv) {
	DBM *dp;
	int flags = O_RDWR|O_CREAT;
	Object d, sym = argv[1];

	Check_Type(sym, T_Symbol);
	if (EQ(sym, Intern("reader")))
		flags = O_RDONLY;
	else if (EQ(sym, Intern("writer")))
		flags = O_RDWR;
	else if (!EQ(sym, Intern("create")))
		Primitive_Error("invalid argument: ~s", sym);
	if ((dp = dbm_open(Get_String(argv[0]), flags,
			argc == 3 ? Get_Integer(argv[2]) : 0666)) == 0)
		return False;
	d = Alloc_Object(sizeof(struct s_dbm), t_dbm, 0);
	DBMF(d)->dbm = dp;
	DBMF(d)->alive = 1;
	return d;

Object p_dbm_close(Object d) {
	Check_Type(d, t_dbm);
	if (!DBMF(d)->alive)
		Primitive_Error("invalid dbm-file: ~s", d);
	DBMF(d)->alive = 0;
	return Void;
Figure 6: Implementation of dbm-open and dbm-close

      The primitive dbm-open shown in Figure @(ndbm2) is called with the name of the database file, a symbol indicating the type of access (reader for read-only access, writer for read/write access, and create for creating a new file with read/write access), and an optional third argument specifying the file permissions for a newly-created database file. A default of 0666 is used for the file permissions if the primitive is invoked with just two arguments. Section @(ch-symbits) will introduce a set of functions that avoid clumsy if-cascades such as the one at the beginning of p_dbm_open(). Primitive_Error() is called with a ``format string'' and zero or more arguments and signals a Scheme error (see section @(ch-error)). dbm-open returns #f if the database file could not be opened, so that the caller can deal with the error.

      Note that dbm-close first checks the alive bit to raise an error if the database pointer is no longer valid because of an earlier call to dbm-close. This check needs to be performed by all primitives working on dbm-file objects; it may be useful to wrap it in a separate function--together with the initial type-check. Ideally, database objects should be closed automatically during garbage collection when they become inaccessible; section @(ch-term) will introduce functions to accomplish this.

      At least two primitives dbm-store and dbm-fetch need to be added to the database extension to make it really useful; these are not shown here (their implementation is fairly simple and straightforward). Using these primitives, the extension discussed in this section can be used to write Scheme code such as this procedure (which looks up an electronic mailbox name in the mail alias database maintained on most UNIX systems):

(define expand-mail-alias
  (lambda (alias)
    (let ((d (dbm-open "/etc/aliases" 'reader)))
      (if (not d)
          (error 'expand-mail-alias "cannot open database"))
        (dbm-fetch d alias)
        (dbm-close d)))))

(define address-of-staff (expand-mail-alias "staff"))

Markup created by unroff 1.0,    September 24, 1996,    net@informatik.uni-bremen.de