File I/O

This page describes the mechanism LightWave provides to move plug-in data into and out of files. The file I/O functions are used by handlers in their load and save callbacks to store and retrieve instance data in scene and object files. These functions can also be used by any plug-in class to read and write files accessed through the File I/O global.

Loading

Data is loaded from files using the functions in an LWLoadState. The lwio.h header file also defines macros for most of these functions. Both the functions and the corresponding macros are listed in the definitions.

   typedef struct st_LWLoadState {
      int   ioMode;
      void *readData;
      int  (*read)    (void *data, char  *, int len);
      int  (*readI1)  (void *data, char  *, int n);
      int  (*readI2)  (void *data, short *, int n);
      int  (*readI4)  (void *data, long  *, int n);
      int  (*readU1)  (void *data, unsigned char  *, int n);
      int  (*readU2)  (void *data, unsigned short *, int n);
      int  (*readU4)  (void *data, unsigned long  *, int n);
      int  (*readFP)  (void *data, float *, int n);
      int  (*readStr) (void *data, char  *, int max);
      LWID (*findBlk) (void *data, const LWBlockIdent *);
      void (*endBlk)  (void *data);
      int  (*depth)   (void *data);
   } LWLoadState;
ioMode
Indicates whether the file contents will be interpreted as binary (LWIO_BINARY) or text (LWIO_ASCII). Handler plug-ins that receive the LWLoadState in their instance load function can usually infer from the ioMode whether they're reading their data from a scene file or an object file. If the LWLoadState is created by the File I/O global's openLoad function, the ioMode matches the one passed to openLoad. This global supports a third ioMode, LWIO_BINARY_IFF.

In ASCII mode, all of the read functions are line-buffered, meaning that they won't wrap around to the next line when reading an array of values. If you ask for six numbers and the current line contains only five, the read functions will return five values rather than trying to get the sixth from the following line.

readData
An opaque pointer to data used by the LWLoadState functions. Pass this as the first argument to these functions.
rn = read( readData, buf, n )
Read raw bytes. In binary mode, n bytes are read directly from the file. In ASCII mode, up to n bytes of the current line are read from the file, possibly leaving more bytes to be read later (the file pointer isn't moved to the next line until all of the current line is read). The return value is the number of bytes actually read (which may be zero in ASCII mode if the current line is empty), or -1 for end of data.
rn = readI1( readData, bytebuf, n )
rn = LWLOAD_I1( ls, bytebuf, n )
Read an array of bytes. These are interpreted as numbers rather than text characters. Returns the number of bytes read.
rn = readI2( readData, shortbuf, n )
rn = LWLOAD_I2( ls, shortbuf, n )
Read an array of two-byte integers. Returns the number of short integers read.
rn = readI4( readData, longbuf, n )
rn = LWLOAD_I4( ls, longbuf, n )
Read an array of four-byte integers. Returns the number of integers read.
rn = readU1( readData, ubytebuf, n )
rn = LWLOAD_U1( ls, ubytebuf, n )
Read an array of unsigned bytes. These are interpreted as numbers rather than text characters. Returns the number of bytes read.
rn = readU2( readData, ushortbuf, n )
rn = LWLOAD_U2( ls, ushortbuf, n )
Read an array of unsigned two-byte integers. Returns the number of short integers read.
rn = readU4( readData, ulongbuf, n )
rn = LWLOAD_U4( ls, ulongbuf, n )
Read an array of unsigned four-byte integers. Returns the number of integers read.
rn = readFP( readData, floatbuf, n )
rn = LWLOAD_FP( ls, floatbuf, n )
Read an array of floating point numbers. Returns the number of floats read.
len = readStr( readData, strbuf, maxn )
len = LWLOAD_STR( ls, strbuf, maxn )
Read a string. Double quotes used to delimit the string in a text file are removed. Returns the length of the string.
id = findBlk( readData, idlist )
id = LWLOAD_FIND( ls, idlist )
Read ahead, looking for the next block. The ID list is a 0-terminated array of LWBlockIdent structures, and the function returns when it finds any one of the blocks in the list. In binary mode, a block is identified by a 4-byte integer constructed using the LWID_ macro defined in the lwtypes.h header file. In ASCII mode, the block ID is a string token. Returns 0 if no blocks in the list were found.
endBlk( readData )
LWLOAD_END( ls )
Move the file pointer to the end of the current block. Call this when you've finished reading a block.
d = depth( readData)
d = LWLOAD_DEPTH( ls )
Returns the current block nesting level, where 0 means we've entered no blocks.

Saving

Data is saved to files using the functions in an LWSaveState.

   typedef struct st_LWSaveState {
      int   ioMode;
      void *writeData;
      void (*write)    (void *data, const char  *, int len);
      void (*writeI1)  (void *data, const char  *, int n);
      void (*writeI2)  (void *data, const short *, int n);
      void (*writeI4)  (void *data, const long  *, int n);
      void (*writeU1)  (void *data, const unsigned char  *, int n);
      void (*writeU2)  (void *data, const unsigned short *, int n);
      void (*writeU4)  (void *data, const unsigned long  *, int n);
      void (*writeFP)  (void *data, const float *, int n);
      void (*writeStr) (void *data, const char  *);
      void (*beginBlk) (void *data, const LWBlockIdent *, int leaf);
      void (*endBlk)   (void *data);
      int  (*depth)    (void *data);
   } LWSaveState;
ioMode
Indicates whether the file contents will be interpreted as binary (LWIO_BINARY) or text (LWIO_ASCII). Handler plug-ins that receive the LWSaveState in their instance save function can usually infer from the ioMode whether they're writing their data to a scene file or an object file. If the LWSaveState is created by the File I/O global's openSave function, the ioMode matches the one passed to openSave. This global supports a third ioMode, LWIO_BINARY_IFF.

In ASCII mode, the write functions are line-buffered, meaning that each call to a write function results in a single newline-terminated line in the file.

writeData
An opaque pointer to data used by the LWSaveState functions. Pass this as the first argument to these functions.
write( writeData, buf, len )
Write raw bytes. In binary mode, len bytes are written directly to the file. In ASCII mode, the buf argument is assumed to be a null-terminated string and the length is computed from that. This string is written with a newline at the end.
writeI1( writeData, bytebuf, n )
LWSAVE_I1( ss, bytebuf, n )
Write an array of bytes. The values are treated as numbers rather than text characters.
writeI2( writeData, shortbuf, n )
LWSAVE_I2( ss, shortbuf, n )
Write an array of two-byte integers. In ASCII mode, these n numbers are all written to a single line. A newline is written after the numbers unless you're currently inside a leaf block.
writeI4( writeData, longbuf, n )
LWSAVE_I4( ss, longbuf, n )
Write an array of four-byte integers.
writeU1( writeData, ubytebuf, n )
LWSAVE_U1( ss, ubytebuf, n )
Write an array of unsigned bytes. The values are treated as numbers rather than text characters. In text files, each byte is written as a pair of hexadecimal digits.
writeU2( writeData, ushortbuf, n )
LWSAVE_U2( ss, ushortbuf, n )
Write an array of unsigned two-byte integers. In text files, the values are written in hex.
writeU4( writeData, ulongbuf, n )
LWSAVE_U4( ss, ulongbuf, n )
Write an array of unsigned four-byte integers. In text files, the values are written in hex.
writeFP( writeData, floatbuf, n )
LWSAVE_FP( ss, floatbuf, n )
Write an array of floats.
writeStr( writeData, strbuf )
LWSAVE_STR( ss, strbuf )
Write a string. In ASCII mode, the string may be contained in double quote marks (which will be removed when the string is later read by the LWLoadState readStr function).
beginBlk( writeData, blockid, leaf )
LWSAVE_BEGIN( ss, blockid, leaf )
Create a new block. blockid is an LWBlockIdent that will be used to label the block. The leaf flag is true if this block will not contain sub-blocks.
endBlk( writeData )
LWSAVE_END( ss )
End the current block.
d = depth( writeData )
d = LWSAVE_DEPTH( ss )
Return the current block nesting level, where zero means you've entered no blocks.

Block Identifiers

The LWBlockIdent structure is used to label blocks.

   typedef struct st_LWBlockIdent {
      LWID        id;
      const char *token;
   } LWBlockIdent;
id
A four-byte code usually built by the LWID_ macro defined in lwtypes.h. Used when writing to binary files. This is also the value returned by findBlk for both binary and ASCII files, which makes it useful as the descriminator in a case statement.
token
A plain text label used when writing to ASCII files. This string should contain no spaces.

When creating custom files for your own use, you may use any ID and label you like. Their only purpose is to identify the data that follows them when you read the file back in.

Example

Most of the file I/O functions are straightforward, so this example code concentrates on the use of the block functions to write and read block-structured data.

LightWave scene files use blocks to create keyword-value pairs and to delimit keyframe data. Blocks also appear as the subchunks in each SURF chunk of an object file. Block structure makes the data self-documenting and more human-readable. It also makes your file format extensible without sacrificing backward compatibility. Older readers will automatically skip blocks they don't recognize and can find blocks even if they've been written in a different order.

We'll create a data structure well suited to blocky storage. This structure is borrowed from an astronomy application, where it describes the circumstances of an observer.

   #include <lwserver.h>
   #include <lwio.h>

   typedef struct {
      float    timezone;
      char     tzname[ 4 ];
      int      ltim[ 6 ];          /* yr mon day hr min sec */
      float    location[ 2 ];      /* lat lon */
      int      horizon_type;
      float    temperature;
      float    pressure;
      float    elevation;
      float    epoch;
   } Observer;

We need labels for each of the blocks. These will be used for both saving and loading. The ID arrays are divided into root blocks in the first and subblocks of the horizon block in the second, which is what we'll need when we read this data back in. The #defines may seem like an extra step now, but they'll come in handy later.

   #define ID_TZON  LWID_( 'T','Z','O','N' )
   #define ID_TZNM  LWID_( 'T','Z','N','M' )
   #define ID_LTIM  LWID_( 'L','T','I','M' )
   #define ID_LOCA  LWID_( 'L','O','C','A' )
   #define ID_EPOC  LWID_( 'E','P','O','C' )
   #define ID_HRZN  LWID_( 'H','R','Z','N' )
   #define ID_TYPE  LWID_( 'T','Y','P','E' )
   #define ID_TEMP  LWID_( 'T','E','M','P' )
   #define ID_PRES  LWID_( 'P','R','E','S' )
   #define ID_ELEV  LWID_( 'E','L','E','V' )

   static LWBlockIdent idroot[] = {
      ID_TZON, "TimeZone",
      ID_TZNM, "TimeZoneName",
      ID_LTIM, "LocalTime",
      ID_LOCA, "Location",
      ID_EPOC, "Epoch",
      ID_HRZN, "Horizon",
      0
   };

   static LWBlockIdent idhrzn[] = {
      ID_TYPE, "Type",
      ID_TEMP, "Temperature",
      ID_PRES, "Pressure",
      ID_ELEV, "Elevation",
      0
   };

This is the save function. Note that it doesn't care whether the LWSaveState's ioMode is LWIO_ASCII or LWIO_BINARY. It also doesn't care whether the LWSaveState came from a handler's save callback or from the file I/O global's openSave function.

   int write_obs( LWSaveState *save, Observer *obs )
   {
      LWSAVE_BEGIN( save, &idroot[ 0 ], 1 );
         LWSAVE_FP( save, &obs->timezone, 1 );
      LWSAVE_END( save );

      LWSAVE_BEGIN( save, &idroot[ 1 ], 1 );
         LWSAVE_STR( save, obs->tzname );
      LWSAVE_END( save );

      LWSAVE_BEGIN( save, &idroot[ 2 ], 1 );
         LWSAVE_I4( save, obs->ltim, 6 );
      LWSAVE_END( save );

      LWSAVE_BEGIN( save, &idroot[ 3 ], 1 );
         LWSAVE_FP( save, obs->location, 2 );
      LWSAVE_END( save );

      LWSAVE_BEGIN( save, &idroot[ 4 ], 1 );
         LWSAVE_FP( save, &obs->epoch, 1 );
      LWSAVE_END( save );

      LWSAVE_BEGIN( save, &idroot[ 5 ], 0 );

         LWSAVE_BEGIN( save, &idhrzn[ 0 ], 1 );
            LWSAVE_I4( save, &obs->horizon_type, 1 );
         LWSAVE_END( save );

         LWSAVE_BEGIN( save, &idhrzn[ 1 ], 1 );
            LWSAVE_FP( save, &obs->temperature, 1 );
         LWSAVE_END( save );

         LWSAVE_BEGIN( save, &idhrzn[ 2 ], 1 );
            LWSAVE_FP( save, &obs->pressure, 1 );
         LWSAVE_END( save );

         LWSAVE_BEGIN( save, &idhrzn[ 3 ], 1 );
            LWSAVE_FP(  save, &obs->elevation, 1 );
         LWSAVE_END( save );

      LWSAVE_END( save );

      return 1;
   }

If the ioMode is LWIO_ASCII, the output of the write_obs function looks like this.

   TimeZone 4
   TimeZoneName "EDT"
   LocalTime 2000 4 24 2 5 30
   Location 37.75 -122.55
   Epoch 2000
   { Horizon
     Type 1
     Temperature 40
     Pressure 30
     Elevation 100
   }

Each leaf block is a single line containing a keyword (the LWBlockIdent token) and a list of values. Non-leaf blocks are delimited by curly brackets and indented to show the block nesting level.

A hex dump of the same data written to a binary file would look like the following. Each block begins with the 4-byte ID and a 2-byte size field. All of the numbers are in big-endian (Internet, Motorola) byte order.

   54 5A 4F 4E 00 04   TZON 4
   40 80 00 00           4.0
   54 5A 4E 4D 00 04   TZNM 4
   45 44 54 00           "EDT"
   4C 54 49 4D 00 18   LTIM 24
   00 00 07 D0           2000
   00 00 00 04           4
   00 00 00 18           24
   00 00 00 02           2
   00 00 00 05           5
   00 00 00 1E           30
   4C 4F 43 41 00 08   LOCA 8
   42 17 00 00           37.75
   C2 F5 19 9A           -122.55
   45 50 4F 43 00 04   EPOC 4
   44 FA 00 00           2000.0
   48 52 5A 4E 00 28   HRZN 40
   54 59 50 45 00 04     TYPE 4
   00 00 00 01             1
   54 45 4D 50 00 04     TEMP 4
   42 20 00 00             40.0
   50 52 45 53 00 04     PRES 4
   41 F0 00 00             30.0
   45 4C 45 56 00 04     ELEV 4
   42 C8 00 00             100.0

The function to read this data just searches for blocks in a loop and switches to load each one. The outer while loop reads root blocks, and the inner loop reads horizon blocks when the HRZN root block is found.

   int read_obs( LWLoadState *load, Observer *obs )
   {
      LWID id;

      while ( id = LWLOAD_FIND( load, idroot )) {
         switch ( id ) {
            case ID_TZON:
               LWLOAD_FP( load, &obs->timezone, 1 );
               break;
            case ID_TZNM:
               LWLOAD_STR( load, obs->tzname, 4 );
               break;
            case ID_LTIM:
               LWLOAD_I4( load, obs->ltim, 6 );
               break;
            case ID_LOCA:
               LWLOAD_FP( load, obs->location, 2 );
               break;
            case ID_EPOC:
               LWLOAD_FP( load, &obs->epoch, 1 );
               break;
            case ID_HRZN:
               while ( id = LWLOAD_FIND( load, idhrzn )) {
                  switch ( id ) {
                     case ID_TYPE:
                        LWLOAD_I4( load, &obs->horizon_type, 1 );
                        break;
                     case ID_TEMP:
                        LWLOAD_FP( load, &obs->temperature, 1 );
                        break;
                     case ID_PRES:
                        LWLOAD_FP( load, &obs->pressure, 1 );
                        break;
                     case ID_ELEV:
                        LWLOAD_FP( load, &obs->elevation, 1 );
                        break;
                  }
                  LWLOAD_END( load );
               }
               break;            
         }
         LWLOAD_END( load );
      }

      return 1;
   }