Ophis/doc/hll4.sgm

271 lines
10 KiB
Plaintext
Raw Normal View History

2012-06-09 08:06:25 +00:00
<chapter>
<title>Functionals</title>
<para>
This essay deals with indirect calls. These are the core of an
enormous number of high level languages: LISP's closures, C's
function pointers, C++ and Java's virtual method calls, and some
implementations of the <literal>switch</literal> statement.
</para>
<para>
These techniques vary in complexity, and most will not be
appropriate for large-scale assembler projects. Of them, however,
the Data-Directed approach is the most likely to lead to organized
and maintainable code.
</para>
<section>
<title>Function Pointers</title>
<para>
Because assembly language is totally untyped, function pointers
are the same as any other sixteen-bit integer. This makes
representing them really quite easy; most assemblers should permit
routines to be declared simply by naming the routine as
a <literal>.word</literal> directly.
</para>
<para>
To actually invoke these methods, copy them to some sixteen-bit
location (say, <literal>target</literal>) and then invoking the
method is a simple matter of the using an indirect jump:
the <literal>JMP&nbsp;(target)</literal> instruction.
</para>
<para>
There's really only one subtlety here, and it's that the indirect
jump is an indirect <emphasis>jump</emphasis>, not an
indirect <emphasis>function call</emphasis>. Thus, if some
function <literal>A</literal> makes in indirect jump to some
routine, when that routine returns, it returns to whoever
called <literal>A</literal>, not <literal>A</literal>
itself.
</para>
<para>
There are several ways of dealing with this, but only one correct
way, which is to structure your procedures so that any call
to <literal>JMP&nbsp;(xxxx)</literal> occurs at the very
end.
</para>
</section>
<section>
<title>A quick digression on how subroutines work</title>
<para>
Ordinarily, subroutines are called with <literal>JSR</literal> and
finished with <literal>RTS</literal>. The <literal>JSR</literal>
instruction takes its own address, adds 2 to it, and pushes this
16-bit value on the stack, high byte first, then low byte (so that
the low byte will be popped off first).
</para>
<para>
But wait, you may object. All <literal>JSR</literal> instructions
are three bytes long. This <quote>return address</quote> is in
the middle of the instruction. And you would be quite right;
the <literal>RTS</literal> instruction pops off the 16-bit
address, adds one to it, and <emphasis>then</emphasis> sets the
program counter to that value.
</para>
<para>
So it <emphasis>is</emphasis> possible to set up
a <quote><literal>JSR</literal> indirect</quote> kind of operation
by adding two to the indirect jump's address and then pushing that
value onto the stack before making the jump; however, you wouldn't
want to do this. It takes six bytes and trashes your accumulator,
and you can get the same functionality with half the space and
with no register corruption by simply defining the indirect jump
to be a one-instruction routine and <literal>JSR</literal>-ing to
it directly. As an added bonus, that way if you have multiple
indirect jumps through the same pointer, you don't need to
duplicate the jump instruction.
</para>
<para>
Does this mean that abusing <literal>JSR</literal>
and <literal>RTS</literal> is a dead-end, though? Not at all...
</para>
</section>
<section>
<title>Dispatch-on-type and Data-Directed Assembler</title>
<para>
Most of the time, you care about function pointers because you've
arranged them in some kind of table. You hand it an index
representing the type of your argument, or which method it is
you're calling, or some other determinator, and then you index
into an array of routines and execute the right one.
</para>
<para>
Writing a generic routine to do this is kind of a pain. First you
have to pass a 16-bit pointer in, then you have to dereference it
to figure out where your table is, then you have to do an indexed
dereference on <emphasis>that</emphasis> to get the routine you
want to run, then you need to copy it out to somewhere fixed so
that you can write your jump instruction. And making this
non-generic doesn't help a whole lot, since that only saves you
the first two steps, but now you have to write them out in every
single indexed jump instruction. If only there were some way to
easily and quickly pass in a local pointer directly...
</para>
<para>
Something, say, like the <literal>JSR</literal> instruction, only not for
program code.
</para>
<para>
Or we could just use the <literal>JSR</literal> statement itself,
but only call this routine at the ends of other routines, much
like we were organizing for indirect jumps to begin with. This
lets us set up routines that look like this:
</para>
<programlisting>
jump'table'alpha:
jsr do'jump'table
.word alpha'0, alpha'1, alpha'2
</programlisting>
<para>
Where the <literal>alpha'x</literal> routines are the ones to be
called when the index has that value. This leaves the
implementation of do'jump'table, which in this case uses the Y
register to hold the index:
</para>
<programlisting>
do'jump'table:
sta _scratch
pla
sta _jmpptr
pla
sta _jmpptr+1
tya
asl
tay
iny
lda (_jmpptr), y
sta _target
iny
lda (_jmpptr), y
sta _target+1
lda _scratch
jmp (_target)
</programlisting>
<para>
The <literal>TYA:ASL:TAY:INY</literal> sequence can actually be
omitted if you don't mind having your Y indices be 1, 3, 5, 7, 9,
etc., instead of 0, 1, 2, 3, 4, etc. Likewise, the instructions
dealing with <literal>_scratch</literal> can be omitted if you
don't mind trashing the accumulator. Keeping the accumulator and
X register pristine for the target call comes in handy, though,
because it means we can pass in a pointer argument purely in
registers. This will come in handy soon...
</para>
</section>
<section>
<title>VTables and Object-Oriented Assembler</title>
<para>
The usual technique for getting something that looks
object-oriented in non-object-oriented languages is to fill a
structure with function pointers, and have those functions take
the structure itself as an argument. This works just fine in
assembler, of course (and doesn't really require anything more
than your traditional jump-indirects), but it's also possible to
use a lot of the standard optimizations that languages such as C++
provide.
</para>
<para>
The most important of these is the <emphasis>vtable</emphasis>.
Each object type has its own vtable, and it's a list of function
pointers for all the methods that type provides. This is a space
savings over the traditional structs-with-function-pointers
approach because when you have many objects of the same class, you
only have to represent the vtable once. So that all objects may
be treated identically, the vtable location is traditionally fixed
as being the first entry in the corresponding structure.
</para>
<para>
Virtual method invocation takes an object pointer (traditionally
called <literal>self</literal> or <literal>this</literal>) and a
method index and invokes the approprate method on that object.
Gee, where have we seen that before?
</para>
<programlisting>
sprite'vtable:
jsr do'jump'table
.word sprite'init, sprite'update, sprite'render
</programlisting>
<para>
We mentioned before that vtables are generally the first entries
in objects. We can play another nasty trick here, paying an
additional byte per object to have the vtable be not merely a
pointer to its vtable routine, but an actual jump instruction to
it. (That is, if an object is at location X, then location X is
the byte value <literal>$4C</literal>,
representing <literal>JMP</literal>, location X+1 is the low byte
of the vtable, and location X+2 is the high byte of the vtable.)
Given that, our <literal>invokevirtual</literal> function becomes
very simple indeed:
</para>
<programlisting>
invokevirtual:
sta this
stx this+1
jmp (this)
</programlisting>
<para>
Which, combined with all our previous work here, takes
the <literal>this</literal> pointer in <literal>.AX</literal> and
a method identifier in <literal>.Y</literal> and invokes that
method on that object. Arguments besides <literal>this</literal>
need to be set up before the call
to <literal>invokevirtual</literal>, probably in some global
argument array somewhere as discussed back in <xref linkend="hll2">.
</para>
</section>
<section>
<title>A final reminder</title>
<para>
We've been talking about all these routines as if they could be
copy-pasted or hand-compiled from C++ or Java code. This isn't
really the case, primarily because <quote>local variables</quote>
in your average assembler routines aren't really local, so
multiple calls to the same method will tend to trash the program
state. And since a lot of the machinery described here shares a
lot of memory (in particular, every single method invocation
everywhere shares a <literal>this</literal>), attempting to shift
over standard OO code into this format is likely to fail
miserably.
</para>
<para>
You can get an awful lot of flexibility out of even just one layer
of method-calls, though, given a thoughtful
design. The <literal>do'jump'table</literal> routine, or one very
like it, was extremely common in NES games in the mid-1980s and
later, usually as the beginning of the frame-update loop.
</para>
<para>
If you find you really need multiple layers of method calls,
though, then you really are going to need a full-on program stack,
and that's going to be several kinds of mess. That's the topic
for the final chapter.
</para>
</section>
</chapter>