|
Навигация
|
Главная » Delphi Бесконечные генераторы значений на Delphi + АссемблерИсточник: habrahabr DCa В функциональных языках программирования есть возможность генерировать бесконечные последовательности значений (как правило чисел) и оперировать этими последовательностями. Реализуется это функцией, которая, не прерывая свою работу, генерирует значения одно за другим на основе своего внутреннего состояния.Но, к сожалению, в обычных языках нет возможности "вернуть" значения в место вызова не выходя из функции. Один вызов - один результат. Генераторы удобно было бы использовать совместно с возможностью Delphi по перечислению значений (GetEnumerator/MoveNext/GetCurrent). В этой статье мы создадим функцию-генератор (может даже бесконечную) и будем использовать ее с таким объектом для перечисления, чтобы всё работало прозрачно без необходимости вникать в реализацию. Причина невозможности вернуть значение без полного выхода из функции в том, что вызываемая функция использует тот же стек, что вызывающая. То есть, если вызванная функция сгенерирует очередное значение, то нужно найти способ вернуть управление программе для обработки. Главное, чтобы локальные данные вызванной функции не повреждались, и, когда потребуется, мы могли запустить ее с того же места, на котором прервали. Начнем с того, что для функции нужен отдельный стек. Ни Windows, ни процессор не могут запретить нам создать несколько стеков и время от времени переключаться между ними. Единственное, что мы потеряем, так это исключение Stack Overflow (конечно только если функция реально выйдет за пределы стека). Вместо него будет сгенерирован стандартный Access Violation. Подходящую для генератора функцию можете написать сами или возьмем что-нибудь знакомое и понятное, например генератор чисел Фибоначчи. Алгоритм, описанный в статье, не ограничивает нас в выборе функции, она может возвращать (генерировать) значения любых типов, а самое главное быть "бесконечной". "Бесконечная" функция генерирует значения, пока перечисление не прервано инструкцией break в теле цикла for-in. Так, например, можно искать файлы на диске, просматривая каждый и прерывая поиск, когда найден нужный. Преимущества этого способа, по сравнению с написанием собственного энумератора, в том, что функция может использовать локальные переменные (например TSearchRec совместно с функциями FindFirst/FindNext/FindClose). А по сравнению с сохранением всех значений во временный массив, генераторы потребляют меньше памяти, а в случае в поиском чего-либо, время в среднем сокращается вдвое (не тратится на формирование оставшейся части массива после найденного элемента). Представьте, что у нас есть такая функция: Функция генерирует числа и "отдает" их энумератору вызовом Generator.Yield.Когда значения выходят за разрядную сетку (второе условие после "and"), функция завершает свою работу. Заметьте, что если Generator.Yield вернет False, то функция тоже завершится. Это произойдет, если энумератор был уничтожен до того, как функция перечислила все числа до 2^64, то есть цикл for-in был прерван инструкцией break, exit или прерван исключением. Код для вывода чисел будет такой: Теперь нужно написать такой класс TGeneratorWithParamТогда нужно выделить данные, не зависящие от возвращаемого типа в отдельный класс, чтобы к этому классу был доступ из ассемблерного кода. Также нужно предусмотреть возможность завершить перечисление как со стороны программы (выход из цикла for-in), так и со стороны функции (выход из функции).Как только основная программа завершает цикл for-in, уничтожается объект TGenerator, в деструкторе которого происходит завершение функции: 1. Снова переключается контекст на выполнение функции. 2. Метод Yield на стороне функции-генератора возвращает False 3. Функция-генератор выходит из цикла и штатно завершает свою работу. Она может также правильно финализировать свои переменные, освободить ресурсы, и т. д. Проделаем с классом TGenerator один интересный трюк. Объявим метод GetEnumerator, а также методы MoveNext и GetCurrent (не забудем и про свойство Current). Метод GetEnumerator будет выглядеть так: Что здесь происходит? Функция возвращает в качестве объекта-энумератора сам объект-генератор.Сделано это для упрощения использования класса, а также исходя из такой особенности: если функция завершила свое выполнение после выхода из цикла for-in, то нет легкого способа снова запустить ее для следующего цикла. Поэтому многократное использование энумератора решено отменить. То есть: 1. Создали генератор 2. Получили энумератор (он же генератор) 3. Перечислили все значения 4. Уничтожили энумератор (он же генератор) Если нужно заново запустить функцию и перечислить все значения, то тогда генератор создается еще раз. Заметим, что если некий объект (или даже запись) в методе GetEnumerator возвращают объект, то он освобождается автоматически после выхода из цикла. То же относится и к интерфейсам и записям, но они удаляются корректо и в других случаях, а то, что это правило касается и объектов, немного нетипично для Delphi, в которой пока нет автоматического удаления созданных объектов (действительно пока, потому что сейчас идет работа над полноценной сборкой мусора, это видно в исходниках system.pas из XE3). При создании генератора нужно выполнить следующие действия: 1. Выделить память под стек. 2. Установить указатель SP. 2. Записать в стек начальные значения. Также для отладочных целей сразу после создания стека можно вписать такую строку: В деструкторе нужно остановить функцию и освободить память, отведенную для стека. Метод MoveNext будет вызывать функцию-генератор, получать от нее значение и проверять, продолжать ли перечисление (то есть не завершилась ли функция). Метод не слишком сложный, особенно, если учесть, сколько всего он делает. Следующий метод выглядит совсем просто. Всего три строки, одна из которых даже никогда не выполняется. Это метод Yield, который вызывается из функции, когда сгенерировано следующее значение. Основная задача этой функции вовсе не вернуть результат в функцию-генератор, а сохранить сгенерированное значение и вернуться в основной контекст, чтобы это значение могло быть обработано внутри цикла for-in, например выведено на экран. На самом деле после того, как стек заменен в процедуре Context.Leave, управление будет сразу передано на строку, следующую после вызова процедуры Context.Enter (в методе MoveNext). Метод Stop выполняется в одном случае: если к моменту, когда вызывается деструктор, функция еще не завершила генерацию значений. Поскольку функции надо финализировать переменные, освободить ресурсы и вообще нормально завершить работу, то нужно еще раз передать управление ей, сделав так, чтобы вызов метода Yield вернул False. Для переключения стека у нас будет отдельная процедура. Она будет использоваться для переключения в обе стороны.В ее задачи входит сохранение состояния в текущий стек и загрузка нового состояния из нового стека. Регистр EIP сохранять не нужно, потому что после выполнения инструкции ret (а она неявно присутствует в любой ассемблерной процедуре Delphi) процессор возвратится по адресу, который сохранен в стеке во время вызова процедур Enter и Leave. Так будет выглядеть процедура Enter: А так Leave: После завершения функции-генератора, выполнение будет передано на эту процедуру, так как ее адрес лежит в стеке ниже всего, заставляя функцию, по достижении инструкции ret, вернуться именно сюда для завершения генерации. Осталась только небольшая служебная функция, получающая значение регистра флагов: Тестировать модуль лучше в консольном приложении. Если использовать модуль в оконном приложение, то нужно удалить вывод на экран с помощью WriteLn.Системное программирование в UNIX средствами Free Pascal (исходники, электронная книга). CASE-средства. Общая характеристика и классификация. Многопоточность в своих приложениях. Часть 1. Synapse в Delphi. Отправка писем с вложениями. RAD Studio XE - вопросы и ответы. Главная » Delphi |
© 2024 Team.Furia.Ru.
Частичное копирование материалов разрешено. |