home *** CD-ROM | disk | FTP | other *** search
/ 17 Bit Software 1: Collection A / 17Bit_Collection_A.iso / files / 61.dms / 61.adf / doc.files / backgrnd.doc next >
Text File  |  1986-03-21  |  18KB  |  431 lines

  1.                 BACKGROUND ON THE EXAMPLE IFF SOURCE CODE
  2.  
  3. Jerry Morrison, 1/30/86
  4.  
  5. The example IFF code is written using a programming style and techniques
  6. that may be unfamiliar to you. So here's a tutorial on "call-back
  7. procedures","enumerators", "interfaces", and "sub-classed structures". I
  8. recommend these programming practices independently of IFF software.
  9.  
  10.  
  11. DEFINITIONS: "CLIENT" VS. "USER"
  12.  
  13. First, some definitions. The word "user" is reserved for a human user of a
  14. software package. That's you and me.
  15.  
  16. A "client" of a software package, on the other hand, is a piece of software
  17. that uses that software package. A program that calls operating system
  18. routines such as "OpenFile" is a client of that operating system.
  19.  
  20.  
  21. CALL-BACK PROCEDURES
  22.  
  23. Consider an operating system subroutine "ListDir" that lists the files in a
  24. disk directory. It might allow you to list just the filenames matching a
  25. pattern like "a*.text". Maybe you can ask it to list just the files created
  26. since yesterday ... or those longer than 2000 bytes. ListDir is a fancy,
  27. general-purpose directory subroutine that lets you pass in a number of
  28. arguments to filter the listing.
  29.  
  30. A C definition might look like:
  31.  
  32.    void ListDir(directory, namePattern, minSize, maxSize, minDate ...); ... {
  33.       for (each file in the directory)
  34.          if ( PatternMatch(namePattern, filename)
  35.                &&  fileSize >= minSize
  36.                &&  fileSize <= maxSize
  37.                &&  fileDate >= minDate
  38.                && ... )
  39.             printf("%s\n", filename);  /* probably fancier than this... */
  40.       }
  41.  
  42. and your call to it:
  43.  
  44.    ListDir(myDir, "a*.text", 0, maxFileSize, date1_1_1900, ...);
  45.  
  46. When you think about it, these filtering arguments make up a
  47. special-purpose "file filtering language". The person who designed this
  48. subroutine "ListDir" might be pretty pleased with his accomplishment. But
  49. in practice he can never put in enough features into this special-purpose
  50. language to satisfy everyone. (You say you need to list just the files
  51. currently open?) And he may have provided a lot of functionality that is
  52. rarely needed. Is this filtering language what he should spending his time
  53. designing, writing, and debugging?
  54.  
  55. A much better technique is to use a "call-back procedure". The concept is
  56. simple: instead of all those filter arguments to ListDir, you pass it a
  57. pointer to a "filter procedure". ListDir simply calls your procedure (via the
  58. pointer) to do the filtering, once per file. It passes each filename to your
  59. "filter proc", which returns "TRUE" to include that file in the listing or
  60. "FALSE" to skip it.
  61.  
  62.    typedef BOOL FilterProc();  /* FilterProc: a BOOL procedure */
  63.  
  64.    void ListDir(directory, filterProc);
  65.          Directory directory;  FilterProc *filterProc;  {
  66.       for (each file in the directory)
  67.          if ( (*filterProc)(filename) )  printf("%s\n", filename);
  68.       }
  69.  
  70. and your code:
  71.  
  72.    BOOL MyFilterProc(filename)  STRING filename;  {
  73.       return(PatternMatch("a*.text", filename));
  74.       }
  75.  
  76.       ...
  77.       ListDir(myDir, MyFilterProc);
  78.  
  79. This technique has many advantages. It gives unlimited flexibility to
  80. ListProc. It means you can use a general-purpose programming language
  81. instead of learning a special-purpose filtering language. It's more
  82. efficient to call a compiled subroutine than to "interpret" the filtering
  83. parameters. And it means you can do anything you want in a filter proc,
  84. from selecting files on the basis of numerology to copying files to backup
  85. tape.
  86.  
  87. In practice, ListDir would have data about each file readily available. So it
  88. should pass this data to the filter proc to save time.
  89.  
  90.  
  91. As Alan Kay once said, "Simple things should be simple and complex things
  92. should be possible."
  93.  
  94.  
  95. STANDARD CALL-BACK PROCEDURE
  96.  
  97. I could extend ListDir to accept a NULL FilterProc pointer to mean "list all
  98. files". More likely, I'd supply a standard call-back procedure "FilterTRUE"
  99. that always returns TRUE. Then ListDir(directory, FilterTRUE) will list all
  100. files with no special test for filterProc == NULL.
  101.  
  102.    BOOL FilterTRUE(filename)  STRING filename;  {
  103.       return(TRUE);
  104.       }
  105.  
  106.  
  107. ENUMERATORS
  108.  
  109. Let's take our ListDir example one step further. Rather than have ListDir
  110. print the selected filenames, have it JUST call your custom proc for every
  111. file. Let your custom proc print the filenames, maybe in your own personal
  112. format. Or maybe have it quietly backup new files, or ask the user which
  113. ones to delete, or ...
  114.  
  115.    typedef CallBackProc(/* filename */);
  116.  
  117.    void ListDir(directory, callBackProc);
  118.          Directory directory;  CallBackProc *callBackProc;  {
  119.       for (each file in the directory)
  120.          (*callBackProc)(filename);
  121.       }
  122.  
  123. and your code:
  124.  
  125.    void MyProc(filename)  STRING filename;  {
  126.       if ( PatternMatch("a*.text", filename) )
  127.          printf("%s\n", filename);
  128.       }
  129.  
  130.       ...
  131.       ListDir(myDir, MyProc);
  132.  
  133. Now we're talking about a full-blown "enumerator". The procedure "ListDir"
  134. is said to "enumerate" all the files in a directory. It "applies" your
  135. call-back procedure to each file. The enumerator scans the directory and
  136. your call-back procedure processes the files. It deals with the internal
  137. directory details and you deal with the printout. A nice separation of
  138. concerns.
  139.  
  140.  
  141. ListDir should come with a standard call-back procedure "PrintFilename"
  142. that lists the filename. By simply passing PrintFilename to ListDir, you
  143. can print a directory. By writing a call-back procedure that selectively
  144. calls the PrintFilename, you can filter the listing.
  145.  
  146.    void PrintFilename(filename)  STRING filename;  {
  147.       printf("%s\n", filename);
  148.       }
  149.  
  150.  
  151. ENUMERATION CONTROL
  152.  
  153. A simple enhancement is to empower the call-back procedure to stop the
  154. enumeration early. That's easy. Have it return "TRUE" to stop. This is very
  155. handy, for example, to quit when you find what you're looking for. Let's
  156. expand this boolean "continue/stop" result into an integer error code.
  157.  
  158.    #define OKAY   0
  159.    #define DONE  -1
  160.    typedef int CallBackProc(/* filename */);
  161.  
  162.    int ListDir(directory, callBackProc);
  163.          Directory directory;  CallBackProc *callBackProc;  {
  164.       int result = OKAY;
  165.       for (each file in the directory) while (result == OKAY)
  166.          result = (*callBackProc)(filename);
  167.       return(result);
  168.       }
  169.  
  170.  
  171. IFF FILE ENUMERATOR
  172.  
  173. Now we'll relate these techniques to the example IFF code. I'm assuming
  174. that you've read "EA IFF 85" Standard for Interchange Format Files. That
  175. memo is available from Commodore as part of their Amiga documentation.
  176. Also ask Commodore for "ILBM" IFF Interleaved Bitmap and the example IFF
  177. source code.
  178.  
  179. Two things make IFF files very flexible for lots of interchange between
  180. programs. First, file formats are independent of RAM formats. That means
  181. you have to do some conversion when you read and write IFF files. Second,
  182. the contents are stored in chunks according to global rules. That means you
  183. have to parse the file, i.e. scan it and react to what's actually there.
  184.  
  185. In the example IFF files IFF.H and IFFR.C, the routines ReadIFF, ReadIList, &
  186. ReadICat are enumeration procedures. ReadIFF scans an IFF file,
  187. enumerating all the "FORM", "LIST", "PROP", and "CAT" chunks encountered.
  188. ReadIList & ReadICat enumerate all the chunks in a LIST and CAT,
  189. respectively.
  190.  
  191. A ClientFrame record is a bundle of pointers to 4 "call-back procedures"
  192. getList, getProp, getForm, and getCat. These 4 procedures are called by
  193. ReadIFF, ReadIList, and ReadICat when the 4 kinds of IFF "groups" are
  194. encountered: "LIST", "PROP", "FORM", or "CAT".
  195.  
  196. These 3 enumerator procedures and 4 client procedures together make up a
  197. reader for IFF files--a very simple recursive descent parser. If you want
  198. to learn more about parsing, a real good place to look is the new edition
  199. "dragon book" by Aho, Ullman, and Sethi.
  200.  
  201. The procedure "SkipGroup" is just a default call-back procedure.
  202.  
  203. The "IFFP" values IFF_OKAY through BAD_IFF are the error codes used by
  204. the IFF enumerators. We use the type "IFFP" to declare variables (and
  205. procedure results) that hold such values. The code "IFF_OKAY" means "AOK;
  206. keep enumerating". The other values mean "stop" for one reason or other.
  207. "IFF_DONE" means "we're all done", while "END_MARK" means "we hit the
  208. end at this nesting level".
  209.  
  210.  
  211. CALL-BACK PROCEDURE STATE
  212.  
  213. ListDir is an enumerator with some internal state--it internally
  214. remembers its place in the directory. It loops over the directory, calling
  215. the client proc once per file. That's fine for some cases and less
  216. convenient for others. Consider this example that just lists the first 10
  217. files:
  218.  
  219.    int count;
  220.  
  221.    int PrintFirst10(filename)  STRING filename;  {
  222.       if (++count > 10)  return(DONE);
  223.       printf("%s\n", filename);
  224.       return(OKAY);
  225.       }
  226.  
  227.    void DoIt();  {
  228.       ...
  229.       count = 0;
  230.       ListDir(myDir, PrintFirst10);
  231.       ...
  232.       }
  233.  
  234. Inherently, the client's code has to be split into code that calls the
  235. enumerator and a call-back procedure. Thus any communication between
  236. the two must be via global variables. In this trivial example, the global
  237. "count" saves state data between calls to PrintFirst10. Often, it's much
  238. more complex. But globals won't work if you need reenterent or recursive
  239. code. We really want "count" to be a local variable of DoIt.
  240.  
  241. Fixing this in Pascal is easy: Define PrintFirst10 as a nested procedure
  242. within DoIt so it can access DoIt's local variables. The manual analog in C
  243. is to redefine the enumerator to pass a raw "client data pointer" straight
  244. through to the call-back procedure. The two client procedures then
  245. communicate through the "client data pointer". DoIt would call
  246. ListDir(myDir, PrintFirst10, &count) which calls PrintFirst10(filename,
  247. &count).
  248.  
  249.    #define OKAY   0
  250.    #define DONE  -1
  251.    typedef int CallBackProc(/* filename, clientData */);
  252.  
  253.    int ListDir(directory, callBackProc, clientData);
  254.          Directory directory; CallBackProc *callBackProc; BYTE *clientData; {
  255.       int result = OKAY;
  256.       for (each file in the directory) while (result == OKAY)
  257.          result = (*callBackProc)(filename, clientData);
  258.       return(result);
  259.       }
  260.  
  261.  
  262. In general, an enumerator is sometimes inconvenient because it takes over
  263. control. Think about this: How could you enumerate two directories in
  264. parallel and copy the newer files from one directory to the other?
  265.  
  266.  
  267. STATELESS ENUMERATOR
  268.  
  269. An alternate form without this disadvantage is the "stateless enumerator".
  270.  
  271. In a stateless enumerator, it's up to the client to keep its place in the
  272. enumeration. Call a procedure like GetNextFilename each time around the
  273. loop.
  274.  
  275.       STRING curFilename = NULL;
  276.       int count = 0;
  277.       do {
  278.          if (++count > 10)  break;  /* stop after 10 files */
  279.          curFilename = GetNextFilename(directory, curFilename);
  280.          if (curFilename == NULL)  break;  /* stop at end of directory */
  281.          printf("%s\n", filename);
  282.          }
  283.  
  284. The stateless enumerator is sometimes better because it puts the client
  285. in control. The above example shows how easy it is to keep state
  286. information between iterations and to stop the enumeration easy. It's also
  287. easy to do things like list two directories in parallel.
  288.  
  289.  
  290. IFF CHUNK ENUMERATOR
  291.  
  292. The following IFFR.C routines make up a stateless IFF chunk enumerator:
  293. OpenRIFF, OpenRGroup, GetChunkHdr and CloseRGroup. Together with
  294. IFFReadBytes, we have a complete layer of "chunk reader" subroutines.
  295. These subroutines are built upon the file stream package in the local
  296. system library.
  297.  
  298. GetChunkHdr is the "get next" procedure you call to get the next IFF chunk.
  299. (GetFChunkHdr, GetF1ChunkHdr, and GetPChunkHdr are subroutines that call
  300. GetChunkHdr and do a little extra work.) OpenRIFF and OpenRGroup do the
  301. initialization needed before you can call GetChunkHdr. CloseRGroup does
  302. the cleanup work.
  303.  
  304. You supply a "GroupContext" pointer each time you call one of these "chunk
  305. reader" procedures. The enumeration state is kept in a GroupContext record
  306. which the *client* must allocate but the *enumerator* routines initialize
  307. and maintain. (The client may peek into a GroupContext but should never
  308. modify it directly.) The two procedures OpenRIFF and OpenRGroup initialize
  309. the GroupContext record. This "opens a context" for reading chunks. The
  310. procedure CloseRGroup cleans up when you're done with a GroupContext.
  311.  
  312. Here's the essense of an IFF scanner program. It handles whatever it finds,
  313. unlike inflexible file readers that demand conformance to a rigid file
  314. format. [Note: This code doesn't check for errors or end-of-context.]
  315.  
  316.    OpenRGroup(..., context);  /* initialize */
  317.    do {
  318.       id = GetChunkHdr(context);  /* get the next chunk's ID */
  319.       switch (id)  {
  320.          case AAAA: {read in an AAAA chunk; break};
  321.          case BBBB: {read in a BBBB chunk; break};
  322.          ...
  323.          default: {};  /* just ignore unrecognized chunks */
  324.          }
  325.    CloseRGroup(context);  /* cleanup */
  326.  
  327. GetChunkHdr reads the next chunk header and returns its chunk ID. You then
  328. dispatch on the chunk ID, that is, switch to a different piece of code for
  329. each type of chunk. If you don't recognize the chunk ID, just keep looping.
  330.  
  331. In each "case:" statement, call IFFReadBytes one or more times to read the
  332. chunk's contents. The readin work you do here depends on the chunk type
  333. and what you need in RAM. Since GetChunkHdr automatically skips to the
  334. start of the next chunk, it doesn't matter if you don't read all the data
  335. bytes.
  336.  
  337. GetChunkHdr does some other things for you automatically. When it reads a
  338. "group" chunk header (a chunk of type "FORM", "LIST", "CAT ", or "PROP") it
  339. automatically reads the subtype ID. That makes it very convenient to just
  340. open the contents of the group chunk as a group context and read the
  341. nested chunks. See the example source program ShowILBM for more about
  342. the relationship between a "GroupContext" and a "ClientFrame".
  343.  
  344. Like all the example IFF code, GetChunkHdr checks for errors. To handle
  345. GetChunkHdr errors, we just add cases to the switch statment. To stop at
  346. end-of-context or an error in a switch case, we add a "while" clause at the
  347. end of the "do" statement.
  348.  
  349.  
  350. CLIENTS, INTERFACES, AND IMPLEMENTORS
  351.  
  352. In the ListDir example, you can see that a lot of flexibility comes from
  353. decoupling the task of tracing through the directory's data structures from
  354. the task of filtering files and printing filenames. This is called
  355. modularity, or simply, dividing a program into parts.
  356.  
  357. Choosing good module boundaries is an art. It has a big impact on a
  358. programmer's ability to coope with lrge programs. Good modularity makes
  359. programs much easier to understand and modify. But this topic would be
  360. another whole tutorial in itself.
  361.  
  362. Just be aware that the example IFF program is divided into various
  363. "modules", each of which implements a different part of the bigger picture.
  364. One such module is the low level IFF reader/writer. It's split into two
  365. files IFFR.C and IFFW.C. Other such modules are the run encoder/decoder
  366. Packer.C and UnPacker.C, and ILBM read/write subroutines ILBMR.C and
  367. ILBMW.C.
  368.  
  369. You'll notice that all three of these "modules" are split into a pair of files.
  370. That's because most linkers aren't fancy enough to automatically eliminate
  371. unused subroutines, e.g. for a program like ShowILBM that reads but doesn't
  372. need the writer code. Also, a program like DeluxePaint wants read and
  373. write code in separate overlays. So think of each pair as a single module.
  374.  
  375. What I want to point out is the basic structure. Each "module" has an
  376. "interface" file (a .H file) that separates the "implementor" .C file(s) from
  377. the "client" programs. This interface is very important, in fact, more
  378. important than the code details inside the .C files. The interfaces for the
  379. above-mentioned modules are called IFF.H, Packer.H, and ILBM.H.
  380.  
  381. Everything about a layer of software that the clients need to know belongs
  382. in its interface: constant and type definitions, extern declarations for the
  383. procedures, and comments. The comments detail the purpose of the module
  384. and each procedure, the procedure arguments, side effects, results, and
  385. error codes, etc. Nothing the clients don't need to know belongs in its
  386. interface: internal implementation details that might change.
  387.  
  388. Thus, the modularization and other important design information is
  389. collected and documented in these interface files. So if you want to
  390. understand what a module does and how to use it, READ ITS INTERFACE.
  391. Don't dive headfirst into the implementation. 
  392.  
  393. Two of the original articles on modular programming are
  394.    D.L. Parnas, "On the Criteria To Be Used in Decomposing Systems into
  395. Modules". Communications of the ACM 15, 12 (Dec. '72), pp 1053-1058.
  396.  
  397.    B. Liskov and S. Zilles, "Programming with Abstract Data Types".
  398. Proceedings ACM SIGPLAN Conference on Very High-Level Languages.
  399. SIGPLAN Notices 9, 4 (April '74), pp 50-59.
  400.  
  401.  
  402. SUBCLASSED STRUCTURES
  403.  
  404. One more technique. In programming, a general-purpose module may define
  405. a structure like ClientFrame. Along comes a more special-purpose program
  406. that needs a structure like it but with specialized fields added on. The
  407. answer is to build a larger structure whose first field is the earlier
  408. structure. This is called "subclassing" a structure, a term that comes from
  409. subclassing in Smalltalk.
  410.  
  411. In the Macintosh(tm) toolbox, the record GrafPort is subclassed to produce
  412. the record WindowRecord, which is subclassed again to produce a
  413. DialogWindow record.
  414.  
  415. Similarly in the example IFF program ShowILBM, the structure ClientFrame
  416. is subclassed to produce the more specialized structure ILBMFrame.
  417.  
  418.    typedef struct {
  419.       ClientFrame clientFrame;
  420.       UBYTE foundBMHD;
  421.       ...
  422.       } ILBMFrame;
  423.  
  424. Since the first field of an ILBMFrame is a ClientFrame, the ShowILBM
  425. procedure ReadPicture can coerce a *ClientFrame pointer to an
  426. *ILBMFrame pointer to pass it to ReadIFF (which knows nothing about
  427. ILBMFrame). When ReadIFF calls back ShowILBM's getForm procedure, we
  428. can coerce it back to an *ILBMFrame pointer. Take a look at ShowILBM to
  429. see how this works.
  430.  
  431.