3

I am trying to migrate Delphi 5 code to Delphi XE7-WIN64 and I am facing a problem with mixed assembly code in the following block. Also I am asm newbie.

procedure IterateMenus(Func: Pointer; Menu1, Menu2: TElMenuItem);
var
  I, J: Integer;
  IIndex, JIndex: Byte;
  Menu1Size, Menu2Size: Integer;
  Done: Boolean;

  function Iterate(var I: Integer; MenuItem: TElMenuItem; AFunc: Pointer): Boolean;
  var
    Item: TMenuItem;
  begin
    if MenuItem = nil then Exit;
    Result := False;
    while not Result and (I < MenuItem.Count) do
    begin
      Item := MenuItem[I];
      if Item.GroupIndex > IIndex then Break;
      asm
        MOV     EAX,Item
        MOV     EDX,[EBP+8]
        PUSH    DWORD PTR [EDX]
        CALL    DWORD PTR AFunc
        ADD     ESP,4
        MOV     Result,AL
      end;
      Inc(I);
    end;
  end;

begin
  I := 0;
  J := 0;
  Menu1Size := 0;
  Menu2Size := 0;
  if Menu1 <> nil then
    Menu1Size := Menu1.Count;
  if Menu2 <> nil then
    Menu2Size := Menu2.Count;
  Done := False;
  while not Done and ((I < Menu1Size) or (J < Menu2Size)) do
  begin
    IIndex := High(Byte);
    JIndex := High(Byte);
    if (I < Menu1Size) then
      IIndex := Menu1[I].GroupIndex;
    if (J < Menu2Size) then
      JIndex := Menu2[J].GroupIndex;
    if IIndex <= JIndex then
      Done := Iterate(I, Menu1, Func)
    else
    begin
      IIndex := JIndex;
      Done := Iterate(J, Menu2, Func);
    end;
    while (I < Menu1Size) and (Menu1[I].GroupIndex <= IIndex) do
      Inc(I);
    while (J < Menu2Size) and (Menu2[J].GroupIndex <= IIndex) do
      Inc(J);
  end;
end;

I am trying to convert the asm block to purepascal since delphi x64 doesn't allow mixed code and asm is not programmer-friendly.

As far I understand address of Item is moved to EAX, Then I am not getting anything. Where did EBP come from? What is ESP and AL? The above code snippet is from ELmenus.pas from ELPack.

So basically what will be the PurePascal version of the asm codeblock?

For func I found this

procedure TElMenuItem.UpdateItems;

  function UpdateItem(MenuItem: TElMenuItem): Boolean;
  begin
    Result := False;
    IterateMenus(@UpdateItem, MenuItem.FMerged, MenuItem);
    MenuItem.SubItemChanged(MenuItem, MenuItem, True);
  end;

begin
  IterateMenus(@UpdateItem, FMerged, Self);
end;

procedure TElMenuItem.PopulateMenu;
var
  MenuRightToLeft: Boolean;

  function AddIn(MenuItem: TElMenuItem): Boolean;
  begin
    MenuItem.AppendTo(FHandle, MenuRightToLeft);
    Result := False;
  end;

begin    // all menu items use BiDiMode of their root menu
  {$ifdef VCL_4_USED}
  MenuRightToLeft := (FMenu <> nil) and FMenu.IsRightToLeft;
  {$else}
  MenuRightToLeft := false;
  {$endif}
  IterateMenus(@AddIn, FMerged, Self);
end;
AEonAX
  • 501
  • 12
  • 24
  • Can you show the function signature of "Func". I.e. what parameters does the function Func expect that is passes as parameter to IterateMenus? – Ville Krumlinde Dec 22 '14 at 11:57
  • Find in original code - what func is used in IterateMenus call? It is useful to know its prototype. [EBP+8] looks like I variable, AL is low byte of func return value (boolean). – MBo Dec 22 '14 at 11:59
  • @VilleKrumlinde Updated Question with "Func" – AEonAX Dec 22 '14 at 12:12

2 Answers2

4

I'm guessing the original programmer wrote the strange code to bypass Delphi not supporting local function pointers. (Which is a limitation that has annoyed me on occasion.)

The following should work. (Though I'm not that sharp on my asm to be certain. I would first test the asm code by stepping through the debugger in D5 to confirm that the line CALL DWORD PTR AFunc is passing the values I expect.)

1) Delcare a function pointer type and move the UpdateItem so that it's no longer local.

type
  TMenuOperation = function (AMenuItem: TElMenuItem): Boolean;
  //Or if you want to move the UpdateItem to a method of a class..
  //TMenuOperation = function (AMenuItem: TElMenuItem): Boolean of object;

2) Make the following changes:

procedure IterateMenus(Func: Pointer; Menu1, Menu2: TElMenuItem);
//becomes
procedure IterateMenus(Func: TMenuOperation; Menu1, Menu2: TElMenuItem);

//...

  function Iterate(var I: Integer; MenuItem: TElMenuItem; AFunc: Pointer): Boolean;
  var
    Item: TMenuItem;
  //becomes
  function Iterate(var I: Integer; MenuItem: TElMenuItem; AFunc: TMenuOperation): Boolean;
  var
    Item: TElMenuItem;

  //...

      Item := MenuItem[I];
      //becomes
      Item := MenuItem[I] as TElMenuItem;

3) And finally the assembler block becomes:

Result := AFunc(Item);
Disillusioned
  • 14,635
  • 3
  • 43
  • 77
3

That asm code is calling a method on Item. I'd say that whoever wrote the code needs their head examined. It's as if they don't know about method pointers.

The way I would tackle this is to look for the code that calls the function and see what is passed as the Func argument. That's what is being called by the asm block. Change the type of the Func argument to an appropriate procedural type and replace the asm block with a call to that method pointer.


OK, now I see that the original code is playing fast and loose with local procedures. What you should do is make Func be an anonymous method type:

Func: TFunc<TElMenuItem, Boolean>;

Then convert the local functions to be anonymous methods.


The usual approach to upgrading Delphi versions is to upgrade the third party libraries at the same time. Were you to do this then I guess you would not need to port the 15 year old library code.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    I'm guessing the original programmer wrote the strange code to bypass Delphi not supporting local function pointers. (Which is a limitation that has annoyed me on occasion.) – Disillusioned Dec 22 '14 at 13:20
  • I think you can do what I described no? – David Heffernan Dec 22 '14 at 13:25
  • Only if you move the function to implementation scope. (Although the issue may have been resovlved in a more recent version of Delphi - I'm unlable to test and confirm.) – Disillusioned Dec 22 '14 at 13:32
  • @Craig It's always been a mistake to treat local functions like that – David Heffernan Dec 22 '14 at 13:34
  • I understand that holding a local function reference for use outside the context of its "owner" routine would be a mistake. But it certainly is something feasible in the context of OP's question. (Just the compiler never permitted it.) I do like the suggestion to use anonymous methods though. – Disillusioned Dec 22 '14 at 13:39
  • 2
    "I'm guessing the original programmer wrote the strange code to bypass Delphi not supporting local function pointers." -- For the record here; The "original programmer" which started and promulgated that pattern was none other than Anders Hejlsberg, the developer of the Turbo Pascal compiler and lead architect for Delphi (now Distinguished Engineer at MS). Yes, it was working around the lack being able to reference a nested procedure. This technique was introduced in Turbo Pascal 6.0 in Turbo Vision. In many ways, this was the very first incarnation of a true "closure". – Allen Bauer Dec 22 '14 at 18:02
  • 1
    Turbo Pascal's Objects unit's TCollection.foreach iterator mechanism was based on a similar construct. The trick might originate there. FPC btw supports proper local function types. ("local" procedure directive) – Marco van de Voort Dec 22 '14 at 18:08
  • @Allen That doesn't change the fact that the code was always poor and always needless. – David Heffernan Dec 22 '14 at 18:09
  • David Heffernan: so how would you solve it without anonymous methods? – Marco van de Voort Dec 22 '14 at 18:11
  • @Marco Pass in a function that isn't a nested function. Either a method or a plain function. – David Heffernan Dec 22 '14 at 18:17
  • 1
    Really? How so? It solved a very real issue, namely being able to iterate over a sequence of items and perform an operation without having to set up some global state or passing in an opaque "context". Yes, *today* there are clearly better solutions, but at the time it was quite useful. To critique a solution only through the lens of the current state of the art, is disingenuous and the epitome of hubris. The electric light was quite an innovation for it's time, however following your logic, it was dim and power-hungry that was poor and needless (hyperbole intended for illustrative purposes). – Allen Bauer Dec 22 '14 at 18:40
  • @Allen All you need is a procedural type that receives the information that it needs. My argument is not that there are better solutions today, rather that there were better solutions then. – David Heffernan Dec 22 '14 at 18:44
  • 1
    Allen beat me to it, that requires global state, which makes reusing in multiple spots hard. (like the container type in TV it was copied from). IOW that is not a solution. The local procedure being able to access the parents frame for state is the whole deal. – Marco van de Voort Dec 22 '14 at 18:44
  • @Marco There's no need for global state. – David Heffernan Dec 22 '14 at 18:49
  • 1
    I know I always second guess Anders H. whenever I get a chance. – Nick Hodges Dec 22 '14 at 18:57
  • 1
    @David, The whole point was to make *all* the local state available. To do what you suggest would have required that the author decide what "state" is valid to pass along and how it should be packaged. By calling the nested procedure, the *user* gets to decide what state is important. It also makes the use of the iterator more about the problem they are trying to solve instead of the mechanics of packaging up and passing along the state. Yes, it was was playing fast-and-loose with things, but it did make things easier for developers. – Allen Bauer Dec 22 '14 at 19:00
  • @Allen My question is whether or not the return is worth the cost. The cost can be seen through the eyes of the poor developer who asked this question. – David Heffernan Dec 22 '14 at 19:05
  • 1
    @Nick I would judge the code on its merits rather than its author. Or do you think that Anders never made a mistake? – David Heffernan Dec 22 '14 at 19:06
  • FWIW, before anon methods I would have made the function type be a method of object. I would then have made a record containing the required state and a single method to be called by the iterator and passed each item. It's more verbose. But it is type safe and breaks no rules and does not take dependencies on compiler implementation detail. Even now with anon methods I sometimes use this technique because of the heap cost of anon methods. @Allen please consider adding scalable dynamic memory allocation. – David Heffernan Dec 22 '14 at 19:15
  • 1
    @David... the "Poor developer" is trying to translate the code, rather than call it. I'm explaining its purpose and the philosophy behind it, which can be helpful in understanding code. This is especially helpful in porting code between platforms. As I said, your solution forces the user of the iterator to conform to what *it* says is the valid state and the form it must be in. I even conceded that it was playing fast-and-loose with the specific implementation. That doesn't (shouldn't) diminish the intent and usefulness. – Allen Bauer Dec 22 '14 at 19:26
  • @Allen I understand and appreciate your comments. I can see why the fast-and-loose design choice was made. I don't think it's a good choice but you are right that it's easy to say that in 2014. – David Heffernan Dec 22 '14 at 19:35
  • 2
    @David, Had the tools been in the state then that they are today, I would clearly agree with your assessment and would have questioned the sanity of the author of that code. As in many things, ignoring or discounting history can inadvertently close one's mind to many valid solutions or even a platform on which to build the next great solution. We are not any smarter (in the aptitude sense) today than developers in the past; we just have the advantage of hindsight. "If I have seen further it is because I stand on the shoulders of giants" - Isaac Newton. – Allen Bauer Dec 22 '14 at 19:47
  • @Allen I still maintain that the code in the Q did not need to play fast-and-loose even when it was written. Hindsight is useful though! – David Heffernan Dec 22 '14 at 19:49
  • 1
    Of course the right solution is to make local procedures passable to procedure variables like (ISO) Pascal intended. But the TP compiler was probably too constrained, both memory and maintainability wise to implement the more seldomly used features. – Marco van de Voort Dec 22 '14 at 22:12
  • @AllenBauer Have you looked at the code in Vcl.Menus lately. It's really quite astonishing. I love the comment that says: *Thunk to to call a local procedure on a class. Kinda hokey if you ask ME.* I cannot understand the need for the `ITERATOR_OBJECTS` define. Surely it's better to do it the `ITERATOR_OBJECTS` way for all targets. Maintaining two separate implementations cannot be good for QA. – David Heffernan Dec 24 '14 at 14:34
  • 1
    @David Yes, I've looked at it. Most of what you are finding is the changes necessary for CLR and Windows 64bit. "ME" isn't a pronoun either. It's initials for Mark Edington, who was a member of the team for manyyears and ported that code to Win64. This is the comment: "IterateMenus implementation should be common. Win64 and CLR both use Iterator objects instead of local procedure for operations which require iterating over the menu items. Calling a local procedure in a class method requires special a ASM thunk" -- It is clear the code was clearly copied from VCL and only a few things changed. – Allen Bauer Dec 24 '14 at 18:40
  • 1
    @David, So, IOW, it is quite clear that we've recognized that calling a nested function as an iterator has been a liability when porting code to other platforms/processors. – Allen Bauer Dec 24 '14 at 18:42
  • @AllenBauer What I don't understand is why the `!ITERATOR_OBJECTS` branch remains. The `ITERATOR_OBJECTS` branch works for all targets. Maintaining two distinct implementations is just asking for maintenance bugs. A change is made to the Win32 branch but not made on the Win64 branch. Testing then fails to find the difference until after release. I'm sure you can picture the scenario. – David Heffernan Dec 24 '14 at 19:02
  • 1
    @David, Because there is code out there that already relies on the existing code. Also, never change tested code that has been working, and has worked for a while. Just because the code may offend one's sensibilities is no reason to change it. So, what "maintenance" issue is there, exactly? "Maintenance" assumes the code is broken (unlike your car, code doesn't "wear out"). – Allen Bauer Dec 24 '14 at 20:05
  • @Allen But the code was changed. All those conditionals were spliced in. That could easily have gone wrong. And the next time that part of the code has to be maintained, it has to be done twice. My judgement is that the decision to maintain two alternate implementations is more risky than a single. – David Heffernan Dec 24 '14 at 20:32
  • 1
    @Allen: Gnu Pascal (a GCC frontend) also had trouble with the TV thunking with the GCC backend, specially across major versions. – Marco van de Voort Dec 27 '14 at 14:57
  • @David: I never thought the CLR/native shared source very maintainable long term anyway. Indy10 still copies much too much data as a result severely hindering more serious usecases. – Marco van de Voort Dec 27 '14 at 15:08
  • @Marco In this case we have, needlessly, quite different implementations for 32 and 64 bit windows. – David Heffernan Dec 27 '14 at 15:49
  • 1
    @David: Delphi never provided support for it since (TP-)objects and FV were already not mainstream when it came out. Moreover what you can ifdef with 2 or 3 archs gets painful with 10+. FPC has been reducing asm in the RTL since mid 2003 and the constructs can be parametrized using a few simple builtins that return parent frames etc. The same primitives are also very useful in implementing the deeper exception handlers too. See FPC rtl, get_caller_* functions. – Marco van de Voort Dec 27 '14 at 17:53