INTERFACE IO_impl; (***************************************************************************) (* Copyright (C) Olivetti 1989 *) (* All Rights reserved *) (* *) (* Use and copy of this software and preparation of derivative works based *) (* upon this software are permitted to any person, provided this same *) (* copyright notice and the following Olivetti warranty disclaimer are *) (* included in any copy of the software or any modification thereof or *) (* derivative work therefrom made by any person. *) (* *) (* This software is made available AS IS and Olivetti disclaims all *) (* warranties with respect to this software, whether expressed or implied *) (* under any law, including all implied warranties of merchantibility and *) (* fitness for any purpose. In no event shall Olivetti be liable for any *) (* damages whatsoever resulting from loss of use, data or profits or *) (* otherwise arising out of or in connection with the use or performance *) (* of this software. *) (***************************************************************************) (* This interface is not for general use; it is intended for users who wish to build their own streams for use with the operations in the 'IO' interface. First some terminology: Stream bed: The actual device/file/whatever to or from which the stream is writing or reading. For example it is common to open a stream to a disc file. In this case the disc file is the stream bed. Stream class: 'IO' provides a generic view of streams, regardless of the underlying stream bed. Real instances of streams, however, are tailored to the stream bed they are working with. A stream class is a particular implementation of a stream which is tailored for some type of stream bed. For example most libraries provide a module which allows the user to open streams to disc files. Such a module is said to implement a stream class. Usually there is one stream class per type of stream bed but it is possible to imagine exceptions to this rule. A library might choose to have two stream classes to deal with disc files; one tailored for bulk sequential access and the other tailored for random access of small amounts of data. The streams provided by 'IO' are buffered i.e. puts and gets to a stream do not access the stream bed directly; they access a buffer. Only if the buffer cannot satisfy the put or get operation is there any interaction with the stream bed. Managing the stream buffer is a complex job and it is largely stream class independent. 'IO' aims to do as much of the work as possible; a stream class implementor is left with the job of writing a few, simple class specific routines. This interface, 'IO_impl' aids stream class implementors in that task. The interface of a stream class typically exports an 'Open' procedure which can be used to open a stream of that class. It may also import some stream class specific procedures. Here is a skeleton for the interface and implementation of a stream class: INTERFACE MyStream; IMPORT IO; TYPE T <: IO.Stream; PROCEDURE Open( (* appropriate arguments - e.g. open mode, name etc. *)) : T RAISES {IO.Error}; (* maybe some stream class specific operations *) END MyStream. MODULE MyStream; IMPORT Text; IMPORT IO, IO_impl; REVEAL T = IO.Stream BRANDED OBJECT (* MyStream specific hidden fields *) METHODS (* MyStream specific hidden methods *) implFlush := Flush; implFill := Fill; implSeek := Seek; implTruncate := Truncate; implClose := Close; implDescribeError := DescribeError; implRecover := Recover; END; PROCEDURE Flush(s: T; READONLY chars: ARRAY OF CHAR): BOOLEAN RAISES {} = BEGIN (* MyStream specific flush code *) END Flush; PROCEDURE Fill(s: T; VAR chars: ARRAY OF CHAR): INTEGER RAISES {} = BEGIN (* MyStream specific fill code *) END Fill; PROCEDURE Seek(s: T; pos: CARDINAL): BOOLEAN RAISES {} = BEGIN (* MyStream specific seek code *) END Seek; PROCEDURE Truncate(s: T; pos: CARDINAL): BOOLEAN RAISES {} = BEGIN (* MyStream specific truncate code *) END Truncate; PROCEDURE Close(s: T): BOOLEAN RAISES {} = BEGIN (* MyStream specific close code *) END Close; PROCEDURE DescribeError(s: T): Text.T RAISES {} = BEGIN (* MyStream specific error message code *) END DescribeError; PROCEDURE Recover(s: T; VAR offset, length: CARDINAL): BOOLEAN RAISES {} = BEGIN (* MyStream specific error recovery code *) END Recover; PROCEDURE Open((* appropriate arguments *)): T RAISES {IO.Error} = VAR s := NEW(T); buffer: REF ARRAY OF CHAR; name: Text.T; BEGIN (* set up buffer, length etc. *) IO_impl.Init(s, buffer, length (* maybe more arguments *)); RETURN s; END Open; (* maybe some stream class specific operations *) BEGIN END MyStream. Many stream classes do not export any class specific operations. In this case the 'MyStream' interface can be simplified; it need not declare a new type and the 'Open' procedure can just return an IO.Stream. Streams provide defaults for the flush, fill, seek, truncate, close, error message and recover procedures. These defaults are exported by this interface; see the comments after their declarations for a detailed explanation of what the stream class specific procedures must do. See the comment after the declaration of 'Init' for detailed information on initialising a stream. The fields in the generic part of the Stream (i.e. all those which appear in the identification of Stream below) should not be written or overriden by the stream class specific code. *) IMPORT Thread, Text; FROM IO IMPORT Stream, Property, PropertySet, AllProperties, OpenMode, Error; TYPE Methods = Thread.Mutex OBJECT METHODS implFlush(READONLY chars: ARRAY OF CHAR): BOOLEAN RAISES {}; implFill(VAR chars: ARRAY OF CHAR): INTEGER RAISES {}; implSeek(pos: CARDINAL): BOOLEAN RAISES {}; implTruncate(length: CARDINAL): BOOLEAN RAISES {}; implClose(): BOOLEAN RAISES {}; implDescribeError(): Text.T RAISES {}; implRecover(VAR offset, length: CARDINAL): BOOLEAN RAISES {}; END; REVEAL Stream <: Methods; (* Stream initialization *) CONST DerivedProperties = PropertySet{ Property.Mapped, (* derived from stream bed length *) Property.Readable, Property.Writable, Property.AppendOnly, (* derived from open mode *) Property.Unbuffered (* derived from buffer length *) }; SpecifiedProperties = AllProperties - DerivedProperties; (* the following properties of the stream are specified directly by the user at stream initialization time: 1) Seekable - is the stream random access? The stream must also be mapped. 2) Truncatable - is the truncate operation allowed on the stream (the stream must also be mapped, writable, seekable and not append only). 3) LineBuffered - if a stream is line buffered it is flushed whenever a put of the newline character occurs. The single character put operations for line buffered streams are substantially slower than those of most other streams. 4) IsBuffer - this signifies a special stream where the stream buffer IS the stream bed. For such a stream filling and flushing are meaningless. *) MappedStreamUsualProperties = PropertySet{ Property.Seekable, Property.Truncatable}; MappedOnlyProperties = PropertySet{ Property.Seekable, Property.Truncatable, Property.IsBuffer}; AllModes = SET OF OpenMode{FIRST(OpenMode)..LAST(OpenMode)}; TruncateModes = SET OF OpenMode{OpenMode.Write, OpenMode.WriteAndRead}; AppendModes = SET OF OpenMode{OpenMode.Append, OpenMode.AppendAndRead}; ReadAndWriteModes = SET OF OpenMode{ OpenMode.Update, OpenMode.WriteAndRead, OpenMode.AppendAndRead}; MappedOrUnmappedModes = SET OF OpenMode{OpenMode.Read, OpenMode.Write}; TruncatableModes = SET OF OpenMode{ OpenMode.Write, OpenMode.Update, OpenMode.WriteAndRead}; PROCEDURE Init( s: Stream; buffer: REF ARRAY OF CHAR; length: CARDINAL; mode: OpenMode := OpenMode.Update; properties := MappedStreamUsualProperties; name: Text.T := NIL; blockSize: CARDINAL := 0) RAISES {Error}; (* As described earlier 'Init' is used by a stream class implementor to initialize a newly created stream. The stream creation usually takes the following form: s := NEW(StreamType); (* 'StreamType' overrides the impl methods *) (* set up buffer, name; do stream class specific initialisation etc. *) IO_impl.Init(s, buffer, name (* maybe more arguments *)); RETURN s; The arguments in detail are: s: The stream to be initialized. buffer: The stream buffer. If the stream buffer is NIL 'Error' will be raised. This can happen if the stream class implementor makes a silly error but it is intended to be used if a class specific error occurs when trying to open the stream bed (e.g. failure to open a file). In such a case the stream class implementor saves away the stream class specific error information (for a later call to 'implDescribeError') and calls 'Init' with a NIL 'buffer' to force an 'Error' to be raised. Note that if the buffer contains only 1 element the stream will be unbuffered i.e. a flush will be forced after every put operation. 'buffer' must have at least one element (if non NIL) or a checked runtime error will result. length: The length of the stream. This argument is used to decide whether the stream is mapped or unmapped. If the stream is a mapped stream an accurate length must be specified; for unmapped streams specify 'UnknownLength'. Note that if 'mode' is in 'TruncateModes' the underlying stream bed is always truncated zero length when the stream is opened. 'Init' checks the 'mode' argument and if it is one of these truncating modes the stream length must be either zero or 'UnknownLength' (using 'UnknownLength' with a truncate mode indicates that the length of the stream is meaningless e.g. for a terminal stream). If 'mode' is a truncate mode and 'length' is not zero or 'UnknownLength' a checked runtime error results. If 'properties' indicates that the stream is a buffer stream 'length' must be less than or equal to the buffer length. If 'length' is 'UnknownLength', indicating an unmapped stream, 'mode' must be either 'Read' or 'Write'. mode: Specifies the mode of the stream. Used to determine whether a stream is read only, write only, append only etc. Note that if the mode is in 'AppendModes' the stream position will be set to be the stream length (see 'length' below) and the user is responsible for ensuring that the position in the stream bed really is 'length'. Unmapped streams must have 'Read' or 'Write' mode. properties: The user can specify some of the properties a stream has. These properties are described in more detail earlier in this file, by the declaration of 'SpecifiedProperties'. If the user supplies any properties which are not in 'SpecifiedProperties' a checked runtime error will result. The 'Truncatable' property is a special case because it can be specified by the user but may be overridden by other arguments; if a 'Truncatable' is supplied in 'properties' but 'mode' is not in 'TruncatableModes' the stream will NOT have the 'Truncatable' property. This is not very consistent but is generally easier to use! name: The name of the stream; usually used when printing out information about a stream error. If 'name' is given as NIL it defaults to "(no name)". blockSize: If a stream is to be block aligned 'blockSize' must be specified. A block aligned stream keeps its fills, flushes and seeks on block boundaries, as far as possible (this cannot always be done - consider a seek to a point which is not on a block boundary on a write only stream). A 'blockSize' of zero (the default) indicates a non block aligned stream. If 'blockSize' is non zero 'NUMBER(buffer^) MOD blockSize' must be zero (i.e. the buffer must be an integral number of blocks) and the stream must be mapped (i.e. 'length # UnknownLength'). There are certain constraints on the arguments to 'Init'. One of them, the constraint that 'buffer' must be non NIL is special; if it is not satisfied 'Error' is raised with 'WhyErrant(s) = Fault.Open'. This special case is provided to give the stream class implementor a way of notifying the user that an open operation has failed. The other constraints on the arguments take a harder line. A violation causes a checked runtime error. Here are the "hard" constraints in full (some have already been mentioned): 1) 's' must be an uninitialized stream 2) the number of elements in 'buffer^' must be greater than zero. 3) 'properties' must not contain any 'DerivedProperties'. 4) if the stream is not mapped i.e. 'length = UnknownLength' then it must have 'mode IN MappedOrUnmappedModes' and it cannot be block aligned or have properties in the set 'MappedOnlyProperties'. 5) if 'mode IN TruncateModes' then 'length' must be zero or 'UnknownLength' 6) if 'mode IN ReadAndWriteModes' then 's' must have the 'Seekable' property. 7) if 's' is block aligned the size of the buffer must be a multiple of 'blockSize'. 8) if 'Truncatable' is in 'properties' the stream must also be mapped and seekable. Remember that if 'Truncatable' is specified but 'mode' is in 'TruncatableModes' the stream 's' will not be given the 'Truncatable' property. However, the check that 's' is mapped and seekable will still be done. 9) if 's' has the 'IsBuffer' property 'length' must be less than or equal to the buffer length. *) (* Below are the defaults for the methods which are supposed to be provided by stream class implementors. Each default procedure is followed by a description of what the method is supposed to do plus a list of situations where the default itself is applicable. If the stream class implementor creates a new stream any one of the 'impl' methods which is not explicitly overridden will be set to the appropriate default. *) PROCEDURE DefaultImplFlush( s: Stream; READONLY chars: ARRAY OF CHAR) : BOOLEAN RAISES {}; (* The 'implFlush' method is called by the stream to flush the given array of chars to the stream bed. 'NUMBER(chars)' is guaranteed to be non zero. It is important to realize that 'chars' may not be part of the original stream buffer; 'IO' sometimes bypasses the buffer and flushes directly from an argument array, to avoid extra copying. Thus 'chars' may be arbitrarily large. This is true even of unbuffered streams; an unbuffered stream just guarantees that every put operation will be immediately flushed; if a put operation puts a large block of characters the 'implFlush' method will be given the whole block and asked to flush it. The boolean result of the call indicates whether the flush succeeded. If the flush fails the stream implementation is expected to squirrel away information about the error (in its stream class specific fields) so a suitable error message can be retrieved by a later call to 'implDescribeError'. After a failed flush occurs 'IO' will cause 'Error' to be raised with 'WhyErrant(s) = Fault.Write'. All the chars in the array 'chars' should be flushed. Block aligned streams (streams for which the 'blockSize' field is non zero) have the following guarantee: EITHER the flush is aligned on a block boundary; in this case 'chars' contains some number of blocks possibly followed by a partial block. OR the flush is not aligned on a block boundary; in this case there will be at most enough characters to fill up the rest of the block. The default flush procedure declared here just returns TRUE. It is suitable for streams which discard or do not allow writes and for streams where the buffer is the stream bed (such streams have the 'IsBuffer' property). *) CONST FillFailed = -1; PROCEDURE DefaultImplFill( s: Stream; VAR chars: ARRAY OF CHAR) : INTEGER RAISES {}; (* The 'implFlush' method is called by the stream to fill the given array of chars from the stream bed (e.g. file, terminal etc.). 'NUMBER(chars)' is guaranteed to be non zero. It is important to realize that 'chars' may not be part of the original stream buffer; 'IO' sometimes bypasses the buffer and fills an argument array directly, to avoid extra copying. Thus 'chars' may be arbitrarily large. The result of the call is the number of chars read unless there is an error in which case the result should be usually be negative (more on this later). 'FillFailed' is provided as a convenient error return but any negative number will do). If a fill fails the stream implementation is expected to squirrel away information about the error (in its stream class specific fields) so a suitable error message can be retrieved by a later call to 'implDescribeError'. After a failed fill 'IO' will cause 'Error' to be raised with 'WhyErrant(s) = Fault.Read'. 'NUMBER(chars)' is guaranteed to be non zero. Block aligned streams (i.e. streams for which 'blockSize' is non zero) are guaranteed that the fill starts on a block boundary. The fill procedure should try to entirely fill 'chars'. If 's' is a mapped stream it will have known length and the fill procedure will never be asked to fill beyond the end of stream. So, for mapped streams, failure to completely fill 'chars' is an error and returning any number other than 'NUMBER(chars)' will cause 'Error' to be raised as described above. For unmapped streams, which have unknown length, failure to fill the buffer is not an error. The fill routine should just return the number of characters read (returning zero indicates end of stream). If the fill routine detects an error it should return a negative number (or, alternatively, a number greater than 'NUMBER(chars)'). To summarize. For mapped streams any result other than 'NUMBER(chars)' indicates failure. For unmapped streams any number outside the range zero to 'NUMBER(chars)' indicates failure. Whether a stream is mapped or unmapped is decided at stream initialization time - see the comments explaining 'Init'. The default fill procedure declared here just returns zero. It is suitable for streams which do not permit reading, for streams which always raise end of stream on a get operation and for streams where the buffer is the stream bed (such streams have the 'IsBuffer' property). *) PROCEDURE DefaultImplSeek(s: Stream; pos: CARDINAL): BOOLEAN RAISES {}; (* The 'implSeek' method is called by the stream when random access to the stream bed is required. It is only ever called if the stream has the 'Seekable' property. The boolean result of the call indicates whether the seek succeeded. If the seek fails the stream implementation is expected to squirrel away information about the error (in its stream class specific fields) so a suitable error message can be retrieved by a later call to 'implDescribeError'. After a failed seek 'IO' causes 'Error' to be raised with 'WhyErrant(s) = Fault.Seek'. The seek should set the position in the stream bed to 'pos'. If the stream is block aligned there is no guarantee that 'pos' will always be block aligned. It is guaranteed that '0 <= pos <= s.len'. Note that this guarantee doesn't mean much if 's.len = UnknownLength'. The default seek procedure just returns TRUE. It is suitable for streams which ignore seeks, streams which do not allow seeks and also for streams where the buffer is the stream bed. The last case is not immediately obvious; it works because seeks within the buffer do not cause a call of 'implSeek' and all seeks on a buffer stream must be within the buffer. *) PROCEDURE DefaultImplTruncate(s: Stream; length: CARDINAL): BOOLEAN RAISES {}; (* The 'implTruncate' method is called by the stream to truncate the underlying stream source/destination to 'length'. The boolean result of the call indicates whether the truncate succeeded. If the truncate fails the stream implementation is expected to squirrel away information about the error (in its stream class specific fields) so a suitable error message can be retrieved by a later call to 'implDescribeError'. After a failed truncate 'IO' causes 'Error' to be raised with 'WhyErrant(s) = Fault.Truncate'. The 'implTruncate' method is only called if 's' is writable (but not append only), seekable and mapped. 'length' is guaranteed to be greater than or equal to the current stream bed position and less than or equal to the current stream bed length. The default truncate method just returns TRUE. It is suitable for streams which are unmapped, not writable, not seekable or append only *) PROCEDURE DefaultImplClose(s: Stream): BOOLEAN RAISES {}; (* The 'implClose' method is called by the stream to close the underlying stream source/destination (e.g. file etc.). The result of the call indicates whether the close succeeded. If a close fails the stream implementation is expected to squirrel away information about the error (in its stream class specific fields) so a suitable error message can be retrieved by a later call to 'implDescribeError'. After a failed close 'IO' usually causes 'Error' to be raised with 'WhyErrant(s) = Fault.Close'. Whether or not 'implClose' succeeds the stream is marked as unusable and it is guaranteed that there will be no further calls to its 'implFlush', 'implFill', 'implSeek' or 'implClose' methods. The default close procedure declared here just returns TRUE. It is suitable for streams where the garbage collection of the stream will close the stream satisfactorily (e.g. a stream which outputs to heap blocks) *) PROCEDURE DefaultImplDescribeError(s: Stream): Text.T RAISES {}; (* The 'implDescribeError' method is called by the stream after one of the errors in the set 'IO.ImplFaults' has occured. It is guaranteed that it will not be called otherwise. The describe error method should return a text describing the error as well as it is able; it is free to return NIL if it is part of an unhelpful stream class! Note that if all the implementation has is an error code it can be easily converted into a text by one of the operations in the standard 'Fmt' interface. The text returned should be kept brief and should not mention the name of the stream or the error class (as given by 'WhyErrant(s)'); it should not contain newlines if they can be avoided. It is intended that a library package should be used to output a full message which combines the stream name, error class and implementation specific error message. Normally the 'implDescribeError' method is called immediately after one of the stream class specific methods has been called and has returned a value indicating that it failed. There is a special case for buffer streams i.e. streams where the buffer is the stream bed. If a write overflows the buffer an 'Error' will be raised with 'WhyErrant(s)' equal to 'Fault.Write'. This means that even though a buffer stream need not provide an 'implFlush' method its 'implDescribeError' method may still deal with write errors! The default error text procedure declared here just returns NIL. It is suitable for streams where no errors are possible or where error messages are considered to be unimportant *) PROCEDURE DefaultImplRecover( s: Stream; VAR offset, length: CARDINAL) : BOOLEAN RAISES {}; (* The 'implRecover' method is rarely used. It is called after one of the non permanent errors in the set 'IO.ImplFaults' has occured (the permanent errors are 'Open' and 'Close') and the user of the stream wants to try to use the stream anyway. If the stream bed can still be used the 'implRecover' method should return TRUE and 'offset' and 'length' should be set as follows: 'offset' should be set to the current position in the stream bed 'length' should be set to the stream bed length (or 'UnknownLength' if the stream is not mapped or the stream length was not changed by the errant operation). After a successful call to 'implRecover' the stream is reset - the current buffer is discarded and the stream position is set to 'offset'. During the reset the following checks are made: 1) If 'offset' is greater than the stream length (after it has been adjusted according to 'length') a checked runtime error results. 2) If 's' is a buffer stream 'length' must be 'UnknownLength' or less than or equal to the buffer length. If the stream bed has been rendered useless by the error 'implRecover' should return FALSE; the values in 'offset' and 'length' are not important. The default recover procedure just returns FALSE. It is suitable for streams where no errors are possible or streams which do not (or cannot) recover from an error. *) (* Some stream implementors may add their own stream operations. The following procedures are intended for use in such operations *) PROCEDURE ValidityCheck(s: Stream) RAISES {Error}; (* Most operations check that a stream is valid (i.e. not errant, closed or uninitialized) before doing anything with it. This procedure checks that a stream is valid and raises an appropriate error if it is not. *) PROCEDURE RaiseImplSpecificError(s: Stream) RAISES {Error}; (* If an error occurs in an implementation specific operation an implementor may want a stream error to be raised. The special error class: 'Fault.ImplSpecific' exists for this purpose. The implementor should call 'RaiseImplSpecificError' to raise this error and mark the stream as errant *) (* The following function is useful when building a wrapper stream - i.e. a stream built on top of another stream. *) PROCEDURE BufferSize(s: Stream): CARDINAL RAISES {Error}; (* Returns the size of the buffer of the given stream. Raises 'Error' if the given stream is not usable i.e. if it is closed, errant, disabled or uninitialized. It is a checked runtime error is 's' is NIL *) END IO_impl.