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 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);
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;
dbm_close(DBMF(d)->dbm);
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"))
(unwind-protect
(dbm-fetch d alias)
(dbm-close d)))))
(define address-of-staff (expand-mail-alias "staff"))