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.

11 comments:

  1. I noticed that you had to specify the as part of the CallNestedFunction call. Shouldn't the compiler be able to get that via type inference from the fact that the parameter is of type TNestedFunction?

    ReplyDelete
  2. oops.. I WordPress eat my .

    Let's try again:

    I noticed that you had to specify the as part of the CallNestedFunction call. Shouldn’t the compiler be able to get that via type inference from the fact that the parameter is of type TNestedFunction ?

    ReplyDelete
  3. arg.. the absence of a "Preview" button when posting comments is a real problem. May I put that up as a feature request?

    Third time is a charm (I hope):

    I noticed that you had to specify the <Double> as part of the CallNestedFunction call. Shouldn’t the compiler be able to get that via type inference from the fact that the parameter is of type TNestedFunction<Double>?

    ReplyDelete
  4. Thorsten,

    Well it is a pre-release compiler. Not everything is completed yet.

    Allen.

    ReplyDelete
  5. Anders,

    What is the problem with assembler? There are just some thing you cannot do in higher level code.

    Allen.

    ReplyDelete
  6. To get this to work you will have to check 'Stack frames' in compiler options.

    Is this something you're doing in preparation for anonymous functions?

    btw
    I'm really looking forward to having generics in Delphi win32

    ReplyDelete
  7. PeterS,
    This actually works without doing that because the compiler will automatically generate a frame if your nested functions access the local variables and/or parameters of the outer function. If the nested function does not, then it will not need to even access the frame value so its value is irrelevant.
    Allen.

    ReplyDelete
  8. Weird I gave tried it and without stack frames I got incorrect values.

    The test project I did was a win32 app in d2007 with just a button and an editcontrol.

    I'm not at that computer right now and I'm to lazy to do another test project, I'll have to check it again after the weekend.

    ReplyDelete
  9. (answering to both blog posts)

    Having the current compiler constraints looks fine to me. Imho, it's a nice thing to wrap the asm part in separate code blocks. The steps which follows now are, at a quick glance:

    - make the engine more general IOW to allow uniform calling of all code block variants (regular procedures/functions, nested ones and methods (ie. 'of object' types) ) - perhaps using overloading? - and put this in VCL / RTL.

    It would be very nice to write something like:


    Unit ToolsDB;

    procedure ScanSelection(aGrid: TMyDBGrid; closure aCodeBlock);
    var
    i: integer;
    begin
    with aGrid do
    for i:=0 to SelectedList.Count-1 do
    begin
    DataSource.DataSet.Bookmark:=Selected[i];
    aCodeBlock; //do an action at every selected row - now we can do it (in a limited manner) with TAction...
    end;
    end;



    - allow anonymous functions

    - explore interface payload (speed, memory at creation/destruction) vs a custom built 'records with methods' or full CG way of manage lifetime (perhaps isn't worth, but just a hint - with a custom built engine should be faster - no try/finally, no inheritance, asynchronous destruction in a separate thread etc.

    - for tasks, allow to specify in an easy manner (as a parameter at call time, perhaps) the thread priority

    - compiler warning when someone messes with UI in a secondary thread. This can happen very easy now. Consider the code from your previous blog post. What's happening if in TForm1.CalcResult we'll add in the 'for' cycle a ShowMessage(IntToStr(Result)); //just for debug?

    - delayed results (aka futures & promises) - it would fit very nice with your IntTask.Value from your previous post.

    - speaking above about TAction: if I do a TMyAction = class (TAction) in code I cannot assign code _easily_ to it. (It needs another method somewhere, cluttering that class definition and after this an assignment). Imho, it would be much better if we have an in-line assignment, something like:


    interface

    TMyAction = class (TAction)
    ...
    end;
    ...

    implementation

    TMyAction.OnExecute = (
    var
    nDate: TDateTime; //shows a var block

    begin
    nDate:=Now;
    ShowMessage('Now is '+DateTimeToStr(nDate));
    end;
    ); //the code block



    - ...and, of course, a free download link for your compiler :-)
    (I'm joking but not so much, because, imho, it's really needed this, not so badly for generics but for the Unicode hurdle - there are to many cases/combinations to see if everything is ok in our code - an early beta (with all disclaimers, warnings etc. etc.) would be very welcomed, imho)

    ReplyDelete
  10. Vsevolod PeretyatkovJanuary 25, 2010 at 11:55 PM

    Thank you!
    But why so little?

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.