File I/OThis 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;
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;
Block Identifiers The LWBlockIdent structure is used to label blocks. typedef struct st_LWBlockIdent { LWID id; const char *token; } LWBlockIdent;
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; } |