8

Consider the following test-case:

{ CompilerVersion = 21 }
procedure Global();

  procedure Local();
  begin
  end;

type
  TProcedure = procedure ();
var
  Proc: TProcedure;
begin
  Proc := Local;  { E2094 Local procedure/function 'Local' assigned to procedure variable }
end;

At line 13 compiler emits message with ERROR level, prohibiting all of the cases of such local procedures usage. "Official" resolution is to promote Local symbol to the outer scope (ie: make it a sibling of Global) which would have negative impact on code "structuredness".


I'm seeking the way to circumvent it in most graceful manner, preferably causing compiler to emit WARNING level message.

Free Consulting
  • 4,300
  • 1
  • 29
  • 50
  • 1
    I'm curious why you'd want a pointer to a local function? There is no graceful manner to force the compiler to allow you this. But perhaps you could find out what code they emit to call a local function, and emit it yourself, with a pointer to that local function, as a Pointer typed variable instead of TProcedure type. – Warren P Feb 18 '11 at 18:59
  • IIRC, Andreas Rejbrand replied with `^untyped` workaround (marginally, yet still valid), but deleted his answer. – Free Consulting Feb 18 '11 at 19:15
  • The problem is that Andreas realised that his answer won't work as soon as you try to call the function outside the context of the procedure in which it is defined. And that sure is what you want to do. If you only called it in the context of the procedure in which it is defined there would be no question. – David Heffernan Feb 18 '11 at 19:27
  • Are you sure, @David? The compiler needs to do special things with the stack when it calls a local function so that it can access its parent's variables and parameters. The compiler doesn't do that if it thinks it's calling an ordinary function (which is what it would think, if you called the `Proc` variable). So, even if you still call the local function within the context of its parent, it still might not work. – Rob Kennedy Feb 18 '11 at 19:47
  • 1
    @Rob The answer that Andreas deleted had a function that did nothing more than call ShowMessage. The state of the stack probably didn't matter. I was trying not to be too unkind about it, especially as he deleted in when he realised the mistake. Anyway, you can see from my answer how I think it ought to be done!! – David Heffernan Feb 18 '11 at 19:51
  • 2
    @David, @Rob: The main thing that made my realise that my code is no good is not that the local procedure cannot be called from outside its parent (who would even consider something *that* pathological?), but rather than you get strange errors (memory corruption) if you try to alter the local variables of `global` inside `local` (which you very often do) when you use "my" code to call `local` from within `global`. – Andreas Rejbrand Feb 18 '11 at 20:03
  • @Andreas Rejbrand, unfortunately, i didnt have a chance to analyse your snippet, clicked on reorder link and *poof* answer gone :-( – Free Consulting Feb 18 '11 at 20:30
  • 1
    @Worm Regards: I have restored it (temporarily, at least) so that everyone can see it while talking about it. – Andreas Rejbrand Feb 18 '11 at 20:32
  • @Andreas - Would it be possible to call it from the outside of the parent procedure? – Sertac Akyuz Feb 18 '11 at 20:36
  • @Sertac: Yes, I just tried that, and it worked. But I do not know what subtle consequences there are. I wouldn't do it. (But of course, the `local` procedure cannot deal with the local variables of `global` in such a case. But a simple `ShowMessage` worked.) – Andreas Rejbrand Feb 18 '11 at 20:38
  • @Sertac: it would if you do it differently. See the example in my answer. – Mason Wheeler Feb 18 '11 at 20:39
  • @Mason, @Andreas - I see, thanks. I vaguely remember a question involving passing the address of a nested procedure for a winapi callback to be able to access form fields, if I'm not mistaken it amounts about the same thing.. – Sertac Akyuz Feb 18 '11 at 21:02
  • 1
    @Sertac: Ouch! That sounds like an ugly hack. Just thinking of what it would involve hurts my head! :P – Mason Wheeler Feb 18 '11 at 21:14

4 Answers4

9

Your best bet is to declare it as reference to procedure using the new anonymous methods feature and then you can keep everything nicely encapsulated.

type
  TProc = reference to procedure;

procedure Outer;
var
  Local: TProc;
begin
  Local := procedure
    begin
      DoStuff;
    end;
  Local;
end;

This gets around the issues that Mason describes by capturing any variables local to the anonymous function.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Exactly. If you're really interested in the details of how capture works, you can find an explanation [here.](http://tech.turbu-rpg.com/13/whats-in-a-name-less-method) – Mason Wheeler Feb 18 '11 at 19:04
  • @Mason Barry Kelly blogged about it quite extensively too – David Heffernan Feb 18 '11 at 19:07
  • @Jeroen Thank you. The poster stated dcc32 version of 21 which is D2010 I believe! – David Heffernan Feb 18 '11 at 20:26
  • 2
    @Jeroen: You mean D2009 and up, don't you? (Though I wouldn't recommend using 2009 if a newer version is available.) – Mason Wheeler Feb 18 '11 at 20:37
  • @David, God no! I run Delphi 2009 Professional. – Andreas Rejbrand Feb 18 '11 at 20:43
  • @David Heffernan @Jeroen Pluimers sorry, i was lazy and didnt run the testcase on older compilers, hence CompilerVersion remark. – Free Consulting Feb 18 '11 at 20:48
  • @Mason I wasn't sure how stable this was in D2009, as the new compiler stuff there was a bit flaky, especially in the original release. @Andreas thanks for confirming it works in D2009. – Jeroen Wiert Pluimers Feb 18 '11 at 21:39
  • Is the anonymous procedure workaround nestable? True nested procedure types (as in original Pascal) are nestable. Of course procedure types become dual pointer then. – Marco van de Voort Feb 19 '11 at 13:30
  • @marco I wouldn't call it a workaround, it's a first class solution. But I don't quite understand your question. Could you elaborate? – David Heffernan Feb 19 '11 at 13:38
  • Define local2 within local, and each nested one accessing the variables of the outer ones. – Marco van de Voort Feb 19 '11 at 17:16
  • @Marco Did you already know the answer to that question? It's an AV! The innermost function needs to access the outer most variable for the AV to occur. If the variable is passed around as a parameter to the innermost function then its fine. – David Heffernan Feb 19 '11 at 17:22
  • No I did not know, but suspected. Btw FPC supports the old (pre Borland) Pascal way with boring out procedures and double pointer procedure types. For Mac Pascal compatibility. (devel versions only) Still it is strange that a modern thing like Delphi can't pass early eighties versions in this regard – Marco van de Voort Feb 19 '11 at 22:46
  • @Marco Well, I suppose I ought to submit a QC (after checking that one doesn't already exist)! Not that I could ever imagine wanting to write such code. – David Heffernan Feb 19 '11 at 22:47
  • It is mostly used for visitor patterns on a procedural level. Build the context in the parent procedure, call the iterator with the local procedure (that then can access the data/context via the access to the parent's var). Alternately, it can be used for procedure types that both accept local and global (parent frm=nil) procedures. E.g. Turbo Vision direly missed it. – Marco van de Voort Feb 28 '11 at 14:09
  • I submitted QC#91876, the AV described above. – David Heffernan Mar 02 '11 at 10:52
5

Here's why you can't do it:

type
  TProcedure = procedure ();

function Global(): TProcedure;
var
  localint: integer;

  procedure Local();
  begin
    localint := localint + 5;
  end;

begin
  result := Local;
end;

Local procedures have access to the outer routine's variable scope. Those variables are declared on the stack, though, and become invalid once the outer procedure returns.

However, if you're using CompilerVersion 21 (Delphi 2010), you've got anonymous methods available, which should be able to do what you're looking for; you just need a slightly different syntax.

Mason Wheeler
  • 82,511
  • 50
  • 270
  • 477
  • Yes, i know the cases. Actually, extending `Local` lifetime over its parent scope brings the trouble. Yet, there are the cases when usage is pefrectly valid. – Free Consulting Feb 18 '11 at 20:34
  • BTW, `localint` is uninitialized. Solving it with writable typed constant creates homebrewn closure :) – Free Consulting Feb 18 '11 at 20:37
  • 2
    @Worm: Yeah, almost, except you only get one of them instead of a new one each time the procedure runs. :) – Mason Wheeler Feb 18 '11 at 20:43
  • 1
    I have assigned local variable to 10 and printed it from local procedure. It printed 0. This stuff may be just broken, avoid it. – OCTAGRAM Apr 17 '23 at 07:44
1

If one really needs to use local procedures in D7 or earlier one could use this trick:

procedure GlobalProc;
var t,maxx:integer; itr,flag1,flag2:boolean; iterat10n:pointer;
    //Local procs:
    procedure iterat10n_01;begin {code #1 here} end;
    procedure iterat10n_10;begin {code #2 here} end;
    procedure iterat10n_11;begin {code #1+#2 here} end;
begin
    //...
    t:=ord(flag2)*$10 or ord(flag1);
    if t=$11 then iterat10n:=@iterat10n_11
      else if t=$10 then iterat10n:=@iterat10n_10
        else if t=$01 then iterat10n:=@iterat10n_01
          else iterat10n:=nil;
    itr:=(iterat10n<>nil);
    //...
    for t:=1 to maxx do begin
        //...
        if(itr)then asm
            push ebp;
            call iterat10n;
            pop ecx;
        end;
        //...
    end;
    //...
end;

However the problem is that adress-registers could differ on different machines - so it's needed to write some code using local proc call and look via breakpoint which registers are used there...

And yeah - in most real production cases this trick is just some kind of palliative.

Markus_13
  • 328
  • 2
  • 14
0

For the records, my homebrewn closure:

{ this type looks "leaked" }
type TFunction = function (): Integer;

function MyFunction(): TFunction;

  {$J+ move it outside the stack segment!}
  const Answer: Integer = 42;

  function Local(): Integer;
  begin
    Result := Answer;
    { just some side effect }
    Answer := Answer + Answer div 2;
  end;

begin
  Result := @Local;
end;


procedure TForm1.FormClick(Sender: TObject);
var
  Func: TFunction;
  N: Integer;
begin
  { unfolded for clarity }
  Func := MyFunction();
  N := Func();
  ShowMessageFmt('Answer: %d', [N]);
end;
Free Consulting
  • 4,300
  • 1
  • 29
  • 50
  • 2
    @Worm If remember correctly, local functions have a different stack setup than global functions, so you need to be careful with this trick. – Jeroen Wiert Pluimers Feb 18 '11 at 21:41
  • @Jeroen Pluimers, stack? `Answer` symbol resides on data segment now. – Free Consulting Feb 18 '11 at 22:38
  • @Worm: The problem with this is that there is only one global `Answer` symbol now. If you used a real closure you'd get a new one each time you ran MyFunction, which means that different invocations from different places wouldn't stomp each other's state the way they would now. – Mason Wheeler Feb 18 '11 at 22:47
  • @Mason Wheeler, it is not global, but, sure, there is only one `Answer` data bound to `Local` routine, exactly what i was demonstrating. – Free Consulting Feb 18 '11 at 23:06
  • 3
    There's not any real need to resort to a filthy hack like this when the compiler will do it in a clean safe way for you. – David Heffernan Feb 18 '11 at 23:06
  • @David Heffernan, ok, prove this given example "unsafe", please. – Free Consulting Feb 18 '11 at 23:12
  • 2
    The point is that there's no need to do it this way. Even if it works, why do it? The closure functionality is how you are supposed to do it? Why fight against that? – David Heffernan Feb 18 '11 at 23:13
  • @Worm: I wasn't talking about `Answer`, I was talking about `Local`. Since `Local` is not global, the compiler can perform a number of shortcuts for setting up the stack when calling it, that can vary by compiler version, so it is inherently usafe. This, and a safe way being available (closures through procedure references), make me totally agree with @David: don't do this hack. – Jeroen Wiert Pluimers Feb 19 '11 at 13:50
  • @Jeroen Pluimers, care to provide more technical details on that? – Free Consulting Feb 19 '11 at 16:03
  • @Worm: The beste source for that right now is Barry Kelly; you can verify yourself by carefully analyzing the disassembly of various method signatures in various Delphi versions. I'd recommend testing one version in the range of D5-D7, one version in the range of D2005-D2007, and one version in the range of D2009-DXE. Testing should include signatures having between zero and 5 parameters (so you cover parameters on the stack and in registers), reference and value parameters, with and without function result, and various calling conventions. – Jeroen Wiert Pluimers Feb 20 '11 at 11:02