Wednesday, November 21, 2007

Placing your code in the forge - Refining a technique

While preparing for this posting, I had the chance to review the code in my Parallel.pas unit.  I was trying to get my implementation of TNestedTask<T> to work properly with nested functions.  The problem I had is that with Win32 generics, you cannot have BASM (Built-in ASseMbler) blocks in the bodies of the methods in a parameterized type.  So I needed to find a way to call a function pointer that can take a type parameter as the result type.  The thing is that depending upon the result type, the calling code has to handle it very differently.  For instance, if the result type were a floating point value, the result would appear on the top of the floating point stack.  If it were a structure > 4 bytes in size, the caller has to pass in a hidden pointer parameter that tells the function where to place the result.  And finally, for simple ordinal types they're returned in a CPU register (EAX).  Even if the compiler were to allow assembler instructions in the bodies of methods in a parameterized type, there is no way for that code to properly "morph" into the correct sequence for handling all the various types of return types.

The compiler doesn't currently (aside from some internal research I've been doing in the compiler) support the notion of a nested procedure pointer, but it does support procedure pointers for any global procedure or function.  My solution was to create three little assembler functions to serve as helpers.  The Delphi compiler is smart enough to forgo the creation of a stack frame for most very simple procedures and functions.  This is especially true for any procedure or function that is nothing more than a pure block of assembly code.  Here's the little bits of code:

function GetFrame: Pointer;
asm
MOV EAX,[EBP]
end;

procedure PushFrame(AEBP: Pointer);
asm
XCHG EAX,[ESP]
PUSH EAX

end;

function PopFrame: Pointer;
asm
POP EDX
POP EAX
PUSH EDX

end;

The first function, GetFrame, will return the calling function's frame.  You should never call another function that calls this function if you're expecting to get the right results.  The other two functions are there for calling the nested procedure/function.  PushFrame takes the frame value stored off from a previous call to GetFrame and injects it onto the stack.  Since this is a function call, we have to swap it with the current top of the stack which is the return address for this function.  Then it is returned to the stack where the "RET" instruction that is generated by the compiler can go back to the right location.  PopFrame just undoes what PushFrame did.


Here's a little bit of code that demonstrates how to use them to call a nested proc:


type
TNestedFunction = function: Double;
TCalculatorFunction = (cAdd, cSubtract, cMultiply, cDivide);

function CallNestedFunction(ANestedFunction: TNestedFunction): Double;
var
LEBP: Pointer;
begin
LEBP := GetFrame;
PushFrame(LEBP);
Result := ANestedFunction;
PopFrame;
end;

procedure CalcFunction(Left, Right: Double; CalculatorFunction: TCalculatorFunction);

function Add: Double;
begin
Result := Left + Right;
end;

function Subtract: Double;
begin
Result := Left - Right;
end;

function Multiply: Double;
begin
Result := Left * Right;
end;

function Divide: Double;
begin
Result := Left / Right;
end;

var
Result: Double;
NestedFunc: TNestedFunction;
begin
case CalculatorFunction of
cAdd: NestedFunc := @Add;
cSubtract: NestedFunc := @Subtract;
cMultiply: NestedFunc := @Multiply;
cDivide: NestedFunc := @Divide;
end;
Result := CallNestedFunction(NestedFunc);
Writeln(Result);
end;

By using the assembler functions above, I can make CallNestedFunction a parameterized function like this:


type
TNestedFunction<T> = function: T;
TCalcClass = class
class function CallNestedFunction<T>(ANestedFunction: TNestedFunction<T>): T; static;
end;

class function TCalcClass.CallNestedFunction<T>(ANestedFunction: TNestedFunction<T>): T;
var
LEBP: Pointer;
begin
LEBP := GetFrame;
PushFrame(LEBP);
Result := ANestedFunction;
PopFrame;
end;

... Same as above ...

var
Result: Double;
NestedFunc: TNestedFunction<Double>;
begin
case CalculatorFunction of
cAdd: NestedFunc := @Add;
cSubtract: NestedFunc := @Subtract;
cMultiply: NestedFunc := @Multiply;
cDivide: NestedFunc := @Divide;
end;
Result := CallNestedFunction<Double>(NestedFunc);
Writeln(Result);
end;

In this way, I simply let the compiler figure out the right way to call through the procedure/function pointer and all those assembler functions do is to make sure the calling frame reference is on the stack.  Before someone asks, you should not wrap the PushFrame, call, PopFrame sequence into a try..finally block (I know it does kind of look like that should be done).  First of all, it will mess up the stack because exception frames use the stack to keep them linked together.  Secondly, it is wholly unnecessary.  If an exception occurs in the call to the nested proc, the system will unwind the stack and make sure each finally block and except block has the proper local frame.  How that all works is a rather complicated topic that would take up a lot of posts.  If you look at how the compiler generates code for a call to a nested procedure, it doesn't try to do any kind of exception wrapping either.  The extra frame value on the stack will be properly cleaned up.