jonjbar
jonjbar

Reputation: 4066

How to expose a dynamic array of records in DWScript?

I've declared a simple record type in a DWScript unit:

TSampleRecord = record
  name: string;
end;

How can I expose such an array from the Delphi application to the script ? For example, the following method in the Delphi application:

// Delphi side
function GetSampleRecordArray(): array of TSampleRecord;

Must be accessible from a script:

// Script side
var myArray: array of TSampleRecord;
myArray := GetSampleRecordArray();

Upvotes: 1

Views: 490

Answers (1)

Peter Wolf
Peter Wolf

Reputation: 3830

Before registering a function in script that returns dynamic array of records you need to:

  1. register record type
  2. register dynamic array of that type

TdwsUnit has helper method ExposeRTTIDynamicArray to expose dynamic arrays for scripting. The method is introduced by helper class TdwsRTTIExposer in unit dwsRTTIExposer. Unfortunately this works only with dynamic arrays of some basic types and not records or objects. Here's a simple class that helps you registering a record type and a dynamic array for the lifetime of TdwsUnit instance:

uses
  System.SysUtils, System.Classes, System.Rtti, dwsComp, dwsExprs, dwsInfo,
  dwsErrors, dwsRTTIExposer;

type
  TDwsDynamicArrayExposer<T: record> = class(TComponent)
  strict private
    FRttiType: TRttiType;
    FDwsSymbol: TdwsSymbol;
    FDwsArray: TdwsArray;
    function GetDwsUnit: TdwsUnit;
  strict protected
    class var RTTIContext: TRttiContext;
    property DwsUnit: TdwsUnit read GetDwsUnit;
    property RttiType: TRttiType read FRttiType;
    property DwsSymbol: TdwsSymbol read FDwsSymbol;
    property DwsArray: TdwsArray read FDwsArray;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure SetInfo(const Info: IInfo; const Values: TArray<T>);
  end;

constructor TdwsDynamicArrayExposer<T>.Create(AOwner: TComponent);
begin
  if not (AOwner is TdwsUnit) then
    raise EArgumentException.Create('Owner must be instance of TdwsUnit.');
  inherited;
  FRttiType := RTTIContext.GetType(TypeInfo(T));
  FDwsSymbol := DwsUnit.ExposeRTTI(FRttiType.Handle);
  FDwsArray := DwsUnit.Arrays.Add;
  FDwsArray.DataType := FDwsSymbol.Name;
  FDwsArray.Name := FDwsSymbol.Name + 'Array';
  FDwsArray.IsDynamic := True;
end;

destructor TdwsDynamicArrayExposer<T>.Destroy;
begin
  if Assigned(DwsUnit) and (not (csDestroying in DwsUnit.ComponentState)) then
  begin
    // in case something went wrong in constructor
    FDwsArray.Free;
    FDwsSymbol.Free;
  end;
  inherited;
end;

function TdwsDynamicArrayExposer<T>.GetDwsUnit: TdwsUnit;
begin
  Result := TdwsUnit(Owner);
end;

procedure TdwsDynamicArrayExposer<T>.SetInfo(const Info: IInfo; const Values: TArray<T>);
var
  Index: Integer;
begin
  Info.Member['Length'].ValueAsInteger := Length(Values);
  for Index := 0 to Length(Values) - 1 do
    TdwsRTTIInvoker.AssignRecordFromValue(Info.Element([Index]),
      TValue.From<T>(Values[Index]), RttiType);
end;

The class also provides conveniece method SetInfo for initializing IInfo instance (parameter, variable, result variable, ...) from a dynamic array.

Now you can define specialized exposer for your TSampleRecord and register function GetSampleRecordArray within the DWS unit:

type
  TSampleRecord = record
    Name: string;
  end;

  TArrayOfSampleRecordExposer = class(TdwsDynamicArrayExposer<TSampleRecord>)
  strict private
    FGetSampleRecordArrayFunction: TdwsFunction;
    procedure OnGetSampleRecordArrayEval(Info: TProgramInfo);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

function GetSampleRecordArray: TArray<TSampleRecord>;
begin
  SetLength(Result, 3);
  Result[0].Name := 'Name 0';
  Result[1].Name := 'Name 1';
  Result[2].Name := 'Name 2';
end;

constructor TArrayOfSampleRecordExposer.Create(AOwner: TComponent);
begin
  inherited;
  FGetSampleRecordArrayFunction := DwsUnit.Functions.Add;
  FGetSampleRecordArrayFunction.Name := 'GetSampleRecordArray';
  FGetSampleRecordArrayFunction.ResultType := DwsArray.Name;
  FGetSampleRecordArrayFunction.OnEval := OnGetSampleRecordArrayEval;
end;

destructor TArrayOfSampleRecordExposer.Destroy;
begin
  if Assigned(DwsUnit) and (not (csDestroying in DwsUnit.ComponentState)) then
    FGetSampleRecordArrayFunction.Free;
  inherited;
end;

procedure TArrayOfSampleRecordExposer.OnGetSampleRecordArrayEval(Info: TProgramInfo);
begin
  SetInfo(Info.ResultVars, GetSampleRecordArray);
end;

Finally you register the Delphi function by instantiating TArrayOfSampleRecordExposer:

Dws := TDelphiWebScript.Create(nil);
DwsUnit := TdwsUnit.Create(Dws);
DwsUnit.UnitName := 'Unit1';
DwsUnit.Script := Dws;
// one-time registration
TArrayOfSampleRecordExposer.Create(DwsUnit);

// ...

DwsProgram := Dws.Compile(
  'var SampleRecords := GetSampleRecordArray;'#13#10 +
  'for var SampleRecord in SampleRecords do'#13#10 +
  '  Println(SampleRecord.Name);');
if DwsProgram.Msgs.Count > 0 then
  raise Exception.Create(DwsProgram.Msgs.AsInfo);
DwsProgramExecution := DwsProgram.Execute;

This should produce the output (DwsProgramExecution.Result.ToString):

Name 0
Name 1
Name 2

Upvotes: 3

Related Questions