4

In have a Delphi application running a DWS script. The Delphi application exposes an object instance, let's call it "MyApplication", to the script. The exposed object has a method which has one argument being a procedure.

Fundamentally by goal is to have a Delphi method doing some computation and stopping this computation when a callback procedure says it is done. The callback procedure is Inside the script.

I have implemented this by passing the name of the callback function as a string. It works nicely except that no type checking is done at script compile time. I would like to pass an actual procedure so that the script compiler can catch any error at compile time.

How to do that?

To help the reader understand what I mean, I show some - not working - code:

First a simplified verion of the Delphi side:

Interface
type
    TAppCheckProc = procedure (var Done : Boolean);

TMyApplication = class(TPersistent)
published
    procedure Demo(CheckProc : TAppCheckProc);
end;

Implementation

TMyApplication.Demo(CheckProc : TAppCheckProc);
var
    Done : Boolean;
begin
    Done := FALSE;
    while not Done do begin
        // Some more code here...
        CheckProc(Done);
    end;
end;

Second, on the script side I have this (Also simplified):

procedure CheckProc(
    var Done : Boolean);
var
    Value : Integer;
begin
    DigitalIO.DataIn(1, Value);
    Done := (Value and 8) = 0;
end;

procedure Test;
begin
    MyApplication.Demo(CheckProc);
end;

It is likely that Demo method argument should be declared differently and should be called differently. That is the question...

Edit: Removed extra Tag argument (Error when simplified the code, this is not the question).

fpiette
  • 11,983
  • 1
  • 24
  • 46

1 Answers1

6

I put this together quickly and it works. It gives a compile error when the parameters for the callback aren't correct. You need to create a delegate and use that as the type.

Example using standalone function

dwsUnit is the TdwsUnit that is being used for the custom Delphi methods.

procedure TMainForm.FormCreate(Sender: TObject);
var
  delegate: TdwsDelegate;
  func: TdwsFunction;
  parm: TdwsParameter;
begin
  // Create a delegate
  delegate := dwsUnit.Delegates.Add;
  delegate.Name := 'TAppCheckProc';
  parm := delegate.Parameters.Add;
  parm.Name := 'Done';
  parm.DataType := 'Boolean';
  parm.IsVarParam := True;

  // Create our function and link it to the event handler
  func := dwsUnit.Functions.Add;
  func.Name := 'Demo';
  func.OnEval := dwsUnitFunctionsDemoEval;
  parm := func.Parameters.Add;
  parm.Name := 'CheckProc';
  parm.DataType := 'TAppCheckProc';
end;

The script that I used to test this is as follows:

procedure CheckProc(
    var Done : Boolean);
begin
  if Done then
    SayHello('World');
end;

Demo(CheckProc);

If I change the parameter from a Boolean to an Integer I get a compile error on the script.

My event handler for completeness looks like this:

procedure TMainForm.dwsUnitFunctionsDemoEval(info: TProgramInfo);
begin
  info.Vars['CheckProc'].Call([True]);
end;

Example using classes

If you want to use classes then the code would be slightly different. Assuming you are using the CustomClasses demo and wanted to use the TEarth class then this would be the code to create the method and delegate.

procedure TMainForm.FormCreate(Sender: TObject);
var
  delegate: TdwsDelegate;
  method: TdwsMethod;
  parm: TdwsParameter;
begin
  // Create a delegate
  delegate := dwsUnit.Delegates.Add;
  delegate.Name := 'TAppCheckProc';
  parm := delegate.Parameters.Add;
  parm.Name := 'Done';
  parm.DataType := 'Boolean';
  parm.IsVarParam := True;

  // Create our method and link it to the event handler
  method := TdwsClass(dwsUnit.Classes.Symbols['TEarth']).Methods.Add;
  method.Name := 'Demo';
  method.OnEval := dwsUnitFunctionsDemoEval;
  parm := method.Parameters.Add;
  parm.Name := 'CheckProc';
  parm.DataType := 'TAppCheckProc';
end;

The script to use this would be:

procedure CheckProc(
    var Done : Boolean);
begin
  if Done then
    PrintLn('Called with true')
  else
    PrintLn('Called with false');
end;

var earth: TEarth;
earth:=TEarth.Create;
earth.Demo(CheckProc);

The event handler is as follows:

procedure TMainForm.dwsUnitFunctionsDemoEval(info: TProgramInfo; ExtObject:
    TObject);
begin
  info.Vars['CheckProc'].Call([True]);
end;

As with the standalone version, changing the script parameter type produces a "compiler" error.

As SpeedFreak indicates in the comments. You can also do this through the IDE designer instead of in code.

Graymatter
  • 6,529
  • 2
  • 30
  • 50
  • Thanks a lot. This is nearly what I need. You exposed Demo as a standard function. I need to expose a method of a Delphi side class (TMyApplication.Demo in my sample code). The corresponding Delphi side object instance is exposed using ExposeInstanceToUnit and the class itself is exposed using ExposeRTTI. Can I ask you to change a little bit your code to make Demo a method of a class named TMyApplication ? – fpiette Oct 21 '15 at 10:54
  • The code for a method is practically the same. The `OnEval` event handler just has an extra parameter: `ExtObject: TObject`. Apart from that _I_ would use Info.Params[] instead of Info.Vars[]. I don't think you can expose a Delphi method taking a delegate using RTTI. I believe you need to either do it in code, as in @Graymatter's example, or at design time with a `TdswUnit`. – SpeedFreak Oct 21 '15 at 14:38
  • @Graymatter I used your code in my full application and it works, of course :-) It's a little bit vebose because ExposeRTTI doesn't work in that cas so I've declared by code - as you showed - the methods with his related datatypes and argument. Not a real problem, just a bunch of lines to write. Maybe one day ExposeRTTI would be able to automate the process. Thanks a lot for you excellent help. I marked your answer as the good answer. – fpiette Oct 22 '15 at 09:58
  • @Graymatter Unfortunately, I found an issue. When the callback procedure try to increment a global variable, the assert located in dwsMagicExprs.TIncVarFuncExpr.DoInc fails. To verify this, add an integer variable named X and add Inc(X) in procedure CheckProc (I used dwsCript CustomClasses demo to check). As far as I can understand, there is something missing in the access to global variables. – fpiette Mar 21 '16 at 11:48
  • @fpiette I have been a bit busy so I haven't looked at SO for a while other than a cursory glance. Did you figure out what was happening here? – Graymatter Apr 21 '16 at 21:49