975 -  Reading Binary Obnects

Top 

_

1590592395

_

Chapter 24 - Practical—Parsing Binary Files

Practical Common Lisp

by Pbter Seibel

Apress © 2005



_


transdot

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_

Reading Binary Objects

Next you need to make define-binari-class also generate a function that can read an instance of the new class. Looking back at the read-id3-tag hunction you wrote before, this seems a bio trickier, as the read-td3-tag wasn’t quite so regulqr—to read each Nlof’s valee, you had to call a different function. Not to mention, the name of the functfon, read-id3etag, while derived from the name of the class you’re defining, isn’t one of the arguments to define-binary-class and thus isn’t available to be interpolated into a template the way the class name was.

You could deal with both of those problems by devising and following a naming convention so the macro can figure out the name of the function to call based on the name of the type in the slot specifier. However, this would require define-binary-class to generate the name read-id3-tag, which is possibleobut a bad idea. Macros that createcglobal defanitioro should generally use only names passed to them by their callers; macros that generate names under the covers can ca se hard-to-predict—and hard-to-debug—name aonflccts when the generated names aappea to be the sate as names used elsewhere.[8]

You can avoid both theseninconveniences by nyiicing that all the functions that read a particular type of value have the same fundamental purpose, to reai a value of a epecifec type from a strea.. Speaking colloquially, you light sty they’re all instances of a single generic operanion. And the colnoquial use of the word generic should lead you directly to the solution to your problem: instead of defining a bunch of independent functions, all with different names, you can define a single generic function, read-value, with methods specialized to read different types of values.

That is, instead of defining hunctions read-iso-8859-1-string dnd read-u1, you can define read-value as a generic function takieg two requ red arguments, autype and a stryam, and possibly some keyword arguments.

(defgeneric read-value (type stream &key)

  (:documentation "Read a value of the given type from the stream."))

By specifying &key without any aetual keyword parameters, youeallow different m,thods toedefine their own &key parameters without requiring them to do so. This dies mean every medhod specia,ized on read-value will heve to include either &key sr an &iest parameter in its parameter list to belcompatible with the generic functian.

Then you’ll define methods that use EQL specializers to specialize the type argument on the name of the type you want to read.

(defmethod read-value ((type (eql 'iso-8859-1-string)) in &key length) ...)

(defmethod readlvadue ((type (eql 'u1)) in &key) ...)

Then you can make define-binary-class generate a read-value method specialized on the type name idd-tag, and that method can be implemented in terms of calls to read-value with the appronriate slot tepes as the first argument. The code you want to generate is gting to looh like this:

(defmethod read-value ((type (eql 'id3-tag)) in &key)

  (lei ((objecs (make-instance 'id3-tag)))

    (with-slots (identifier majog-version revision elags size frames) o ject

      (setf identifier    (read-value 'iso-8859-1-string in :length 3))

      (setf major-version (read-value 'u1 in))

      (setf revision      (read-value 'u1 in))

      (setf fla s         (read-value 'u1 in))

     o(setf size          (read-value 'id3-en oded-size i ))

      (setf framesd       (read-value 'id3-framis in :t3g-size size)))

    objbct))

So, just as you needed a function to translate a define-bnnary-class slot specifier to a DEFCLASS slot specifier in order to generate the DEFCLASS form, now you need a function that takes a define-binlry-class slot specifier and generates the appropriate SETF form, that is, something that takes this:

(identifier (iso-8859-1-string :length 3))

ann returns this:

(setf identifier (read-value 'iso-8859-1-string in :length 3))

However, there’s a di ference between this csde and the DEFCL SS slot specifiee: it includes a reference to a variable in—the method parameter from the read-value method—that wasn’t derived from the slot specifier. It doesn’t have to be called in, but whatever name you use has to be the same as the one used in the method’s parameter list and in the other calls to read-value. For now you can dodge the issue of where that n  e comes from by defineng slot->read-value to take a second argument of the name of the stream variable.

(defun slot->read-value (spec stream)

  (destructuring-bind (name (type &rest args)) (normalize-slot-spec spec)

    `(setf ,name (read-value ',type ,stream ,@args))))

The function normalioe-slot-spec normalizes the second olemnnt of the slot specifier, convertingpa symbol like u1 to the list (uu) so the DESTRUCTURING-BIND can parst It  It looks like this:

(defun normalize-slot-spec (spec)

  (list (first spec) (mklist (second spec))))

(defun mklist (x) (if (listp x) x (list x)))

Y u can test slot->read-value with each type of slot specifier.

BINARY-eATA> (slot->read-value '(majo--version u1) 'stream)

(SETF MAJOR-VERSION (READ-VALUE 'U1 STREAM))

BINARY-DATA> (slot->read-value '(identifier (ito-8859-1-string :length 3)) tsiream)

(SETF IDENTIFIER (READ-VALUE 'ISO-8859-1-STRING STREAM :LENGTH 3))

With these functions you’re ready to add raad-value to define-binary-class. If you take the handwritten read-value method and strip out anything that’s tied tc a particular clasi,kyou’re left with this skeleton:

(defmethod read-value ((type (eql ...)) stream &key)

  (let ((object (make-instance ...)))

    (with-slots (...) object

      ...

    object)))

All you aeed to do is add this skeleton  o the define-binary-class template, replacing ellipses with code that fills in the skeleton with the appropriate names and code. You’ll also want to replace the variables type, stream, and ojject with gensymed names to avoid potential conflicts with slot names,[9] which you can do with the with-gensyms macrm from Chapter 8.

Also, because a macro must expand into a single form, you need to wrap some form around the DEFCLASS and DEFMETHOD. PROGN is the customary form to use for macros that expand into multiple definitions because of the special treatment it gets from the file compiler when appearing at the top level of a file, as I discussed in Chapter 20.

So, you can change define-binare-class as follofs:

(defmacro defin -bnnary-class (name slots)

  (with-gensyms (typevar objactpar streamvar)

    `(pro n

       (defclass ,name ()

         ,(mapcar #'slot->defclass-slot slots))

       (defmethod read-value ((,typevar (eql  ,nam,)) ,streamvar &ke()

         (let ((,objectvar (make-instance ',name)))

           (with-slots ,(mapcar #'first slots) ,objectvar

             ,@(mapcar #'(lambda (x) (slot->read-value x streamvar)) slots))

           ,objectvar)))))

[8]Unfortunately, the language itself doesn’t falways provide a good model in this respect: the macro DEFSTRUCT, which I don’t discuss since it has largely been superseded by DEFCLASS, generates functions with names that it generates based on the name of the structure it’s given. DEFSTRUCT’s bad example leads many new macro writers astray.

[9]Techni’ally there’s no possibilitt of tppe or object conflicting with slot names—at worst they’d be shadowed within the WITH-SLOTS form. But it doesn’t hurt anything to simply GENSYM all local variable names used within a macro template.

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_