Yield в CodeGear RAD Studio (Delphi 2007 for Win32)

в 7:37, , рубрики: Delphi, yield, Программирование

Привет!

Понадобилось внезапно портировать программу с C# на Delphi. В программе на C# активно использовался yield. Погуглив просторы интернета в надежде не заниматься изобретением велосипеда, удалось найти реализацию yield для Win32 на базе фиберов для Embarcadero Delphi 2009 и выше, но вот беда — требовалось сделать порт под CodeGear RAD Studio для версии Delphi 2007, в которой ещё отсутствовали обобщённые типы и анонимные методы. Менять версию Delphi на более позднюю было нельзя и поэтому пришлось переписать найденную реализацию yield для более ранней версии.

Взяв исходник юнита с реализаций yield для Delphi 2009 и выше за авторством Andriy Gerasika я его переделал для Delphi 2007.

Требовалось сделать поддержку yield только для Win32 и только для Delphi 2007, поэтому много менять не пришлось, только убрать generics, которых ещё не было в требуемой версии Delphi и сделать эмуляцию замыканий.

В оригинальном коде обобщённые типы (generics) были в стандартном виде:

type
  TYield<T> = procedure (Value: T) of object;
  TYieldProc<T> = reference to procedure(Yield: TYield<T>);

  TYieldEnumerator<T> = class
  {...}
  end;

  TYieldEnumerable<T> = record
  {...}
  end;

и посредством вывода типа через generic

TYieldEnumerator<T>

можно было задавать уже конкретные типы, как то

TYieldEnumerator<Integer>

TYieldEnumerator<Char>

и т.д., компилятор сам следил за корректным типом возвращаемого/возвращаемых значений, внутренних переменных и свойств. В Delphi 2007 требовалось как-то обойтись без generics, по возможности сохранив всю функциональность. Поэтому для хранения возвращаемого yield значения я решил использовать запись типа TVarRec из стандартного юнита System:

TVarRec = record
  case Byte of
    vtAnsiString: (VAnsiString: Pointer;);
    vtBoolean: (VBoolean: Boolean;);
    vtChar: (VChar: Char;);
    vtClass: (VClass: TClass;);
    vtCurrency: (VCurrency: PCurrency;);
    vtExtended: (VExtended: PExtended;);
    vtInt64: (VInt64: PInt64;);
    vtInteger: (VInteger: Integer;
                VType: Byte;);
    vtInterface: (VInterface: Pointer;);
    vtObject: (VObject: TObject;);
    vtPChar: (VPChar: PChar;);
    vtPointer: (VPointer: Pointer;);
    vtPWideChar: (VPWideChar: PWideChar;);
    vtString: (VString: PShortString;);
    vtVariant: (VVariant: PVariant;);
    vtWideChar: (VWideChar: WideChar;);
    vtWideString: (VWideString: Pointer;);
end;

которая в принципе может содержать любое значение. Для не POD типов (запией и классов) можно хранить указатель на них, TVarRec.VPointer, все остальные прекрасно хранятся и в записи TVarRec.

Так же пришлось изменить типы для

TYield<T> = procedure (Value: T) of object;
TYieldProc<T> = reference to procedure(Yield: TYield<T>);

убрав из обоих generic, а из второго reference to procedure за неимением анонимных методов в Delphi 2007:

TYield = procedure(aValue: TVarRec) of object;
TYieldProc = procedure(aYield: TYield; aYieldData: Pointer);

Переменная aYieldData типа Pointer в дальнейшем используется для эмуляции замыканий, которые тоже отсутствуют в Delphi 2007 (ведь надо же где-то хранить аргументы функции, из которой будет вызываться наш yield).

И изменил оригинальные классы с generics

TYieldEnumerator<T> = class
  private
    fYieldProc: TYieldProc<T>;
    fEOF: Boolean;
    fValue: T;
    property YieldProc: TYieldProc<T> read fYieldProc;
  private
    ThreadFiber: Cardinal;
    CallerFiber: Pointer;
    CalleeFiber: Pointer;
    FiberException: Pointer;
    procedure Execute; stdcall;
    procedure Yield(aValue: T);
  public
    constructor Create(const aYieldProc: TYieldProc<T>);
    destructor Destroy; override;
  public // enumerator
    function MoveNext: Boolean;
    property Current: T read fValue;
  end;

  TYieldEnumerable<T> = record
  private
    fYieldProc: TYieldProc<T>;
    property YieldProc: TYieldProc<T> read fYieldProc;
  public
    constructor Create(const aYieldProc: TYieldProc<T>);
    function GetEnumerator: TYieldEnumerator<T>;
  end;

сделав свои «generic» классы с учётом всех именений, наследованием от которых можно сэмулировать любые типы переменных:

TYieldEnumerator = class
  public
    constructor Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
    destructor Destroy; override;
  public
    function MoveNext: Boolean;
  private
    procedure Execute; stdcall;
    procedure Yield(aValue: TVarRec);
  protected
    m_yieldProc: TYieldProc;
    m_yieldData: Pointer;
    m_value: TVarRec;
  private
    m_threadFiber: Pointer;
    m_callerFiber: Pointer;
    m_calleeFiber: Pointer;
    m_fiberException: Pointer;
    m_done: Boolean;
  public
    property Current: TVarRec read m_value;
  end;

  TYieldEnumerable = record
  public
    constructor Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
    function GetEnumerator: TYieldEnumerator; inline;
  private
    m_yieldProc: TYieldProc;
    m_yieldData: Pointer;
  end;

Видно что они не так уж сильно отличаются от оригинальных, разве что прибавилось немного переменных и в конструкторах добавился параметр типа Pointer на данные для замыканий.

И в конструкторе TYieldEnumerator.Create слегка изменил получение «волокна» из текущего потока при самом первом вызове, добавил проверку на код ошибки $1e00 (актуально для Windows 7 и выше) и добавил бросание исключения при ошибке вызова ConvertThreadToFiber(nil):

 m_callerFiber := GetCurrentFiber;
  if (m_callerFiber = nil) or (Cardinal(m_callerFiber) = $1e00) then begin
    m_threadFiber := Pointer(ConvertThreadToFiber(nil));
    if m_threadFiber = nil then
      raise EAbort.CreateFmt('TYieldEnumerator.Create error: %d', [GetLastError]);
    m_callerFiber := GetCurrentFiber;
  end;

Вот в принципе и все модификации.

unit Yield_Win32;

interface

type
  TYield = procedure(aValue: TVarRec) of object;
  TYieldProc = procedure(aYield: TYield; aYieldData: Pointer);

  { TYieldEnumerator }
  TYieldEnumerator = class
  public
    constructor Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
    destructor Destroy; override;
  public
    function MoveNext: Boolean;
  private
    procedure Execute; stdcall;
    procedure Yield(aValue: TVarRec);
  protected
    m_yieldProc: TYieldProc;
    m_yieldData: Pointer;
    m_value: TVarRec;
  private
    m_threadFiber: Pointer;
    m_callerFiber: Pointer;
    m_calleeFiber: Pointer;
    m_fiberException: Pointer;
    m_done: Boolean;
  public
    property Current: TVarRec read m_value;
  end;

  { TYieldEnumerable }
  TYieldEnumerable = record
  public
    constructor Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
    function GetEnumerator: TYieldEnumerator; inline;
  private
    m_yieldProc: TYieldProc;
    m_yieldData: Pointer;
  end;

implementation

uses
  SysUtils,
  Windows;

procedure ConvertFiberToThread; external kernel32 name 'ConvertFiberToThread';

function GetCurrentFiber: Pointer;
asm
  mov eax, fs:[$10]
end;

{ TYieldEnumerator }
constructor TYieldEnumerator.Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
var
  _Execute: procedure of object; stdcall;
  __Execute: TMethod absolute _Execute;
begin
  inherited Create;
  m_callerFiber := GetCurrentFiber;
  if (m_callerFiber = nil) or (Cardinal(m_callerFiber) = $1e00) then begin
    m_threadFiber := Pointer(ConvertThreadToFiber(nil));
    if m_threadFiber = nil then
      raise EAbort.CreateFmt('TYieldEnumerator.Create error: %d', [GetLastError]);
    m_callerFiber := GetCurrentFiber;
  end;
  m_yieldProc := aYieldProc;
  m_yieldData := aYieldData;
  _Execute := Execute;
  m_calleeFiber := CreateFiber(0, __Execute.Code, __Execute.Data);
end;

destructor TYieldEnumerator.Destroy;
begin
  FreeMem(m_yieldData);
  DeleteFiber(m_calleeFiber);
  if m_threadFiber <> nil then
    ConvertFiberToThread;
  inherited;
end;

function TYieldEnumerator.MoveNext: Boolean;
begin
  if m_done then begin
    Result := False;
    Exit;
  end;
  m_done := True;
  SwitchToFiber(m_calleeFiber);
  if m_fiberException <> nil then
    raise TObject(m_fiberException);
  Result := not m_done;
end;

procedure TYieldEnumerator.Execute;
begin
  try
    m_yieldProc(Yield, m_yieldData);
  except
    m_fiberException := AcquireExceptionObject;
  end;
  SwitchToFiber(m_callerFiber);
end;

procedure TYieldEnumerator.Yield(aValue: TVarRec);
begin
  m_value := aValue;
  m_done := False;
  SwitchToFiber(m_callerFiber);
end;

{ TYieldEnumerable }
constructor TYieldEnumerable.Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
begin
  m_yieldProc := aYieldProc;
  m_yieldData := aYieldData;
end;

function TYieldEnumerable.GetEnumerator: TYieldEnumerator;
begin
  Result := TYieldEnumerator.Create(m_yieldProc, m_yieldData);
end;

end.

Пример PowersOf2.dpr из оригинального архива тоже изменил. В нём видно как дополнительный параметр конструктора aYieldData используется для эмуляции замыканий, как через вложенную функцию делаются замыкания и как методом наследования получать Yield классы для других переменных, в частности Integer. Остальные типы делаются по аналогии.

program PowersOf2;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Yield_Win32 in 'Yield_Win32.pas';

type
  {************************************}
  { create Yield enumerator of Integer }
  {************************************}

  { TYieldEnumeratorInteger }
  TYieldEnumeratorInteger = class(TYieldEnumerator)
  private
    function GetValue: Integer; inline;
  public
    property Current: Integer read GetValue;
  end;

  { TYieldEnumerableInteger }
  TYieldEnumerableInteger = record
  public
    constructor Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
    function GetEnumerator: TYieldEnumeratorInteger; inline;
  private
    m_yieldProc: TYieldProc;
    m_yieldData: Pointer;
  end;

{ TYieldEnumeratorInteger }
function TYieldEnumeratorInteger.GetValue;
begin
  Result := m_value.VInteger;
end;

{ TYieldEnumerableInteger }
constructor TYieldEnumerableInteger.Create(const aYieldProc: TYieldProc; aYieldData: Pointer);
begin
  m_yieldProc := aYieldProc;
  m_yieldData := aYieldData;
end;

function TYieldEnumerableInteger.GetEnumerator: TYieldEnumeratorInteger;
begin
  Result := TYieldEnumeratorInteger.Create(m_yieldProc, m_yieldData);
end;

function Power(Number: Integer; Exponent: Integer): TYieldEnumerableInteger;
  type
    PYieldData = ^TYieldData;
    TYieldData = record
      Number: Integer;
      Exponent: Integer;
    end;
  var
    p: PYieldData;

  procedure DoYield(Yield: TYield; pData: PYieldData);
  var
    i: Integer;
    v: TVarRec;
  begin
    v.VInteger := 1;
    for i := 1 to pData^.Exponent do begin
      v.VInteger := v.VInteger * pData^.Number;
      Yield(v);
    end;
  end;
begin
  GetMem(p, SizeOf(TYieldData));
  p^.number := Number;
  p^.exponent := Exponent;
  Result := TYieldEnumerableInteger.Create(@DoYield, p);
end;

var
  i: Integer;
begin
  try
    for i in Power(2, 9) do begin
      Writeln(i);
    end;
  Readln;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

Ну, вот вроде бы и всё. Как «говорил» перед завершением пример из Turbo Pascal 7.0 под названием bgidemo.pas:

Tha's all folks!

Оригинальная страница Andriy Gerasika с его реализацией yield для Delphi, с которой всё и началось.

Автор: pfemidi

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js