EAStream

Introduction

EAStream is the core IO interface for all of UTF. It consists of typedefs, enumerations, and the IStream interface class.

The IStream class looks like this:

class IStream
{
public:
    virtual          ~IStream();
    virtual int       AddRef() = 0;
    virtual int       Release() = 0;
    virtual uint32_t  GetType() const = 0;
    virtual int       GetAccessFlags() const = 0;
    virtual int       GetState() const = 0;
    virtual bool      Close() = 0;
    virtual size_type GetSize() const = 0;
    virtual bool      SetSize(size_type size) = 0;
    virtual off_type  GetPosition(PositionType positionType) const = 0;
    virtual bool      SetPosition(off_type position, PositionType positionType) = 0;
    virtual size_type GetAvailable() const = 0;
    virtual size_type Read(void* pData, size_type nSize) = 0;
    virtual bool      Flush() = 0;
    virtual bool      Write(const void* pData, size_type nSize) = 0;
};

Most of this document will revolve around the discussion of the IStream class.

Design considerations

The following are class-wide design considerations. Per-function issues are discussed in the function documentation itself.

Best Practices

We list some best practices regarding stream usage.

Subclassing IStream

We will discuss two ways of extending the IStream interface: subclassing and adapting. In this section we cover subclassing, and in the next we cover adapting.

Subclassing an IStream is probably best described by example. In the class below we have a FileStream, which implements the IStream interface for disk files. The code that's highlighed in blue is the new code added by the FileStream class on top of what already exists in IStream.

class FileStream : public IStream
{
public:
    enum Type
    { 
        kTypeFileStream = 0x34722300
    };
 
    enum Share
    {
        kShareNone   = 0x00,     /// No sharing.
        kShareRead   = 0x01,     /// Allow sharing for reading.
        kShareWrite  = 0x02,     /// Allow sharing for writing.
        kShareDelete = 0x04      /// Allow sharing for deletion.
    };
 
    enum UsageHints
    {
        kUsageHintNone       = 0x00,
        kUsageHintSequential = 0x01,
        kUsageHintRandom     = 0x02
    };
 
public:
    FileStream(const char* pPath8 = NULL);
    FileStream(const char16_t* pPath16);
 
    // FileStream
    // Does not copy information related to an open file, such as the file handle.
    FileStream(const FileStream& fs);

    virtual ~FileStream();

    // operator=
    // Does not copy information related to an open file, such as the file handle.
    FileStream& operator=(const FileStream& fs);

    virtual int       AddRef();
    virtual int       Release();

    virtual void      SetPath(const char* pPath8);
    virtual void      SetPath(const char16_t* pPath16);
    virtual size_t    GetPath(char* pPath8, size_t nPathLength);
    virtual size_t    GetPath(char16_t* pPath16, size_t nPathLength);

    virtual bool      Open(int nAccessFlags = kAccessFlagRead, int nCreationDisposition = kCDDefault,
                           int nSharing = kShareRead, int nUsageHints = kUsageHintNone); 
    virtual bool      Close();
    virtual uint32_t  GetType() const { return kTypeFileStream; }
    virtual int       GetAccessFlags() const;
    virtual int       GetState() const;

    virtual size_type GetSize() const;
    virtual bool      SetSize(size_type size);

    virtual off_type  GetPosition(PositionType positionType = kPositionTypeBegin) const;
    virtual bool      SetPosition(off_type position, PositionType positionType = kPositionTypeBegin);

    virtual size_type GetAvailable() const;

    virtual size_type Read(void* pData, size_type nSize);
    virtual bool      Write(const void* pData, size_type nSize);
    virtual bool      Flush();
};

The UTF provides a formal FileStream class like the one above. Other subclasses of IStream are implemented in the UTF, including a few in the ResourceMan module. Also of note is MemoryStream, which turns an arbitrary block of memory into an IStream.

Adapting IStream

Another way to extend IStreams is to adapt them. By this we mean write a wrapper around them instead of subclass them. The strength of adapters is that they can be bound to a stream dynamically and thus impart their functionality onto any other stream, including other stream adapters. We discuss two types of adapaters here: class-based adapters and function-based adapters.

Here is an example of an adapter found in EAStreamChild. Note the code in blue, which is related to the StreamChild adapting another stream:

/// StreamChild
///
/// Implements a fixed-size read-only stream which is a 'child' of a parent
/// stream. This is useful if you have a system whereby a single large file
/// consists of smaller files or a single large database record consists of
/// multiple sub-records and you want each sub-record to look like a standalone
/// stream to the user.
 
class StreamChild : public IStream
{
public:
    enum { kTypeStreamChild = 0x3472233a };

    StreamChild(IStream* pStreamParent = NULL, size_type nPosition = 0, size_type nSize = 0);
   ~StreamChild();

    int       AddRef();
    int       Release();
    bool      Open(IStream* pStreamParent, size_type nPosition, size_type nSize);
    bool      Close();
    uint32_t  GetType() const { return kTypeStreamChild; }
    int       GetAccessFlags() const;
    int       GetState() const;
    size_type GetSize() const;
    bool      SetSize(size_type);
    off_type  GetPosition(PositionType positionType = kPositionTypeBegin) const;
    bool      SetPosition(off_type position, PositionType positionType = kPositionTypeBegin);

    size_type GetAvailable() const;
    size_type Read(void* pData, size_type nSize);

    bool      Flush();
    bool      Write(const void* pData, size_type nSize);

protected:
    int         mnRefCount;
    int         mnAccessFlags;
    IStream*    mpStreamParent;
    size_type   mnPositionParent;                
    size_type   mnPosition;
    size_type   mnSize;
};

Other class adapters provided by the UTF include:

Here is an example of some of the function adapters found in EAStreamAdapter:

// Read a uint32_t and converts it from big endian to whatever local endian is.
bool ReadUint32(IStream* pIS, uint32_t* value, size_type count, Endian endianSource = kEndianBig);
 
// Read a line of text and move the stream to the beginning of the next line.
size_type ReadLine(IStream* pIS, char16_t* pLine, size_type nMaxCount, Endian endianSource = kEndianBig);
 
// Read a uint64_t from a stream.
EA::IO::IStream& operator>>(EA::IO::IStream& s, uint64_t& val);

// Write a nul-terminated C string to a stream.
inline EA::IO::IStream& operator<<(EA::IO::IStream& s, const char16_t* str);

Example usage

Here we provide an example of writing to a stream.

bool SaveGame(const char16_t* pFilePath)
{
    bool result = false;
 
    AutoRefCount<FileStream> pStream = new FileStream(pFilePath);
 
    if(pStream->Open(kAccessFlagReadWrite, kCDOpenAlways))
    {
        result = gAI.Save(pStream);
 
        if(result)
            result = gSimulation.Save(pStream);
 
        if(result)
            result = gSceneManager.Save(pStream);
 
        pStream->Close();
    }
 
    return result;
}

Here we provide an example of reading from a stream.

#include <Foundation/EAStreamAdapter.h>
 
bool AI::Load(IStream* pStream)
{
    bool result = false;
  
    mDepth = ReadFloat(pStream);
    mWidth = ReadFloat(pStream);

    pStream->SetPosition(64, kPositionTypeCurrent); // Skip 64 bytes.
 
    mTimer = ReadUint32(pStream);
    mTaper = ReadUint16(pStream);

    return result;
}

Interface

Typedefs:

/// off_type
/// Used to denote data position offsets. This value must be signed.
/// We define this so that we can smoothly migrate to 64 bit data.
typedef <platform-specific> off_type;   
 
/// size_type
/// Used to denote data position values. This value must be unsigned. 
/// sizeof(off_type) should equal sizeof(size_type).
typedef <platform-specific> size_type;

/// kSizeTypeError
/// Used to designate an error condition for many functions that return size_type.
const size_type kSizeTypeError = (size_type)-1;

Enumerations:

/// enum AccessFlags
/// Defines stream access flags, much like file access flags.
enum AccessFlags
{
    kAccessFlagNone      = 0x00,  /// No specified flags. Also used to indicate that a given IO stream is closed.
    kAccessFlagRead      = 0x01,  /// Used for identifying read access to an entity.
    kAccessFlagWrite     = 0x02,  /// Used for identifying write access to an entity.
    kAccessFlagReadWrite = 0x03   /// Used for identifying both read and write access to an entity.
};
 
/// enum CD (creation disposition)
/// Specifies aspects of how to create or not create a file during opening of it.
enum CD
{
    kCDCreateNew        = 1,      /// Fails if file already exists.
    kCDCreateAlways     = 2,      /// Never fails, always opens or creates and truncates to 0.
    kCDOpenExisting     = 3,      /// Fails if file doesn't exist, keeps contents.
    kCDOpenAlways       = 4,      /// Never fails, creates if doesn't exist, keeps contents.
    kCDTruncateExisting = 5,      /// Fails if file doesn't exist, but truncates to 0 if it does.
    kCDDefault          = 6       /// Default (implementation-specific) disposition
};
 
/// enum PositionType
/// Defines the positional basis for a user GetPosition or SetPosition action.
enum PositionType
{
    kPositionTypeBegin   = 0,    /// For GetPosition refers to absolute index of next byte to read; always positive. For SetPosition, refers to absolute index of next byte to be written; always positive.
    kPositionTypeCurrent = 1,    /// For GetPosition always returns zero. For SetPosition, refers to position relative to current position; can be positive or negative.
    kPositionTypeEnd     = 2     /// For GetPosition returns to position relative to end (i.e. the negative of bytes left to read); always negative. For SetPosition, refers to position relative to end; can be positive or negative.
};
 
/// enum LineEnd
/// Defines textual line ending property types.
enum LineEnd
{
    kLineEndNone    = 0,     /// Refers to no line termination. When writing, it means to append nothing.
    kLineEndAuto    = 1,     /// Refers to automatic line termination. When writing, it means to append kLineTerminationNewline if there isn't one already.
    kLineEndNewline = 2,     /// Refers to "\n" line termination. When writing, it means to append a newline always.
    kLineEndUnix    = 2,     /// Same as kLineEndNewline.
    kLineEndWindows = 3      /// Refers to "\r\n" line termination. When writing, it means to append a newline always.
};
 
/// enum State
/// Defines state values or function return values. Zero means success and non-zero means failure in general.
/// Note that various stream types may define their own errors in addition to these generic errors.
enum State
{
    kStateSuccess =  0,
    kStateError   = -1,
    kStateNotOpen = -2
};
 
/// enum Endian
/// Defines endian-ness. This is appropriate for working with binary numerical data. 
enum Endian
{
    kEndianBig,     /// Big endian.
    kEndianLittle,  /// Little endian.
    kEndianLocal    /// Whatever endian is native to the machine.
};

IStream:

class IStream
{
public:
    /// ~IStream
    /// Virtual destructor. Having this destructor allows us to delete an IStream pointer
    /// without knowing its base class. Due to the C++ language design, you cannot have
    /// a pure virtual destructor but instead must implement it, at the least with { }.
    virtual ~IStream();

    /// AddRef
    /// Returns the new reference count. Implementations of the IStream interface are not 
    /// normally required to make their AddRef implementation thread-safe, as the sharing 
    /// of streams between threads is something that must be coordinated at a higher level.
    ///
    /// It often happens that users of streams would like to reference-count them so that
    /// streams can be handed off from a creator to an owner or can be shared between owners.
    ///
    /// Note that the use of reference counting by a system is optional and is determined 
    /// by the system and ideally documented as such by the system or the stream creator.
    /// A call to AddRef commits a system to using AddRef/Release to maintain the lifetime
    /// of the object thereafter.
    virtual int AddRef() = 0;

    /// Release
    /// Returns the new reference count, may be 0, in which case the object has been destroyed.
    /// Every call to AddRef must be matched by a call to Release.
    virtual int Release() = 0;

    /// GetType
    /// Returns the type of the stream, which is different for each Stream subclass.
    /// This function can be used for run-time type identification. A type of zero means
    /// the type is unknown or invalid.
    virtual uint32_t GetType() const = 0;

    /// AccessFlags
    /// Returns one of enum AccessFlags.
    /// This function also tells you if the stream is open, as a return value 
    /// of zero means the stream is not open. It is not allowed that a stream  
    /// be open with no type of access.
    virtual int GetAccessFlags() const = 0;

    /// GetState
    /// Returns the error state of the stream.
    /// Returns kStateSuccess if OK, else an error code.
    /// This function is similar to 'errno' or a 'get last error' facility.
    virtual int GetState() const = 0;

    /// Close
    /// Closes the stream and releases resouces associated with it.
    /// Returns true upon success, else false.
    /// If the return value is false, GetState will give the error code.
    /// If an IStream encounters an error during operations on an open
    /// stream, it is guaranteed that you can safely call the Close function 
    /// on the stream.
    virtual bool Close() = 0;

    /// GetSize
    /// Returns the size of the stream, which is not the same as the size
    /// of bytes remaining to be read from the stream. Returns (size_type)-2 
    /// if the stream is of indeterminate size. Returns (size_type)-1 
    /// (a.k.a. kSizeTypeError) upon error.
    virtual size_type GetSize() const = 0;

    /// SetSize
    /// Sets the size of the stream, if possible. It is debatable whether this
    /// function should be present in IStream or only in suclasses of 
    /// StreamBase which are writable. For consistency with GetSize, we put 
    /// the function here. But also consider that a SetSize function is not 
    /// necessarily a data writing function, depending on the stream implementation.
    virtual bool SetSize(size_type size) = 0;

    /// GetPosition
    /// Gets the current read/write position within the stream. The read and 
    /// write positions of a stream must be the same value; you cannot have a
    /// read position that is different from a write position. However, a 
    /// Stream subclass can provide such functionality if needed. 
    /// Returns (size_type)-1 (a.k.a. kSizeTypeError) upon error.
    virtual off_type GetPosition(PositionType positionType = kPositionTypeBegin) const = 0;

    /// SetPosition
    /// Sets the read/write position of the stream. If the specified position is 
    /// beyond the size of a fixed stream, the position is set to the end of the stream.
    /// A writable stream subclass may provide a policy whereby setting the position
    /// beyond the end of the stream results in an increase in the stream size.
    virtual bool SetPosition(off_type position, PositionType positionType = kPositionTypeBegin) = 0;

    /// GetAvailable
    /// Returns the number of bytes available for reading.
    /// Returns (size_type)-1 (a.k.a. kSizeTypeError) upon error.
    /// This function is non-blocking; it should return immediately.
    virtual size_type GetAvailable() const = 0;

    /// Read
    /// Reads bytes from the stream given by the input count 'nSize'.
    /// If less then nSize bytes are available, then those bytes will be read.
    /// Returns the number of bytes read. A return value of zero means that there
    /// were no bytes to be read or no bytes were requested to be read. 
    /// Returns (size_type)-1 (a.k.a. kSizeTypeError) if there was an error. 
    /// You can use this return value or IStream::GetState to determine the error.
    /// Input size values equal to (size_type)-1 (a.k.a. kSizeTypeError) are not valid input.
    /// Upon error, the stream pointer is at the position it was upon the error occurrence.
    virtual size_type Read(void* pData, size_type nSize) = 0;

    /// Flush
    /// Flush any non-empty stream write buffers. 
    /// If the return value is false, GetState will give the error code.
    /// This function implements the flushing as per the underlying file system.
    /// The behavior of the Flush function varies with the underlying platform.
    ///
    /// A common use of Flush is write a file to disk immediately in order to prevent
    /// the file from being corrupted if the application crashes before the file
    /// is closed. However, on desktop platforms such as Windows this strategy is
    /// unnecesary, as the Windows OS file flush doesn't write the file to disk as 
    /// might be expected. This actually is not a problem, because the Windows OS
    /// manages files outside the process and if your process crashes the OS will 
    /// take care of safely closing the files. Only if the machine power is lost or
    /// if certain kinds of kernel-level crashes occur may you lose file data.            
    virtual bool Flush() = 0;

    /// Write
    /// Writes bytes to the stream. 
    /// If false is returned, you can use IStream::GetState to determine the error.
    /// Upon error, the stream pointer is at the position it was upon the error occurrence.
    virtual bool Write(const void* pData, size_type nSize) = 0;