Выбрать главу

(define-binary-type u1 () (unsigned-integer :bytes 1))

which will expand to this:

(progn

(defmethod read-value ((#:g161887 (eql 'u1)) #:g161888 &key)

(read-value 'unsigned-integer #:g161888 :bytes 1))

(defmethod write-value ((#:g161887 (eql 'u1)) #:g161888 #:g161889 &key)

(write-value 'unsigned-integer #:g161888 #:g161889 :bytes 1)))

To support both long- and short-form define-binary-type calls, you need to differentiate based on the value of the spec argument. If spec is two items long, it represents a long-form call, and the two items should be the :reader and :writer specifications, which you extract as before. On the other hand, if it's only one item long, the one item should be a type specifier, which needs to be parsed differently. You can use ECASE to switch on the LENGTH of spec and then parse spec and generate an appropriate expansion for either the long form or the short form.

(defmacro define-binary-type (name (&rest args) &body spec)

(ecase (length spec)

(1

(with-gensyms (type stream value)

(destructuring-bind (derived-from &rest derived-args) (mklist (first spec))

`(progn

(defmethod read-value ((,type (eql ',name)) ,stream &key ,@args)

(read-value ',derived-from ,stream ,@derived-args))

(defmethod write-value ((,type (eql ',name)) ,stream ,value &key ,@args)

(write-value ',derived-from ,stream ,value ,@derived-args))))))

(2

(with-gensyms (type)

`(progn

,(destructuring-bind ((in) &body body) (rest (assoc :reader spec))

`(defmethod read-value ((,type (eql ',name)) ,in &key ,@args)

,@body))

,(destructuring-bind ((out value) &body body) (rest (assoc :writer spec))

`(defmethod write-value ((,type (eql ',name)) ,out ,value &key ,@args)

,@body)))))))

The Current Object Stack

One last bit of functionality you'll need in the next chapter is a way to get at the binary object being read or written while reading and writing. More generally, when reading or writing nested composite objects, it's useful to be able to get at any of the objects currently being read or written. Thanks to dynamic variables and :around methods, you can add this enhancement with about a dozen lines of code. To start, you should define a dynamic variable that will hold a stack of objects currently being read or written.

(defvar *in-progress-objects* nil)

Then you can define :around methods on read-object and write-object that push the object being read or written onto this variable before invoking CALL-NEXT-METHOD.

(defmethod read-object :around (object stream)

(declare (ignore stream))

(let ((*in-progress-objects* (cons object *in-progress-objects*)))

(call-next-method)))

(defmethod write-object :around (object stream)

(declare (ignore stream))

(let ((*in-progress-objects* (cons object *in-progress-objects*)))

(call-next-method)))

Note how you rebind *in-progress-objects* to a list with a new item on the front rather than assigning it a new value. This way, at the end of the LET, after CALL-NEXT-METHOD returns, the old value of *in-progress-objects* will be restored, effectively popping the object of the stack.

With those two methods defined, you can provide two convenience functions for getting at specific objects in the in-progress stack. The function current-binary-object will return the head of the stack, the object whose read-object or write-object method was invoked most recently. The other, parent-of-type, takes an argument that should be the name of a binary object class and returns the most recently pushed object of that type, using the TYPEP function that tests whether a given object is an instance of a particular type.

(defun current-binary-object () (first *in-progress-objects*))

(defun parent-of-type (type)

(find-if #'(lambda (x) (typep x type)) *in-progress-objects*))

These two functions can be used in any code that will be called within the dynamic extent of a read-object or write-object call. You'll see one example of how current-binary-object can be used in the next chapter.[270]

Now you have all the tools you need to tackle an ID3 parsing library, so you're ready to move onto the next chapter where you'll do just that.

25. Practicaclass="underline" An ID3 Parser

With a library for parsing binary data, you're ready to write some code for reading and writing an actual binary format, that of ID3 tags. ID3 tags are used to embed metadata in MP3 audio files. Dealing with ID3 tags will be a good test of the binary data library because the ID3 format is a true real-world format—a mix of engineering trade-offs and idiosyncratic design choices that does, whatever else might be said about it, get the job done. In case you missed the file-sharing revolution, here's a quick overview of what ID3 tags are and how they relate to MP3 files.

MP3, also known as MPEG Audio Layer 3, is a format for storing compressed audio data, designed by researchers at Fraunhofer IIS and standardized by the Moving Picture Experts Group, a joint committee of the International Organization for Standardization (ISO) and the International Electrotechnical Commission (IEC). However, the MP3 format, by itself, defines only how to store audio data. That's fine as long as all your MP3 files are managed by a single application that can store metadata externally and keep track of which metadata goes with which files. However, when people started passing around individual MP3 files on the Internet, via file-sharing systems such as Napster, they soon discovered they needed a way to embed metadata in the MP3 files themselves.

Because the MP3 standard was already codified and a fair bit of software and hardware had already been written that knew how to decode the existing MP3 format, any scheme for embedding information in an MP3 file would have to be invisible to MP3 decoders. Enter ID3.

The original ID3 format, invented by programmer Eric Kemp, consisted of 128 bytes stuck on the end of an MP3 file where it'd be ignored by most MP3 software. It consisted of four 30-character fields, one each for the song title, the album title, the artist name, and a comment; a four-byte year field; and a one-byte genre code. Kemp provided standard meanings for the first 80 genre codes. Nullsoft, the makers of Winamp, a popular MP3 player, later supplemented this list with another 60 or so genres.

вернуться

270

The ID3 format doesn't require the parent-of-type function since it's a relatively flat structure. This function comes into its own when you need to parse a format made up of many deeply nested structures whose parsing depends on information stored in higher-level structures. For example, in the Java class file format, the top-level class file structure contains a constant pool that maps numeric values used in other substructures within the class file to constant values that are needed while parsing those substructures. If you were writing a class file parser, you could use parent-of-type in the code that reads and writes those substructures to get at the top-level class file object and from there to the constant pool.