Реализация co-routines в FreePascal
Долго думал что написать про структуры данных и алгоритмы и всё никак. То времени не хватало, то писать лень
mayton тут сподвиг меня проверить можно ли сделать co-routines для современной ОС без поддержки самой ОС (Очень надеюсь она нам мешать не будет)
заготовка класса (специально сделал её похожей на TThread):
конструктор иницализирующий стэк:
Переключать стэк будем следующим образом:
сохраняем все регистры
обмениваем стэковый регистр с RetAddr
восстанавливаем все регистры (но уже из другого стэкового блока)
возвращяемся к прошлой деятельности
для 32-битного кода
для 64-битного кода (надеюсь ничего не напутал, пока очень плохие знание по 64-битному ассемблеру, я обошёлся сохраненим регистров RBX, RDI, RSI, R12-R15, RBP за счёт предзаголовка в asm на входе на выходе совпадает с RSP (вещь скорее всего зависимая от компилятора))
Иницализировать стэк для первого запуска будем делать почти аналогичным образом:
сохраняем все регистры
обмениваем стэковый регистр с RetAddr
передаём управление InitCoRoutine
для 32-битного кода
для 64-битного кода
LocalSwitchInit и LocalSwitch имеют абсолютно одинаковые декларации и модели вызова stdcall (это важно для 32-битного режима), это позволяет использовать LocalSwitch
для выхода из первой инициализации
Для теста я написал вот такой примитивный код
будет вполне ожидаемый примитивный вывод
Ну и весь код модуля длясамых нетерпеливых тех, кто всегда сначала смотрит завершение :
PS: Весь код может использоваться без каких либо гарантий на правах "AS IS"
PS2: :желательно бы добавить протекцию стэкового буфера на его начальные адреса, что-бы хоть как-то ловить переполнение стэка
mayton тут сподвиг меня проверить можно ли сделать co-routines для современной ОС без поддержки самой ОС (Очень надеюсь она нам мешать не будет)
заготовка класса (специально сделал её похожей на TThread):
TCoRoutine = class(TObject) private // CoRoutine создана FStarted: Boolean; // действие завершено FFinished: Boolean; // действие прервано FTerminated: Boolean; // Возможная ошибка FFatalException: TObject; // Буфер для стэка Stack: array of byte; // Значение прошлого значения стэкового регистра RetAddr: PByte; strict protected // переключение стэка procedure Switch(); protected // основная процедура выполнения procedure Execute; virtual; public constructor Create(AStackSize: Integer = 0); destructor Destroy; override; property FatalException: TObject read FFatalException; property Finished: Boolean read FFinished; property Terminated: Boolean read FTerminated; // следующий расчёт function MoveNext(): Boolean; // прервать вычисления procedure Terminate(); end;
конструктор иницализирующий стэк:
+ |
Переключать стэк будем следующим образом:
для 32-битного кода
+ |
для 64-битного кода (надеюсь ничего не напутал, пока очень плохие знание по 64-битному ассемблеру, я обошёлся сохраненим регистров RBX, RDI, RSI, R12-R15, RBP за счёт предзаголовка в asm на входе на выходе совпадает с RSP (вещь скорее всего зависимая от компилятора))
+ |
Иницализировать стэк для первого запуска будем делать почти аналогичным образом:
для 32-битного кода
+ |
для 64-битного кода
+ |
LocalSwitchInit и LocalSwitch имеют абсолютно одинаковые декларации и модели вызова stdcall (это важно для 32-битного режима), это позволяет использовать LocalSwitch
для выхода из первой инициализации
procedure InitCoRoutine(Obj: TCoRoutine); stdcall; begin LocalSwitch(Obj); // инициализация завершена, возвращаем управление в конструктор try Obj.Execute; except on E: EAbort do; else Obj.FFatalException := TObject(AcquireExceptionObject()); end; Obj.FFinished := True; repeat LocalSwitch(Obj); until False; end;
Для теста я написал вот такой примитивный код
+ |
будет вполне ожидаемый примитивный вывод
stdout |
---|
1 2 3 4 5 6 7 8 9 10 |
Ну и весь код модуля для
+ |
PS: Весь код может использоваться без каких либо гарантий на правах "AS IS"
PS2: :желательно бы добавить протекцию стэкового буфера на его начальные адреса, что-бы хоть как-то ловить переполнение стэка