Watch, Follow, &
Connect with Us

The Oracle at Delphi













Older Stuff



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.

Posted by Allen Bauer on November 21st, 2007 under CodeGear |



12 Responses to “Placing your code in the forge - Refining a technique”

  1. Thorsten Engler Says:

    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?

  2. Thorsten Engler Says:

    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 ?

  3. Thorsten Engler Says:

    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>?

  4. Anders E. Andersen Says:

    Uhmm.. Assembler?

    No thanks..

  5. Allen Bauer Says:

    Thorsten,

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

    Allen.

  6. Allen Bauer Says:

    Anders,

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

    Allen.

  7. PeterS Says:

    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

  8. Allen Bauer Says:

    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.

  9. PeterS Says:

    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.

  10. m. Th. Says:

    (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)

  11. Vsevolod Peretyatkov Says:

    Thank you!
    But why so little?

  12. Sofa Table Says:

    Hey
    Placing your code in the forge - Refining a technique
    Many Thanks, I’m very glad to peer your post
    An interesting dialogue is worth comment
    Thank you!

Leave a Comment

Server Response from: BLOGS2