(major-version :initarg :major-version :accessor major-version)
Easy enough. First define a simple function to translate a symbol to the corresponding keyword symbol.
(defun as-keyword (sym) (intern (string sym) :keyword))
Now define a function that takes a define-binary-class slot specifier and returns a DEFCLASS slot specifier.
(defun slot->defclass-slot (spec)
(let ((name (first spec)))
`(,name :initarg ,(as-keyword name) :accessor ,name)))
You can test this function at the REPL after switching to your new package with a call to IN-PACKAGE.
BINARY-DATA> (slot->defclass-slot '(major-version u1))
(MAJOR-VERSION :INITARG :MAJOR-VERSION :ACCESSOR MAJOR-VERSION)
Looks good. Now the first version of define-binary-class is trivial.
(defmacro define-binary-class (name slots)
`(defclass ,name ()
,(mapcar #'slot->defclass-slot slots)))
This is simple template-style macro—define-binary-class generates a DEFCLASS form by interpolating the name of the class and a list of slot specifiers constructed by applying slot->defclass-slot to each element of the list of slots specifiers from the define-binary-class form.
To see exactly what code this macro generates, you can evaluate this expression at the REPL.
(macroexpand-1 '(define-binary-class id3-tag
((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 result, slightly reformatted here for better readability, should look familiar since it's exactly the class definition you wrote by hand earlier:
(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)))
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.
267
Unfortunately, the language itself doesn't always 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.