For instance, an ID3 tag defined in the 2.2 version of the specification consists of a header made up of a three-character ISO-8859-1 string, which is always 'ID3'; two one-byte unsigned integers that specify the major version and revision of the specification; eight bits worth of boolean flags; and four bytes that encode the size of the tag in an encoding particular to the ID3 specification. Following the header is a list of
If you look at the world through the lens of object orientation, composite structures look a lot like classes. For instance, you could write a class to represent an ID3 tag.
(defclass id3-tag ()
((identifier :initarg :identifier :accessor identifier)
(major-version :initarg :major-version :accessor major-version)
(revision :initarg :revision :accessor revision)
(flags :initarg :flags :accessor flags)
(size :initarg :size :accessor size)
(frames :initarg :frames :accessor frames)))
An instance of this class would make a perfect repository to hold the data needed to represent an ID3 tag. You could then write functions to read and write instances of this class. For example, assuming the existence of certain other functions for reading the appropriate primitive data types, a read-id3-tag function might look like this:
(defun read-id3-tag (in)
(let ((tag (make-instance 'id3-tag)))
(with-slots (identifier major-version revision flags size frames) tag
(setf identifier (read-iso-8859-1-string in :length 3))
(setf major-version (read-u1 in))
(setf revision (read-u1 in))
(setf flags (read-u1 in))
(setf size (read-id3-encoded-size in))
(setf frames (read-id3-frames in :tag-size size)))
tag))
The write-id3-tag function would be structured similarly—you'd use the appropriate write-* functions to write out the values stored in the slots of the id3-tag object.
It's not hard to see how you could write the appropriate classes to represent all the composite data structures in a specification along with read-foo and write-foo functions for each class and for necessary primitive types. But it's also easy to tell that all the reading and writing functions are going to be pretty similar, differing only in the specifics of what types they read and the names of the slots they store them in. It's particularly irksome when you consider that in the ID3 specification it takes about four lines of text to specify the structure of an ID3 tag, while you've already written eighteen lines of code and haven't even written write-id3-tag yet.
What you'd really like is a way to describe the structure of something like an ID3 tag in a form that's as compressed as the specification's pseudocode yet that can also be expanded into code that defines the id3 -tag class
Since you already have a rough idea what code your macros will need to generate, the next step, according to the process for writing a macro I outlined in Chapter 8, is to switch perspectives and think about what a call to the macro should look like. Since the goal is to be able to write something as compressed as the pseudocode in the ID3 specification, you can start there. The header of an ID3 tag is specified like this:
ID3/file identifier 'ID3'
ID3 version $02 00
ID3 flags %xx000000
ID3 size 4 * %0xxxxxxx
In the notation of the specification, this means the 'file identifier' slot of an ID3 tag is the string 'ID3' in ISO- 8859-1 encoding. The version consists of two bytes, the first of which—for this version of the specification—has the value 2 and the second of which—again for this version of the specification—is 0. The flags slot is eight bits, of which all but the first two are 0, and the size consists of four bytes, each of which has a 0 in the most significant bit.
Some information isn't captured by this pseudocode. For instance, exactly how the four bytes that encode the size are to be interpreted is described in a few lines of prose. Likewise, the spec describes in prose how the frame and subsequent padding is stored after this header. But most of what you need to know to be able to write code to read and write an ID3 tag is specified by this pseudocode. Thus, you ought to be able to write an s-expression version of this pseudocode and have it expanded into the class and function definitions you'd otherwise have to write by hand—something, perhaps, like this:
(define-binary-class id3-tag
((file-identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)
(frames (id3-frames :tag-size size))))
The basic idea is that this form defines a class id3-tag similar to the way you could with DEFCLASS, but instead of specifying things such as :initarg and :accessors, each slot specification consists of the name of the slot—file- identifier, major-version, and so on—and information about how that slot is represented on disk. Since this is just a bit of fantasizing, you don't have to worry about exactly how the macro define- binary-class will know what to do with expressions such as (iso-8859-1-string :length 3), u1, id3-tag-size, and (id3-frames :tag-size size); as long as each expression contains the information necessary to know how to read and write a particular data encoding, you should be okay.
Okay, enough fantasizing about good-looking code; now you need to get to work writing define- binary-class—writing the code that will turn that concise expression of what an ID3 tag looks like into code that can represent one in memory, read one off disk, and write it back out.
To start with, you should define a package for this library. Here's the package file that comes with the version you can download from the book's Web site:
(in-package :cl-user)
(defpackage :com.gigamonkeys.binary-data
(:use :common-lisp :com.gigamonkeys.macro-utilities)
(:export :define-binary-class
:define-tagged-binary-class
