Streams in Allegro CL

The index for the Allegro CL Documentation is in index.htm. The documentation is described in introduction.htm.

This document contains the following sections:

1.0 Simple-stream introduction
2.0 Simple-stream background
   2.1 Problems with Gray streams
   2.2 A new stream hierarchy
3.0 The programming model
   3.1 How to get a simple-stream and how to get a Gray stream
   3.2 Trivial Stream Dispatch
   3.3 Simple-stream Description
4.0 Device Level Functionality
   4.1 Device Interface
5.0 Implementation of Standard Interface Functionality for Simple-Streams
   5.1 Implementation of Common Lisp Functions for Simple-Streams
   5.2 Extended Interface Functionality
      5.2.1 The endian-swap keyword argument to read-vector and write-vector
6.0 Higher Level functions
7.0 Simple-stream Class Hierarchy
8.0 Implementation Strategies
9.0 Control-character Processing
10.0 Device-writing Tips
   10.1 Defining new stream classes
   10.2 Device-open
   10.3 From-scratch device-open
   10.4 Implementation Helpers for device-read and device-write
   10.5 Other Stream Implementation Functions and Macros

Starting in release 6.0, Allegro CL has a new streams model using simple-streams, which formally have no element-type but act in general as if they have element type (unsigned-byte 8). The new implementation is described in this document. Release 5.0.1 and earlier used a stream implementation called Gray streams. That implementation is still supported. It is described in gray-streams.htm

A simple-stream is created whenever a file is opened (with open) without an element-type specified. If open is called with an element-type specified, a Gray stream is created.

The transition from Gray streams to simple-streams should be easy and transparent unless you have done extensive stream customization.

It is unlikely that users who are not concerned with stream details will have to concern themselves with this document. Common stream usages such as opening files for reading and writing) will simply work as expected. If you want things to work virtually identically to how they worked in release 5.0.1, see 3.1 How to get a simple-stream and how to get a Gray stream, where it says that you get 5.0.1 behavior so long as you specify an element-type to open and that you should use character as the element type when your 5.0.1 code did not specify one. Do note that you cannot assume that system-created streams will be Gray streams in 6.0.

1.0 Simple-stream introduction

A new kind of stream is introduced, called a simple-stream. This stream is intended to serve as the new preferred alternative to Gray streams, which is what previous versions of Allegro CL used to implement Common Lisp streams functionality. Simple streams are simpler than Gray streams, easier to extend, and faster. Both kinds of stream may co-exist in a lisp, and compatibility is maintained for the standard Common Lisp streams interface, the Gray streams implementation maintains its compatibility with previous Gray versions with minimal source intervention when they have been used.

The Allegro CL simple-stream is the next generation evolving from the stream of the same name in Common Graphics (the windowing systems used by Allegro CL on Windows). The Common Graphics simple-stream was created as a basis for window-based input and output. The new generation of simple-stream is designed to include this window-based I/O and at the same time to retain the speed always expected for non-window based streams. It is also designed to promote further advances in technology requirements such as International Character sets and server-based I/O.

2.0 Simple-stream background

We felt a new stream implementation was needed because of problems we found with the Gray stream design. In this section, we describe these problems and describe the new implementation.

2.1 Problems with Gray streams

  1. Gray streams distinguish input and output directions per class, forcing combination and mixins in order to model the 3 different modes (input only, output only, and input/output) for various stream classes.
  2. Gray streams, in accordance with CL, distinguish streams by element-type. We have found this to be an unfortunate limitation, since it makes it hard to transfer varying-width elements in the same stream. Allegro CL 5.0.1 introduced bivalent streams, which allow both varying-width elements and character elements to be transferred. This was a step in the right direction, and enables web servers to be written more easily and efficiently.
  3. Gray streams methods, which define the specific streams implementation, are defined immediately below the level of the CL streams interface, which causes a couple of problems:
    1. The CLOS dispatch is performed at a higher level than is necessary, thus creating inefficient instruction execution paths that are not easily optimizable.
    2. The implementation interface of Gray streams, which is specified using CLOS, overlaps in its behavior, thus causing confusion as to what specializations are needed. For example, the obvious implementation for stream-read-char-no-hang is to call stream-read-char after a call to stream-listen. However, since subclassing a stream can result in a version which does not perform this listen/read combination, further subclassing is not possible without having the source to this version, since it is otherwise not possible to know whether to define a method for stream-read-char-no-hang, or for stream-read-char and stream-listen, or perhaps for all three, in which case it would be unknown which code would be actually executed.
  4. Gray streams force the duplication of a large amount of code, for the implementation of the basic functionalities such as stream-read-char. In a sense, this is due to the fact that the implementation level is too high, and this forces the duplication of effort in the implementation.

2.2 A new stream hierarchy

A new class hierarchy of streams with a base-class of simple-stream removes the problems inherent in Gray streams. Simple-streams are more efficient, and are simpler in concept, making it easier to extend the streams interface by object-oriented means.

A major simplification in the simple-stream hierarchy over Gray streams is the collapsing of many class distinctions into one:

The Gray streams class hierarchy distinguishes between different kinds of stream usage. The simple-stream hierarchy distinguishes between different kinds of external I/O device or pseudo-device.

This device-level interface is CLOS oriented, but is at a much lower level than the Gray streams implementation level, making it much more efficient in both execution speed and in space.

3.0 The programming model

3.1 How to get a simple-stream and how to get a Gray stream

The Gray streams implementation (which has been in Allegro CL for some time) will continue to be available in release 6.0. Programs using customized Gray streams will, therefore, continue to work as in earlier releases with only minor changes. (Users of Gray streams must ensure, as described below, that calls to open have the element-type keyword argument specified -- if it is unspecified, give it the value character. Users must also be careful not to assume a stream they did not create is a Gray stream, and that can be done using synonym streams or by loading a compatibility package, again as described below in this section. Gray streams are described in gray-streams.htm.)

The normal mechanism used to specify a simple-stream is to call open (and thus any callers of open, such as with-open-file) to open the stream without specifying the element-type. This will cause a simple-stream to be created, instead of a Gray stream. A Gray stream will be created if an element-type argument of any kind is given to the open call.

A potential problem arises when legacy code requiring Gray streams calls open with no element-type argument. Under CL specification this kind of open causes the element-type to default to character. All CL functionality will be compatible between Gray streams and simple-streams, but if the user was counting on specific Gray stream functionality in character streams, then the open call must be changed to include :element-type 'character as arguments, which will force a Gray stream.

Another problem arises when Gray streams application code assumes that the stream it is handed is a Gray stream, and thus tries to call stream- methods on it. (Allegro CL names the Gray streams CLOS substrate stream-[cl-function-name], e.g. stream-read-char for read-char.) Allegro CL with the new stream implementation solves this problem by defining its synonym-stream implementation as a Gray stream, but in such a way that all calls via the synonym-stream-symbol are non-Gray-specific. For example, a call to stream-read-char to a synonym-stream will result in a call to read-char on the synonym-stream-symbol. This is slightly slower than dispatching on stream-read-char, but it does provide for compatibility with legacy code. A programmer who doesn't want to rewrite a subsystem to use simple-streams can simply ensure that any stream passed to that subsystem is a synonym stream.

If the stream being manipulated is one that is not easily wrapped as a synonym-stream, (e.g. *terminal-io*) a second approach is provided in the form of the module :gray-compat. This module contains methods on simple-streams for generic functions normally associated with Gray streams. If, for example, the call

(stream-read-char *standard-input*) 

exists in legacy code, then requiring the :gray-compat module (by evaluating (require :gray-compat)) defines a method on stream-read-char for simple-streams, which simply wraps some argument and return-value processing around a call to read-char. This again is slower than calling read-char directly, but provides compatibility for legacy code.

3.2 Trivial Stream Dispatch

CL stream-functions read-byte, write-byte, read-char, etc., all distinguish in a trivial manner whether the stream is a Gray or simple stream. If a Gray stream is detected, the associated Gray generic function is called for the stream, so that for example, read-char calls stream-read-char, write-char calls stream-write-char, etc. However, if the stream is determined to be a simple-stream, then the specified lower level functionality for the function is called, which may involve calls to specific device-level functionality. This is described in section 5.1 Implementation of Common Lisp Functions for Simple-Streams below.

Note that this trivial dispatch does not use any CLOS dispatch mechanism, and the functionality that is called for a simple-stream may be inlined in the function. Thus, for example, all write-byte operations for a simple-stream are performed without any function calls, unless the buffer fills up and device-write or device-extend must be called.

3.3 Simple-stream Description

A simple-stream has no specific element-type associated with it. Instead, the fundamental unit of transfer for a simple stream that is not a string stream is the octet or 8-bit byte, and all transfers are made at the lowest level with respect to octets. It is up to the implementation to decide how to optimize data transfers for particular situations where data paths are either wider or narrower than 8 bits.

A simple-stream is always buffered. Whereas support is provided for buffering for CL and Gray streams, buffering is not explicitly required in these stream specifications. However, the explicit requirement that simple-streams are buffered allow a simpler and potentially more efficient model. Note that there is no direct interface to simple-stream buffers. The buffering layer resides just below the CL interface level, and the device layer is just below the buffering layer:

       User Level              Strategy Level         Device-level      
                          |                      |                      
    --------------------                                                
   |  CL functionality  | |                      |                      
    --------------------                                                
     |    |         |     |                      |                     
     |    |          -------------------------------------              
     |    |               |                      |         |            
     |    |                                                v            
     |    |               |                      |  ------------------- 
     |    |                                        | Control-character |
     |    |               |                      | |   processing      |
     |    |                                         ------------------- 
     |    |               |                      |    |    |   |        
     |    |                           .---------------     |   |        
     |    |               |           v          |         |   |        
     |    |                  ------------------            |   |        
     |    |               | | external-format  | |         |   |        
     |    |                 |   processing     |           |   |        
     |    |               |  ------------------  |         |   |        
     |    |                        |                       |   |        
     |     -------------------.    |    .------------------    |        
     |                    |   v    v    v        |             |        
     |                       ---------------                   |        
     |                    | |   Buffering   |    |             |        
     |                       ---------------                   |        
     |                    |        |             |             |        
     |                              -----------------------.   |        
      --------------------------------------------------.  |   |        
                          |                      |      v  v   v        
                                                    -----------------   
                          |                      | |   Device layer  |  
                                                    -----------------   

String streams bypass the external-format (that is, External File Format, as defined in Common Lisp) processing, since their destinations are not really external.

Programmers will work with simple-streams at various levels, wearing one of three different hats at any one time:

  1. As an applications programmer (or as a user), who calls the standard interface functionality (including standard Common Lisp and related functions described in 5.1 Implementation of Common Lisp Functions for Simple-Streams below).
  2. As a device-level programmer, who extends the stream interface by writing device-level methods for subclassed streams.
  3. As a strategy-level programmer, who implements the standard interface functionality (which calls the device-level functionality). Strategy is an advanced level and most programmers will not need to program at this level (using instead, the tools already provided). Strategy is discussed in this document but explicit strategy-level programming rules and tips are outside its scope.

There is also a set of functions provided which aid in the implementation of the device layer, and which at the same time are themselves User Level functions. These functions allow the design of encalpsulating streams, where the encapsulating stream's device level becomes the encapsulated stream's user level. These functions are discussed in 10.4 Implementation Helpers for device-read and device-write.

The intended interaction by the user or applications programmer is to work above the buffer level. The user does so by calling standard CL functions. The device-level programmer may define new classes and device-level methods for "drivers" (we are using the word analogously to device drivers that are implemented in operating systems), but even then it is not intended that the user call the device-level methods directly. But note that it is possible to call device-level methods if all of the rules are followed. The strategy-level programmer may design an alternate API that calls the device-level, but it must conform to the requirements that allow the device-level to work properly. The intended role of the API is that it is a thin layer which manipulates its buffer and thus deals with the device layer as little as possible. Such API's are intended to be very fast.

4.0 Device Level Functionality

The device level is called that because it provides an underlying implementation that can be specialized to suit particular kinds of stream connections, in a similar manner to a device driver in an operating system.

Only simple-streams provide a device layer; Gray streams do not. The device layer puts the object implementation of a simple-stream at a lower level than the object layer of Gray streams.

The goals of the device layer are:

The device layer is not intended to be called directly, except by strategies for higher-level API interfaces that conform to strategy rules. Such APIs should be very lightweight and fast so that there is no need or temptation to call the device-layer directly. Creators of such higher-level APIs must be especially careful to understand the buffering issues involved, including those described in device-read and device-write.

Note that the device layer can implement whatever kind of connection it is set up to do. Usually this means that it will talk directly to a file handle or file descriptor number. However, the connection can be made to a stream of a different type instead of directly to an operating-system level file. By this means, Java style stream encapsulations can be created by the device-level programmer.

Such encapsulation functionality is done automatically by some functions provided as implementation helpers (ee 10.4 Implementation Helpers for device-read and device-write).

4.1 Device Interface

Simple-streams are normally opened with device-open and closed with device-close. device-buffer-length returns the desired length of buffers to be allocated for the stream, if any. device-file-position returns a positive integer that is the current octet (8-bit byte) position of the device represented by its argument stream. device-file-length returns the number of octets (8-bit bytes) in the argument stream if possible.

device-read fills a buffer (if possible) with data from its argument stream. device-clear-input clears any pending input on the device connected to its argument stream. device-write writes from the buffer to the argument stream. device-clear-output clears pending output.

Methods that don't fall under the strict buffer-unaware read-write device methods include device-extend and device-finish-record. Unlike device-read and device-write, these methods may manipulate stream slots, allocate new work spaces, or call out recursively to higher level stream functions. The intention here is to separate the pure fill and flush aspect of device-read and device-write from the more complex aspects of mapping and record-orientation.

The one exception to the buffer-unaware separation in device-read and device-write is when they receive a null buffer argument from the strategy layer, and their start and end arguments are not the same. This will occur if the buffer that would have been passed is the actual buffer of the stream.

Under this circumstance the device-read/device-write method has a little leeway; it must assume that the null buffer argument refers to the appropriate buffer in the actual stream, and must retrieve that argument for use. However, it is free to detach and/or replace the buffer with another of the same size. Also, in the case of device-read, the length of the buffer must be used as the end argument, which will also be nil if the buffer argument is nil (unless end is also eql to start). This flagging of the stream's buffer enables device-read and device-write methods to be written that perform advanced buffer-management and asynchronous read-write operations.

5.0 Implementation of Standard Interface Functionality for Simple-Streams

The first subsection describes the implementation of standard Common Lisp functions that deal with streams. Note that the behavior is usually different for Gray streams (where the CL function usually calls an Allegro-CL-specific associated generic functions) and for simple-streams (on which the CL function usually operates directly).

The second subsection describes additional functions that operate on streams, but are specific to Allegro CL.

5.1 Implementation of Common Lisp Functions for Simple-Streams

Given the device interface, we can now describe how standard Common Lisp functions and some related Allegro CL functions are implemented in terms of these driver functions. Because the intention of this section is to provide implementation information, but not to describe how to use the functions, usage details such as argument lists are not provided.

open

Function

Package: common-lisp

See open for the ANSI description.

For both Gray and simple streams, open effectively turns into a call to make-instance of a stream class. Additionally, for simple-streams, a shared-initialize after method calls device-open to actually establish the connection with the external device or file. If the device-open call then fails and thus returns nil, then device-close is called immediately with a true abort argument.

As it did in previous versions of Allegro CL, cl:open has an &allow-other-keys specification, and an &rest argument. This &rest argument forms the basis of the make-instance initargs when it is called via apply.

A special case exists for an open with :direction :probe: this case is not a normal open and does not actually result in a connection of any kind being made. Instead, make-instance is called to make an instance of probe-simple-stream.

close

Generic Function

Package: common-lisp

See close for the ANSI description.

The Gray stream system in Allegro CL implements close as a generic function, which is perfectly legal according to CL, which defines close as a function (i.e. a generic function is indeed a function). However, a generic function implies a specialization capability that does not exist for simple-streams; simple-stream specializations should be on device-close. Besides Gray streams, close can be specialized on streams that are neither Gray or simple-streams. One example of this is Allegro CL's passive socket connection. Because of this, close remains a generic function, but for simple streams is treated as if non-generic, that is simple-streams should not specialize on close, but should specialize on device-close instead. The method for simple-streams simply calls device-close precisely once, and a method for fundamental stream (the top-level Gray stream class) does what it did for previous versions of Allegro CL: it breaks the connection and sets a closed-flag in the stream.

If the abort keyword argument is true, any buffers are cleared without being flushed. If abort is false, then any unflushed buffers are forced out to the device before closing.

read-byte

Generic Function

Package: common-lisp

See read-byte for the ANSI description.

For a Gray stream, read-byte calls stream-read-byte.

Otherwise: If the stream's buffer is empty, an attempt is made to fill the buffer by calling device-read with the blocking argument set to true. If -1 was returned, then we are at eof; either eof-value is returned or else an end-of-file error occurs.

If the stream's buffer is now not empty, the next octet (8-bit byte) is extracted from the buffer and returned.

read-char

Function

Package: common-lisp

See read-char for the ANSI description.

For a Gray stream, read-char calls stream-read-char.

Otherwise: The external format is called to accumulate (as if using read-byte) as many octets (8-bit bytes) as is necessary to form a character. If an end-of-file is generated by any of the read-bytes, eof processing is done depending on the eof arguments.

If the character that results is a control character and the control-in table has a function for that character, then it is a function with two arguments (the stream and the character) which is called to interpret the control-character at this time. If the control-in function returns, it returns either a character which is processed normally, or nil, which is interpreted as an eof and eof processing is done. Note that the control-in handler must not try to do any reading from the stream at all; the intention for the control-in handler is to translate an already-received character to another, or to perform an operation and return a character. For ligatures and other multiple-character inputs, a composing external-format should be used or created, or else an encapsulation created for such translations.

If we got this far, the character length is recorded for unread-char and the character is returned.

Note that if eof occurs while reading a character, the actions taken by read-char depend on the external-format. The default action, and by far the most common, is to do eof processing. However, the external format may decide to return a character (saved from a previous read-char) or to generate an error.

unread-char

Function

Package: common-lisp

See unread-char for the ANSI description.

For a Gray stream, unread-char calls stream-unread-char.

Otherwise: if the unread-char character-length is set, then place the buffer and file position back to that and unset the unread-char length. Error if the unread-char character-length is not set.

read-char-no-hang

Function

Package: common-lisp

See read-char-no-hang for the ANSI description.

For a Gray stream, read-char-no-hang calls stream-read-char-no-hang.

Otherwise: The external format is called to accumulate (as if using excl::read-byte-no-hang) as many octets (8-bit bytes) as is necessary to form a character. If an end-of-file is generated by any of the byte reads, eof processing is done depending on the eof arguments.

If the character is a control character and the stream-class specifies interpretation of such characters, it is performed at this time, which may include eof-processing for a control-D.

If we got this far, the character length is recorded for unread-char and the character is returned.

Note that if it is not possible to complete the build of the character, the actions taken by read-char-no-hang depend on the situation:

peek-char

Function

Package: common-lisp

See peek-char for the ANSI description.

For a Gray stream, peek-char calls stream-peek-char. Otherwise: a read-char equivalent is done, followed by an unread-char.

listen

Function

Package: common-lisp

See listen for the ANSI description.

An extra argument above and beyond the CL spec is added to listen that specifies whether to listen for only an octet or to listen for a complete character. This solves the problem of having only a partial character in the stream, thus causing the next read-char to hang. The default for this extra listen-for-character argument is false, which provides the standard CL functionality to listen for any data.

For a Gray stream, listen calls stream-listen.

Otherwise: If the buffer is not empty, true is returned. Otherwise device-read is called with a null blocking argument. If that returns 0, then nil is returned, otherwise true is returned.

If the added optional argument is nil, only an octet (8-bit byte) is looked for, otherwise external-format processing is used to attempt to build a character in a non-blocking way; if it is determined that the character can definitely be built, then t is returned. However, the state of the stream is left in such a way that an unread-char can be done even after the listen (as is appropriate).

read-line

Function

Package: common-lisp

See read-line for the ANSI description.

For a Gray stream, read-line calls stream-read-line and processes the return values according to eof-error-p processing.

Otherwise: String buffers are allocated as necessary and read-char equivalent is performed until either a #\newline or eof is seen. A new string is allocated of the proper length and filled with the copied data from the temporary buffer(s) and then returned along with the missing newline flag.

Note: The read-line functionality can be optimized in the following way: A string buffer is allocated (this first one presumably on the stack) and read-char equivalent is performed until the next #\newline or eof is seen (or until the buffer is full, at which time new buffers are allocated as necessary). A new string of the proper length is then constructed and filled with the copied data from the temporary buffer(s) and then returned along with the missing newline flag.

read-sequence

Function

Package: common-lisp

Arguments: sequence stream &key start end partial-fill

See read-sequence for the ANSI description. Note that Allegro CL uses the additional partical-file keyword argument, which is not specified in ANSI CL.

For a Gray stream, read-sequence calls stream-read-sequence.

Otherwise: If the sequence is a string, then for every element of the string, a read-char equivalent is performed. Following the last read-character, the unread-char length is set (instead of at every character read).

If the sequence is an octet vector (i.e. a vector of (signed-byte 8) or (unsigned-byte 8) elements), then the equivalent of read-vector is performed.

Any other sequence type generates an error (for a simple-stream).

The partial-fill keyword argument

This argument controls the behavior when there are not enough objects (of whatever is being read) on stream to fill the sequence passed as the first argument (at least as far as end, if given) and no EOF is seen. The ANSI specification for read-sequence requires it to block is until the sequence is filled or an EOF is seen. In the Allegro CL implementation, the ANSI behavior (blocking) is observed if partial-fill is nil (the default).

If partial-fill is true, however, read-sequence will block for the first element, but will not block for any elements after the first, and so may return prior to the request being completed.

In all cases, read-sequence returns the index in the sequence of the next element not read.

Note: the partial-fill is new in release 6.0. In release 5.0.1, the non-blocking (and thus non-ANSI-compliant) behavior was implemented for buffered bivalent streams used for sockets in most but not all cases (the exact details are complicated). Users used to the non-blocking behavior will be surprised by the blocking behavior. Now, non-blocking behavior only occurs when partial-fill is specified true. Users expecting non-blocking behavior should specify partial-fill true.

clear-input

Function

Package: common-lisp

See clear-input for the ANSI description.

For a Gray stream, clear-input calls stream-clear-input. Otherwise: if there is any input buffering in the stream, it is thrown away. Then device-clear-input is called.

write-byte

Function

Package: common-lisp

See write-byte for the ANSI description.

For a Gray stream, write-byte calls stream-write-byte.

Otherwise: If the buffer is full, device-write is called to first write the buffer out, so that the buffer is made empty. An octet (8-bit byte) is expected as input. It is now stored into the stream's (non-full) buffer.

This is the lowest level functionality in the output portion of the CL API functions. Higher level functions which may call this function are: write-char, write-sequence, write-vector. Whether or not these functions actually call write-byte, call an internal but similar function, or expand all of write-byte's functionality inline is not specified.

write-char

Function

Package: common-lisp

See write-char for the ANSI description.

For a Gray stream, write-char calls stream-write-char.

Otherwise: If the character to be output is a control character, the control-out table is consulted for a control-out function for that character. If one exists it is assumed to be a function of two arguments (the stream and the character), and is called for device-level processing. If the control-out function exists and returns non-nil, then no further action is taken for this character since it was handled successfully in the control-out function. If the control-out function does not exist or exists and returns nil, then normal processing continues for that character. Normal processing means that the character is treated as itself, to be sent uninterpreted to the stream.

The external-format functionality currently in effect is called for the character, which may result in any number of octets (8-bit bytes) being generated. These octets are then treated as if write-byte were called for each one, in the order they were received from the external-format processing.

write-string

Function

Package: common-lisp

See write-string for the ANSI description.

For a Gray stream, write-string calls stream-write-string.

Otherwise: For each character in the specified range in the string, the equivalent of a write-char is performed.

write-sequence

Function

Package: common-lisp

See write-sequence for the ANSI description.

For a Gray stream, write-sequence calls stream-write-sequence.

Otherwise: If the sequence is a string, then the equivalent of write-string is performed.

If the sequence is an octet vector (i.e. a vector of (signed-byte 8) or (unsigned-byte 8) elements), then the equivalent of write-vector is performed. Any other sequence type generates an error (for a simple-stream).

terpri

Function

Package: common-lisp

See terpri for the ANSI description.

For a Gray stream, terpri calls stream-terpri. Otherwise: the equivalent of a write-char of #\newline is performed.

fresh-line

Function

Package: common-lisp

See fresh-line for the ANSI description.

For a Gray stream, fresh-line calls stream-fresh-line. Otherwise: if the stream can be determined to be at the start of a line, then nothing is done and nil is returned, otherwise the equivalent of a write-char of #\newline is performed.

finish-output

Function

Package: common-lisp

See finish-output for the ANSI description.

For a Gray stream, finish-output calls excl::stream-finish-output. Otherwise: if there is any output in the stream's output buffer, is is written via device-write with a non-nil blocking argument.

Note that since Allegro CL does not queue writes, and since device-write calls are not required to write all of the requested bytes, the current implementation of finish-output loops on device-write calls until all of the unprocessed data are transferred.

force-output

Function

Package: common-lisp

See force-output for the ANSI description.

For a Gray stream, force-output calls stream-force-output. Otherwise: if there is any output in the stream's output buffer, is is written via device-write with a blocking argument of nil.

Note that since Allegro CL does not queue writes, and since device-write calls are not required to write all of the requested bytes, the current implementation of force-output is similar to finish-output, in that it loops on device-write calls until all of the unprocessed data are transferred.

clear-output

Function

Package: common-lisp

See clear-output for the ANSI description.

For a Gray stream, clear-output calls stream-clear-output. Otherwise: the stream's output buffer is cleared and device-clear-output is called on the stream.

file-position

Function

Package: common-lisp

See file-position for the ANSI description.

For a Gray stream, file-position calls excl::stream-file-position.

Otherwise: For simple-streams that are not string simple-streams, file-positions are always specified as a number of octets (8-bit bytes). For string simple-streams, file-positions are specified as number of characters.

If the position-spec argument is not given, the file position is calculated, possibly involving a call to device-file-position, and returned. Note that the file position may be precached in the stream, and device-file-position may have been called by some other CL functions.

If the position-spec argument is given, then the new file position is calculated and stored. This may involve a call to (setf device-file-position), if the position is outside of the buffer range.

stream-element-type

Function

Package: common-lisp

See stream-element-type for the ANSI description.

For a Gray stream, stream-element-type returns the appropriate value.

For a simple-stream, stream-element-type always returns (unsigned-byte 8).

5.2 Extended Interface Functionality

These additional functions are provided in Allegro CL for operating on streams. Each is described on its own documentation page.

5.2.1 The endian-swap keyword argument to read-vector and write-vector

The endian-swap keyword argument to read-vector and write-vector allows the byte-ordering to be controlled so as to allow big-endian and little-endian machines to communicate with each other. Each version of Allegro CL has either :big-endian or :little-endian on its *features* list to identify it appropriately. The endian-swap argument is effective only in reads into and writes from vectors that are not strings, and is silently ignored if given when a string is being passed to read-vector or write-vector.

There are three kinds of value that can be given to the endian-swap argument:

The byte-swapping mechanism relies on the fact that objects are always aligned on 8 or 16 byte boundaries, depending on whether the lisp is a 32-bit or 64-bit lisp. Therefore, it is unadvisable to specify a numeric value of greater than 7 (in a 32-bit lisp) or 15 (in a 64-bit lisp).

The byte swapping mechanism is in fact implemented by performing a logxor on the current index of the next byte to get out of the vector. The resultant xor'ed index is used as the true byte index into the array. No attempt is made to ensure that the index is valid (within range): it is the user's responsibility to ensure that. This is always ensured if the endian-swap specification matches the element width of the vector (e.g. an (unsigned-byte 16) vector is given an endian-swap value of :byte-16, or a double-float vector is given an endian-swap value of :byte-64).

The following table shows how the bytes actually appear after swapping. Since the swapping is symmetrical, it can be used in either direction, for both reading and writing. Given the natural byte order of bytes A, B, C, D, E, F, G, H to start, the table shows the byte order of the resultant bytes for some example cases:

name           value           order

:byte-8           0    A  B  C  D  E  F  G  H
:byte-16          1    B  A  D  C  F  E  H  G
  ----            2    C  D  A  B  G  H  E  F
:byte-32          3    D  C  B  A  H  G  F  E
  ----            4    E  F  G  H  A  B  C  D
  ----            5    F  E  H  G  B  A  D  C
  ----            6    G  H  E  F  C  D  A  B
:byte-64          7    H  G  F  E  D  C  B  A
                  ...

6.0 Higher Level functions

These functions are all written as if they call lower level CL functions, and do not necessarily call device-level functionality directly.

format
pprint
prin1
prin1-to-string
princ
princ-to-string
print
read
read-delimited-list
read-from-string
read-preserving-whitespace
write-line
write-to-string

7.0 Simple-stream Class Hierarchy

The class hierarchy for streams starts with stream at the head, and implementations which include other stream classes such as Gray streams will place those stream classes as subclasses of stream. In Allegro CL, the only subclasses of stream are simple-stream and fundamental-stream. fundamental-stream denotes a Gray stream.

The simple-stream class hierarchy is divided into three fundamental simple-stream classes (which in turn have subclasses not listed in the diagram), based on the kinds of buffering they do:

          --> fundamental-stream ...
         |      (Gray streams)
         |
stream --+
         |                     --> single-channel-simple-stream ...
         |                    | 
          --> simple-stream --+--> dual-channel-simple-stream ...
                              | 
                               --> string-simple-stream ...

These simple-stream subclasses cannot be mixed. They are intended to implement three styles of input/output in fundamentally different ways.

single-channel-simple-stream
dual-channel-simple-stream
string-simple-stream

8.0 Implementation Strategies

The basic behavior of the Common Lisp functions is described in 5.1 Implementation of Common Lisp Functions for Simple-Streams. This description must be taken on an as-if basis, which means that the specific functions described may not actually be called at all, or else they might be implemented using compiler-macros to call lower-level functions after type inferencing proofs have been established. However, the device-level interface does not have this freedom; those methods applicable for the stream class must be called in the way specified. This is to guarantee to the device-writer that methods that are written for a particular purpose will indeed be called.

However, the selection of methods to call when appropriate depends on the strategy used. Listed below are various sets of functions that are called for various stream types.

9.0 Control-character Processing

Whenever a control-character is seen when reading or writing on a stream, a decision must be made as to what to do with these characters. In a "raw" environment, the characters are processed as themselves; when writing they are inserted into the buffer (possibly after translation to octet form) and when reading they are simply returned as Lisp characters (possibly after having been assembled from octet form). In a "cooked" environment, at least some control characters turn into instructions at the device level, and are not inserted into or extracted from the stream as characters.

An example of this is terpri, which is simply a write-char of a #\newline. On a terminal stream, a terpri simply sends the #\newline as a character (though its sending may require a column indicator in the stream to be set to 0 as well). However, a window stream should not see a #\newline at the device-level, instead the action should be to "move the cursor down one line and to the far left side of the window".

The simple-streams design allows for both of these kinds of action. Each stream has two slots, a control-in slot and a control-out slot, which may contain tables of functions that are consulted when the character being read or written is determined to be a control character. The actions taken are as follows:

Control-tables are built with make-control-table and are stored into the appropriate control-in or control-out slots by device-open.

10.0 Device-writing Tips

This section gives some tips for device-writing. It is not comprehensive, and some of the functions and macros it refers to may or may not be documented. The section is Allegro CL specific, but may be taken as a guide for other implementations as well.

10.1 Defining new stream classes

New stream classes may be created which subclass existing classes. If the superclass chosen is a currently instantiable class, such as terminal-simple-stream, file-simple-stream, etc., then the device methods may be used as they are, or they may be called by call-next-method by the more specialized method. If the superclass chosen is one of the three major streams (single-channel-simple-stream, dual-channel-simple-stream, or string-simple-stream) then much of the device functionality will have to be written from scratch. There may be some methods that exist to provide defaults (for example, the default device-buffer-length method specializes on simple-stream to provide a default for all simple-streams). Other methods, such as device-open, have no appropriate default action, and are thus not supplied.

To define a new stream class in Allegro CL, the iodefs module must be required to provide some defining macros. The class may be then defined using excl::def-stream-class:

(require :iodefs) 

(def-stream-class blarg (terminal-simple-stream) 
  ((slot1 :initform nil) 
   (slot2 :initform nil :accessor blarg-slot1)) 
  (:default-initargs :input-handle 
        (error "blarg stream must have a :input-handle arg"))) 

10.2 Device-open

Each primary method to device-open returns a stream that is fully connected to its device; it can perform all operations intended on that device. When a primary method performs a call-next-method to do a device-open on a less-specific device, that functionality is complete when the call-next-method returns.

For example, suppose a whiz-bang is a type of file which has a header line associated with it, to be internalized and then ignored as data. The whiz-bang stream might be defined as

(def-stream-class whiz-bang 
                 (file-simple-stream) 
  ((header :initform nil :accessor whiz-bang-header))) 

The device-open for whiz-bang might call the primary-method for the file, and then do its own work afterward:

(defmethod device-open ((stream whiz-bang) options) 
  (declare (ignore options)) 
  (let ((success (call-next-method))) 
    (when success ;; read and internalize the header 
       (setf (whiz-bang-header stream) (read-line stream)) 
     t))) 

Note that:

  1. The stream is fully operational as a file-simple-stream after the call-next-method, unless it returns nil, which indicates that the device-open failed. File operations may thus be performed on the stream.
  2. Whiz-bang operations may be performed on the stream after successful return from device-open at this level. Presumably this might include querying the whiz-bang-header slot for its content or for a print-method.
  3. Before, around, and after methods should not be used to perform initializations that might be used by any more-specific device in its device-open call.

10.3 From-scratch device-open

A device-open that does not call-next-method must perform the following:

  1. It must make the connection with its device, perhaps using an operating system call or other low-level mechanism. This includes setting the input-handle and/or output-handle slot, if appropriate.
  2. It must set the external-format, based on the options given. Non-generic functions provided are single-channel-install-ef-methods, dual-channel-install-ef-methods, and string-install-ef-methods. (setf stream-external-format) may be used to accomplish this in a generic way.
  3. It must install the buffer(s) into the stream. Any buffers to be installed are obtained either by finding them in the options list or by allocating them after calling device-buffer-length to determine what length of buffer to allocate. Buffers that already exist in a resourced stream may be reused, if appropriate.
  4. It must set the instance flags as appropriate. The instance flags byte is not the same as the flags slot in the stream. The instance-flags can only be seen by inspecting the stream in "raw" mode (see inspector.htm for information on raw mode). The flag bits are accessed very quickly to determine what kind of stream it is: gray or simple, single/dual/string, input and/or output, and possibly xp (i.e. pretty-printing string). A stream that does not have these flags set will not be streamp, even if it is of stream class. The add-stream-instance-flags macro is provided to add appropriate flag bits. The actual format of the flags is not discussed in this document.

10.4 Implementation Helpers for device-read and device-write

The following functions allow device-read and device-write methods to be implemented. They should never be used at any level other than the device-level, because they do only minimal checking on their arguments.

Note that the supplied device-read and device-write functions do not generate errors themselves, but pass them back to the higher level for processing. This allows read-octets and write-octets to pass errors back as well, as the implementation of a higher level (encapsulating) device-read and device-write.

10.5 Other Stream Implementation Functions and Macros

The following operators are named by symbols exported from the excl package. They are loaded with

(require :iodefs)

Copyright (c) 1998-2000, Franz Inc. Berkeley, CA., USA. All rights reserved. Created 2000.10.5.