delphidwscript

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

Solution

  • 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