Книга: Practical Common Lisp
Reading Binary Objects
Reading Binary Objects
Next you need to make define-binary-class
also generate a function that can read an instance of the new class. Looking back at the read-id3-tag
function you wrote before, this seems a bit trickier, as the read-id3-tag
wasn't quite so regular—to read each slot's value, you had to call a different function. Not to mention, the name of the function, read-id3-tag
, 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 possible but a bad idea. Macros that create global definitions should generally use only names passed to them by their callers; macros that generate names under the covers can cause hard-to-predict—and hard-to-debug—name conflicts when the generated names happen to be the same as names used elsewhere.[267]
You can avoid both these inconveniences by noticing that all the functions that read a particular type of value have the same fundamental purpose, to read a value of a specific type from a stream. Speaking colloquially, you might say they're all instances of a single generic operation. And the colloquial 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 functions read-iso-8859-1-string
and read-u1
, you can define read-value
as a generic function taking two required arguments, a type and a stream, 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 actual keyword parameters, you allow different methods to define their own &key
parameters without requiring them to do so. This does mean every method specialized on read-value
will have to include either &key
or an &rest
parameter in its parameter list to be compatible with the generic function.
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 read-value ((type (eql 'u1)) in &key) ...)
Then you can make define-binary-class
generate a read-value
method specialized on the type name id3-tag
, and that method can be implemented in terms of calls to read-value
with the appropriate slot types as the first argument. The code you want to generate is going to look like this:
(defmethod read-value ((type (eql 'id3-tag)) in &key)
(let ((object (make-instance 'id3-tag)))
(with-slots (identifier major-version revision flags size frames) object
(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 flags (read-value 'u1 in))
(setf size (read-value 'id3-encoded-size in))
(setf frames (read-value 'id3-frames in :tag-size size)))
object))
So, just as you needed a function to translate a define-binary-class
slot specifier to a DEFCLASS
slot specifier in order to generate the DEFCLASS
form, now you need a function that takes a define-binary-class
slot specifier and generates the appropriate SETF
form, that is, something that takes this:
(identifier (iso-8859-1-string :length 3))
and returns this:
(setf identifier (read-value 'iso-8859-1-string in :length 3))
However, there's a difference between this code and the DEFCLASS
slot specifier: 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 name comes from by defining 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 normalize-slot-spec
normalizes the second element of the slot specifier, converting a symbol like u1
to the list (u1)
so the DESTRUCTURING-BIND
can parse 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)))
You can test slot->read-value
with each type of slot specifier.
BINARY-DATA> (slot->read-value '(major-version u1) 'stream)
(SETF MAJOR-VERSION (READ-VALUE 'U1 STREAM))
BINARY-DATA> (slot->read-value '(identifier (iso-8859-1-string :length 3)) 'stream)
(SETF IDENTIFIER (READ-VALUE 'ISO-8859-1-STRING STREAM :LENGTH 3))
With these functions you're ready to add read-value
to define-binary-class
. If you take the handwritten read-value
method and strip out anything that's tied to a particular class, you're left with this skeleton:
(defmethod read-value ((type (eql ...)) stream &key)
(let ((object (make-instance ...)))
(with-slots (...) object
...
object)))
All you need to do is add this skeleton to 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 object
with gensymed names to avoid potential conflicts with slot names,[268] which you can do with the with-gensyms
macro 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-binary-class
as follows:
(defmacro define-binary-class (name slots)
(with-gensyms (typevar objectvar streamvar)
`(progn
(defclass ,name ()
,(mapcar #'slot->defclass-slot slots))
(defmethod read-value ((,typevar (eql ',name)) ,streamvar &key)
(let ((,objectvar (make-instance ',name)))
(with-slots ,(mapcar #'first slots) ,objectvar
,@(mapcar #'(lambda (x) (slot->read-value x streamvar)) slots))
,objectvar)))))
- Binary Files
- Binary Format Basics
- Strings in Binary Files
- Composite Structures
- Designing the Macros
- Making the Dream a Reality
- Reading Binary Objects
- Writing Binary Objects
- Adding Inheritance and Tagged Structures
- Keeping Track of Inherited Slots
- Tagged Structures
- Primitive Binary Types
- The Current Object Stack
- 24. Practical: Parsing Binary Files
- Writing Binary Objects
- Binary Files
- Binary Format Basics
- Binary Serialization
- Creating and Deleting Device Objects
- 2. Binary – the way micros count
- Reading Documentation
- Reading Manual Pages with man
- 1.5.1. Suggestions for Additional Reading
- 2.5.1. Suggestions for Additional Reading
- 3.4.1. Suggestions For Additional Reading