While looking for CL code that I might leverage for the task, I came
across a thread on c.l.l. about parsing PE files, where someone
suggested to the original poster possibly employing the techniques and
code in Chapter 24 of PCL. Coincidentally, I had considered this
approach, since I had referred to the same when I needed to parse some
DBase files. I decided to start from there.
After defining the PE binary structure, I came across a surprise.
Whereas individual tests of the sub-structures succeeded, putting them
together yielded some strange results, for the following composite
binary class:
(define-binary-class win32-pe-image-header
(pe-image-header-signature
coff-file-header
standard-optional-image-header
win32-optional-image-header) ())
The results didn't match the information in hexdump and windump.
Something was fishy. Because I had recently skimmed chapters 24 & 25,
I vaguely recalled something about the method combination that Peter
Seibel employed to address incorrect instantiation when inheritance
was involved. This vague recollection was enough for me to try
reversing the specified subclasses, and viola! [sic] it worked with
the following:
(define-binary-class win32-pe-image-header
(win32-optional-image-header
standard-optional-image-header
coff-file-header
pe-image-header-signature) ())
Though it works, this situation is rather disturbing and non-intuitive
to me only because it's the reverse of what one would expect without
delving into the details of the supporting framework. In other words,
the semantics don't match the surface form. It might cause some
serious head scratching for me or anyone else looking at the code in
the future.
I checked the HyperSpec to review details about method combination.
7.6.6.4 lists the built-in method combination types, but I couldn't
find details in the HyperSpec about the individual types and am left
wondering if one must infer the behavior of each type based on the
function or special form with the same name.
Does the HyperSpec have a section detailing the behaviors of the built-
in types?
Does anyone have a suggestion for a built-in type that would remedy
this snafu with minimal changes to the supporting framework, or would
fixing the issue require a custom method combination or other
workaround?
Does this violation of expectation reflect an oversight in the design
of the framework, is this an unintended usage of the framework
(despite its apparent fit for the described intention), or was it a
case of it being "good enough" or a kludge?
...
> Does this violation of expectation reflect an oversight in the design
> of the framework, is this an unintended usage of the framework
> (despite its apparent fit for the described intention), or was it a
> case of it being "good enough" or a kludge?
From the source code:
(defgeneric read-object (object stream)
(:method-combination progn :most-specific-last)
(:documentation "Fill in the slots of object from stream."))
You can see that it uses the PROGN method combination.
Primary methods run MOST SPECIFIC LAST. The CLOS default
is MOST SPECIFIC FIRST. So in this case, the
order is reversed.
PROGN means that the applicable primary methods
will all be executed and the return value is
the value of the last one (or whatever
around methods do). As you can see
above, the applicable methods are sorted
MOST SPECIFIC LAST.
Thanks, a more careful review of 7.6.6.4 reveals the implications of
the individual built-in types, although I found the language somewhat
densely nebulous. The answer was in,
"If there are no around methods or if call-next-method is called by
the least specific around method, a Lisp form derived from the name of
the built-in method combination type and from the list of applicable
primary methods is evaluated to produce the value of the generic
function. Suppose the name of the method combination type is operator
and the call to the generic function is of the form
(generic-function a1...an)
Let M1,...,Mk be the applicable primary methods in order; then the
derived Lisp form is
(operator <M1 a1...an>...<Mk a1...an>)"
My difficulty was with the phrase "a Lisp derived from the name", as
in some derivation of the CL function or special operator with the
same but not specifically specified as the same behavior as the CL
function with the same name. Sometimes, I'm just too literal. The
(operator...) example should have clarified it for me. Whereas it
might be a completely logical inference, contradiction and inference
has caused me too many headaches.
After posting the original query, I considered that the optimal
solution would be reversing the order of the subclasses in the define-
binary-class macro. This would fix the violation of expectation on the
surface form, and one could clearly see in the macro definition that
the order was reversed to satisfy the explicit method combination
type. Documentation of define-binary-class would easily clarify any
confusion presented by the method combination of the generic method
and the specified order of the class inheritance. End result: no
violation of expectation and one doesn't need to dig, dig, dig to
understand all of the individual pieces to understand the whole. (read-
object is several levels down the chain, read-value is the interface
to read an object by classname, and the user never directly defines
read-value or read-object because they are wrapped in a DSL, but read-
value doesn't specify a most-specific-last method combination.)
So I have answered my own questions with the exception of the last one
involving design philosophy. Thanks.
To clarify my previous post,
(defmacro define-binary-class (name (&rest superclasses) slots)
(with-gensyms (objectvar streamvar)
`(define-generic-binary-class ,name ,(reverse superclasses) ,slots
(defmethod read-object progn ((,objectvar ,name) ,streamvar)
(declare (ignorable ,streamvar))
(with-slots ,(new-class-all-slots slots `,(reverse
superclasses)) ,objectvar
,@(mapcar #'(lambda (x) (slot->read-value x streamvar))
slots))))))
(defmacro define-tagged-binary-class (name (&rest superclasses) slots
&rest options)
(with-gensyms (typevar objectvar streamvar)
`(define-generic-binary-class ,name ,(reverse superclasses) ,slots
(defmethod read-value ((,typevar (eql ',name)) ,streamvar &key)
(let* ,(mapcar #'(lambda (x) (slot->binding x streamvar))
slots)
(let ((,objectvar
(make-instance
,@(or (cdr (assoc :dispatch options))
(error "Must supply :disptach form."))
,@(mapcan #'slot->keyword-arg slots))))
(read-object ,objectvar ,streamvar)
,objectvar))))))
Notice the reversals of superclasses. Now,
(define-binary-class win32-pe-image-header
(pe-image-header-signature
coff-file-header
standard-optional-image-header
win32-optional-image-header) ())
functions as expected with no surprises--unless you're reading
Hanunó'o (http://en.wikipedia.org/wiki/Writing_system#Directionality).
Caveat: I haven't tested define-tagged-binary-class, but I revised it
with the reversal to be consistent with define-binary-class.
--
Robert A. Uhl
I've got a lot more than nine words for snow, and I don't even need to
resort to Eskimo. This is because I have a powerful descriptive vocabulary.
--Cecil Adams
So the theory, as far as I recall, was that class inheritance was
intended to be used like this:
All FOOs start with certain fields. BARs extend FOO and then have some
other fields, and so on. In such a case you want READ-OBJECT to read
the fields defined by the more general class first and then the fields
added by the more specific class. Thus the :most-specific-last in the
DEFGENERIC of READ-OBJECT. It's not clear to me how this ought to map
to multiple inheritance. Per 4.3.5, "A class precedes its direct
superclasses, and a direct superclass precedes all other direct
superclasses specified to its right in the superclasses list of the
defclass form." Thus in your original class definition the most
specific of the direct superclasses was pe-image-header-signature. I'm
not sure reversing the order of the direct superclasses under the
covers is the right answer, though it does give you the behavior you
desire in this case. I think I would probably say this is a case where
you have a HAS-A relationship rather than IS-A: a win32-pe-image-
header consists of a pe-image-header-signature followed by a win32-
optional-image-header followed by a standard-optional-image-header
followed by a coff-file-header but it is not an instance of those
things.
-Peter
Hi, Peter,
I thank you for exposing the flaws in my thinking. Indeed, I mangled
the relations. The flu must have compromised my logic.
My problem was not understanding that types defined with define-binary-
types and types defined with define-binary-class were interchangeable
as slots for define-binary-class. Reviewing the source code of the
framework didn't reveal any reason to preclude it, so I tried it.
(define-binary-class win32-pe-image-header ()
((pe-image-header-signature le-dword)
(coff-header coff-file-header)
(standard-optional-header standard-optional-image-header)
(win32-optional-header win32-optional-image-header)))
Of course, this works as it should. My confusion in the start of this
thread would have been avoided had I looked at the framework more
carefully and tried it this way in the first place.
Thanks, again, for helping with my stinkin' thinkin'.