The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Twelve (Part 4)

Table of Content

Chapter Twelve (Part 6) 

CHAPTER TWELVE:
PROCEDURES: ADVANCED TOPICS (Part 5)
12.4 - Passing Procedures as Parameters
12.4 Passing Procedures as Parameters

Many programming languages let you pass a procedure or function name as a parameter. This lets the caller pass along various actions to perform inside a procedure. The classic example is a plot procedure that graphs some generic math function passed as a parameter to plot.

Standard Pascal lets you pass procedures and functions by declaring them as follows:

procedure DoCall(procedure x);
begin

        x;

end;

The statement DoCall(xyz); calls DoCall that, in turn, calls procedure xyz.

Passing a procedure or function as a parameter may seem like an easy task - just pass the address of the function or procedure as the following example demonstrates:

procedure PassMe;
begin
        Writeln('PassMe was called');
end;

procedure CallPassMe(procedure x);
begin
        x;
end;

begin {main}
        CallPassMe(PassMe);
end.

The 80x86 code to implement the above could look like the following:

PassMe          proc    near
                print
                byte    "PassMe was called",cr,lf,0
                ret
PassMe          endp

CallPassMe      proc    near
                push    bp
                mov     bp, sp
                call    word ptr [bp+4]
                pop     bp
                ret     2
CallPassMe              endp

Main            proc    near
                lea     bx, PassMe      ;Pass address of PassMe to
                push    bx              ; CallPassMe
                call    CallPassMe
                ExitPgm
Main            endp

For an example as simple as the one above, this technique works fine. However, it does not always work properly if PassMe needs to access non-local variables. The following Pascal code demonstrates the problem that could occur:

program main;

        procedure dummy;
        begin end;

        procedure Recurse1(i:integer; procedure x);

                procedure Print;
                begin

                        writeln(i);

                end;

                procedure Recurse2(j:integer; procedure y);
                begin

                        if (j=1) then y
                        else if (j=5) then Recurse1(j-1, Print)
                        else Recurse1(j-1, y);

                end;

        begin {Recurse1}

                Recurse2(i, x);

        end;

begin {Main}

        Recurse1(5,dummy);

end.

This code produces the following call sequence:

Recurse1(5,dummy) --> Recurse2(5,dummy) --> Recurse1(4,Print) -->
Recurse2(4,Print) --> Recurse1(3,Print) --> Recurse2(3,Print) -->
Recurse1(2,Print) --> Recurse2(2,Print) --> Recurse1(1,Print) -->
Recurse2(1,Print) --> Print

Print will print the value of Recurse1's i variable to the standard output. However, there are several activation records present on the stack that raises the obvious question, "which copy of i does Print display?" Without giving it much thought, you might conclude that it should print the value "1" since Recurse2 calls Print when Recurse1's value for i is one. Note, though, that when Recurse2 passes the address of Print to Recurse1, i's value is four. Pascal, like most block structured languages, will use the value of i at the point Recurse2 passes the address of Print to Recurse1. Hence, the code above should print the value four, not the value one.

This creates a difficult implementation problem. After all, Print cannot simply access the display to gain access to the global variable i - the display's entry for Recurse1 points at the latest copy of Recurse1's activation record, not the entry containing the value four which is what you want.

The most common solution in systems using a display is to make a local copy of each display whenever calling a procedure or function. When passing a procedure or function as a parameter, the calling code copies the display along with the address of the procedure or function. This is why Intel's enter instruction makes a copy of the display when building the activation record.

If you are passing procedures and functions as parameters, you may want to consider using static links rather than a display. When using a static link you need only pass a single pointer (the static link) along with the routine's address. Of course, it is more work to access non-local variables, but you don't have to copy the display on every call, which is quite expensive.

The following 80x86 code provides the implementation of the above code using static links:

wp              textequ <word ptr>

Dummy           proc    near
                ret
Dummy           endp

; PrintIt; (Use the name PrintIt to avoid conflict).
;
;       stack:
;
;       bp+4:   static link.

PrintIt         proc    near
                push    bp
                mov     bp, sp
                mov     bx, [bp+4]      ;Get static link
                mov     ax, ss:[bx-10]  ;Get i's value.
                puti
                pop     bp
                ret     2
PrintIt         endp

; Recurse1(i:integer; procedure x);
;
;       stack:
;
;       bp+10:  i
;       bp+8:   x's static link
;       bp+6: x's address

Recurse1        proc    near
                push    bp
                mov     bp, sp
                push    wp [bp+10]      ;Push value of i onto stack.
                push    wp [bp+8]       ;Push x's static link.
                push    wp [bp+6]       ;Push x's address.
                push    bp              ;Push Recurse1's static link.
                call    Recurse1
                pop     bp
                ret     6
Recurse1        endp

; Recurse2(i:integer; procedure y);
;
;       stack:
;
;       bp+10: j
;       bp+8:  y's static link.
;       bp+6:  y's address.
;       bp+4:  Recurse2's static link.

Recurse2        proc    near
                push    bp
                mov     bp, sp
                cmp     wp [bp+10], 1   ;Is j=1?
                jne     TryJeq5
                push    [bp+8]          ;y's static link.
                call    wp [bp+6]       ;Call y.
                jmp     R2Done

TryJeq5:        cmp     wp [bp+10], 5   ;Is j=5?
                jne     Call1
                mov     ax, [bp+10]
                dec     ax
                push    ax
                push    [bp+4]          ;Push static link to R1.
                lea     ax, PrintIt     ;Push address of print.
                push    ax
                call    Recurse1
                jmp     R2Done

Call1:          mov     ax, [bp+10]
                dec     ax
                push    ax
                push    [bp+8]          ;Pass along existing
                push    [bp+6]          ; address and link.
                call    Recurse1

R2Done:         pop     bp
                ret     6
Recurse1        endp

main            proc
                push    bp
                mov     bp, sp
                mov     ax, 5           ;Push first parameter.
                push    ax
                push    bp              ;Dummy static link.
                lea     ax, Dummy       ;Push address of dummy.
                push    ax
                call    Recurse1
                pop     bp
                ExitPgm
main            endp

There are several ways to improve this code. Of course, this particular program doesn't really need to maintain a display or static list because only PrintIt accesses non-local variables; however, ignore that fact for the time being and pretend it does. Since you know that PrintIt only accesses variables at one particular lex level, and the program only calls PrintIt indirectly, you can pass a pointer to the appropriate activation record; this is what the above code does, although it maintains full static links as well. Compilers must always assume the worst case and often generate inefficient code. If you study your particular needs, however, you may be able to improve the efficiency of your code by avoiding much of the overhead of maintaining static lists or copying displays.

Keep in mind that thunks are special cases of functions that you call indirectly. They suffer from the same problems and drawbacks as procedure and function parameters with respect to accessing non-local variables. If such routines access non-local variables (and thunks almost always will) then you must exercise care when calling such routines. Fortunately, thunks never cause indirect recursion (which is responsible for the crazy problems in the Recurse1 / Recurse2 example) so you can use the display to access any non-local variables appearing within the thunk.

Chapter Twelve (Part 4)

Table of Content

Chapter Twelve (Part 6) 

Chapter Twelve: Procedures: Advanced Topics (Part 5)
27 SEP 1996