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 theioMode
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'sopenLoad
function, theioMode
matches the one passed toopenLoad
. This global supports a thirdioMode
,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 ton
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 thelwtypes.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 theioMode
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'sopenSave
function, theioMode
matches the one passed toopenSave
. This global supports a thirdioMode
,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, thebuf
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. Theleaf
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 #define
s
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; }