А. Скробов
Использование фиберов в перечислителях
Скачать
исходный код к статье
Перечислители
Очень часто бывает необходимо проделать однообразные действия над семейством объектов, например окнами на экране, файлами в каталоге, или элементами в коллекции. Назовём объект, владеющий таким семейством, "поставщиком", а объект, выполняющий над ним действия – "потребителем". Если эти два объекта создаются и поддерживаются разными программистами, то обычно поставщик даёт потребителю не непосредственный доступ к данным семейства, а опосредованный – каждому объекту присваивается идентификатор (строка или число, уникальное в пределах семейства), и поставщик разрешает потребителю выполнять некоторый набор действий над каждым объектом, указывая его идентификатор.
Как же поставщик может предоставить потребителю список всех объектов семейства? Первый и наиболее очевидный способ – создать временный массив со всеми их идентификаторами. Но чтобы не тратить время и память на создание этого массива, можно использовать "перечислители" (enumerators), которые будут по очереди выдавать потребителю идентификаторы всех объектов семейства. Такие перечислители могут быть двух видов:
-
цикл выполняется на стороне поставщика (например, EnumWindows или EnumFontFamilies в WinAPI). В этом случае потребитель передаёт поставщику callback-функцию, которая будет вызываться для каждого объекта семейства. Состояние процесса перечисления поставщик может хранить в локальных переменных процедуры, содержащей цикл; потребителю же придётся как-то ассоциировать с callback-функцией структуру, хранящую состояние этого процесса.
-
цикл выполняется на стороне потребителя (например, FindFirstFile/FindNextFile или Module32First/Module32Next в WinAPI). В этом случае уже потребитель хранит состояние процесса перечисления в локальных переменных процедуры, содержащей цикл, а поставщик вынужден хранить его в некой структуре, ассоциированной с функцией перечисления.
Важно, что стандартной моделью перечислителя для COM (и поэтому для VB тоже) является вторая, описанная в SDK как интерфейс IEnumXXXX. Поставщик должен создать объект-перечислитель, реализующий такой интерфейс; состояние процесса перечисления хранится в членах этого объекта и недоступно потребителю; потребитель вызывает в цикле метод Next, и получает объекты семейства по одному.
Именно это происходит при использовании в VB цикла For Each o In c: у объекта c запрашивается перечислитель, реализующий интерфейс IEnumVARIANT (назовём его e), и цикл преобразуется в следующий (схематично):
While e.Next(o) 'получим новый объект
'тело цикла
Wend
Подробности реализации и использования интерфейса IEnumVARIANT в VB последуют ниже.
Рэймонд Чен описывает в своих статьях два этих вида перечислителей как "callback-based" и "consumer-based" соответственно, потому что в каждом случае либо поставщику, либо потребителю приходится проигрывать: состояние одного из них хранится в некой отдельной структуре. Очень просто написать "переходник" для потребителя первого типа и поставщика второго:
Sub EnumXXX(ByVal f As CallbackObject)
Dim o
If XXXFirst(o) Then 'получим первый объект
f.Callback o 'вернём первый объект
While XXXNext(o) 'получим новый объект
f.Callback o 'вернём первый объект
Wend
End If
End Sub
Здесь цикл в функции-перечислителе получает от поставщика по одному объекту и передаёт его в callback-функцию потребителя.
Такой переходник делает ситуацию более "справедливой" – неудобно в равной степени и поставщику, и потребителю. К счастью, есть способ лучше, и Чен рассказывает о нём в своей статье. Оказывается, можно сделать и противоположный переходник: когда поставщик видит callback-функцию, которой в цикле передаёт объекты по одному, а потребитель – функцию Next, которую (опять же в цикле) опрашивает. В этом случае и поставщик, и потребитель хранят своё состояние в локальных переменных и не вынуждены нагромождать дополнительных структур.
Фиберы
В другой моей статье рассказывается о сущности фиберов и их использовании в VB-программах. Вкратце, фиберы – это процедуры, которые выполняются попеременно, причём переход выполнения от одного фибера к другому осуществляется явно, вызовом соответствующей функции API. В той статье рассказывалось, как можно использовать их для имитации одновременного выполнения нескольких циклов как некий суррогат многопоточности в VB. Здесь мы опять хотим выполнять одновременно два цикла, в поставщике и в потребителе, причём явное переключение фиберов играет уже ключевую роль: вызов callback-функции из цикла поставщика будет на самом деле "возвратом" в цикл потребителя, а вызов функции Next – "возвратом" в цикл поставщика. Схематично это можно показать так:
Private Producer As Fiber, Consumer As Fiber, LastItem, Done As Boolean
Function XXXFirst(o) As Boolean
'инициализация перечисления
Set Consumer = ThisFiber 'сохраняем текущий фибер
Set Producer = CreateFiber(AddressOf StartEnumeration) 'создаём второй фибер
Done = False
XXXFirst = XXXNext(o) 'получим первый объект
End Sub
Function XXXNext(o) As Boolean
Debug.Assert Not Done 'если перечисление уже закончено – ошибка
Producer.Switch 'получим один объект от поставщика
If Done Then 'был ли объект?
Set Producer = Nothing 'уничтожаем второй фибер
Else
o = LastItem 'вернём полученный объект
End If
XXXNext = Done
End Sub
Sub Callback(o)
LastItem = o 'сохраним полученный объект
Consumer.Switch 'передадим его потребителю
End Sub
Sub StartEnumeration()
EnumXXX AddressOf Callback 'запустим цикл перечисления
Done = True 'перечисление закончено, объектов больше нет
Consumer.Switch 'последний переход к потребителю
End Sub
Здесь метод Switch в XXXNext передаёт управление на конец процедуры Callback, а методы Switch в Callback и StartEnumeration – на строку If в XXXNext.
Мы не полностью избавились от нелокальных переменных, но, поскольку каждый фибер имеет собственный стек, их количество теперь существенно ограничено: идентификаторы обоих фиберов, последний полученный объект, и флаг завершения перечисления. То, что относящиеся к поставщику и потребителю данные могут размещаться в их локальных переменных, важно, если эти данные имеют сложную структуру – например, рекурсивную, как в примере Чена. (В этом примере функция-обработчик потребителя запрашивает у поставщика перечисление подобъектов каждого объекта)
Пример
В статье Чена показано, как построить с помощью фиберов перечислитель, удобный и поставщику, и потребителю. Я же собираюсь привести пример построения с помощью фиберов "переходника" между циклом For Each (потребителем второго типа) и функцией EnumWindows (поставщик первого рода), т.е. реализовать "коллекцию окон". Когда она будет готова, окна можно будет перебирать подобным кодом:
Dim w As Window , Windows As New Windows
For Each w In Windows
Debug.Print Hex(w.hWnd)
Next
У нашего класса Window будет единственный член – свойство hWnd, но вы в ваших программах можете добавлять в него любые методы и свойства для работы с окнами Windows.
Такого же результата можно было бы добиться и без фиберов, просто заполнив временный массив объектов Window всеми окнами, и перебирая этот массив. Однако создание временного массива уничтожает всю пользу от перечислителя; так что этим способом мы пользоваться не будем.
Построение такого переходника сопряжено с дополнительными сложностями: интерфейс IEnumVARIANT невозможно реализовать с помощью оператора Implements. Есть два выхода: реализовывать объект-перечислитель в обычном модуле, как это делает Эдуардо Морцилло; этого примера там почему-то уже нет, но его копия доступны в форумах VBStreets, или воспользоваться готовой реализацией IEnumVARIANT. Первый выход неудобен тем, что большая часть кода в проекте окажется скучными "шестерёнками" COM-объекта, которые VB обычно генерирует автоматически. Поэтому я собираюсь использовать собственную библиотеку CustEnum, лежащую вместе с ассемблерными исходниками
здесь. Эта библиотека "оборачивает" VB-объект, реализующий более простой интерфейс EnumObject, реализацией интерфейса IEnumVARIANT. Она занимает меньше 7Кб, не требует никаких дополнительных библиотек, и не создаст ощутимого довеска к вашим программам.
Начнём с лежащего в архиве с CustEnum примера перечислителя, перебирающего массив. Первым делом переименуем clsArray в Windows, clsEnum – в EnumWindows, и добавим новый класс Window с единственной строчкой кода:
Public hWnd As Long
Как отмечалось ранее, вы можете добавить в этот класс и любой другой код для работы с отдельным окном. В классе Windows достаточно оставить три строчки:
Public Function NewEnum() As IUnknown
Set NewEnum = RegisterEnumerator(New EnumWindows)
End Function
Нужно только убедиться, что у метода NewEnum по-прежнему Procedure ID = -4 (Tools → Procedure Attributes → Advanced). Такое значение Procedure ID сообщает VB, что именно эта процедура возвращает объект-перечислитель для нашего поставщика (объекта Windows). Собственно объект-перечислитель будет реализован в классе EnumWindows; большая часть кода проекта будет сосредоточена именно в этом, невидимом пользователю, классе (в ActiveX-библиотеке ему можно даже установить Instancing: Private).
Подключим к нашему проекту поддержку фиберов: добавим в него файлы modFibers и IFiber из предыдущей статьи про фиберы. Это ещё не всё "возведение подпорок": чтобы метод класса EnumWindows можно было передать как callback-функцию в API EnumWindows, нужна дополнительная обёртка (см. файлы modEnumWindows и IEnumWindows в архиве). Ничего премудрого в этом нет – код практически дублирует методы CreateFiber и FiberProc в modFibers – поэтому я не привожу его в тексте этой статьи.
Теперь заполним класс EnumWindows, в соответствии с показанной выше схемой, таким кодом:
Implements EnumObject
Implements IFiber
Implements IEnumWindows
Private pProducer As Long, pConsumer As Long, LastItem As Long, Done As Boolean
Private Sub Class_Initialize()
modFibers.Initialize
pConsumer = pFiberMain
pProducer = CreateFiber(Me)
Done = False
End Sub
Private Function EnumObject_NextValue(Value As Variant) As ReturnCode
If Done Then
EnumObject_NextValue = S_FALSE
Else
SwitchToFiber pProducer
If Done Then
'уничтожение отработавшего фибера перенесено в Class_Terminate
Class_Terminate
EnumObject_NextValue = S_FALSE
Else
Set Value = New Window
Value.hWnd = LastItem
EnumObject_NextValue = S_OK
End If
End If
End Function
Private Function IEnumWindows_EnumWindowsProc(ByVal hWnd As Long) As Long
LastItem = hWnd
SwitchToFiber pConsumer
IEnumWindows_EnumWindowsProc = 1 'продолжать перечисление
End Function
Private Sub IFiber_FiberProc()
EnumWindows Me
Done = True
SwitchToFiber pConsumer
End Sub
Private Sub Class_Terminate()
'уничтожаем фибер поставщика, "застывший" посередине процесса перечисления
'удаление фиберов небезопасно в IDE, делаем его только в скомпилированном коде
If modFibers.Compiled Then DeleteFiber pProducer
End Sub
Private Function EnumObject_Clone(NewEnum As IEnumVARIANT) As ReturnCode
'нетривиально реализуется, и всё равно не используется VB
EnumObject_Clone = E_NOTIMPL
End Function
Private Function EnumObject_Reset() As ReturnCode
'заново инициализируем процесс перечисления
Class_Terminate
Class_Initialize
EnumObject_Reset = S_OK
End Function
Private Function EnumObject_Skip(ByVal Number As Long) As ReturnCode
'примитивнейшая реализация: нужное число раз вызовем Next
Dim c As Long
For c = 1 To Number
EnumObject_NextValue Null
Next
EnumObject_Skip = IIf(Done, S_FALSE, S_OK)
End Function
Первая часть этого кода один-в-один повторяет нашу схему: XXXFirst переименовалось в Class_Initialize, XXXNext – в EnumObject_NextValue, Callback – в IEnumWindows_EnumWindowsProc, а StartEnumeration – в IFiber_FiberProc. Вторая часть кода – это специфика реализации IEnumVariant: EnumObject_Clone должен возвращать копию объекта-перечислителя, готовую продолжить перечисление с той же позиции (не реализовано, поскольку API EnumWindows соответствующей функциональностью не обладает), EnumObject_Reset должен "сбросить" объект-перечислитель в изначальное состояние, EnumObject_Skip – пропустить в перечислении указанное число итераций. Интересная особенность класса EnumWindows – полное отсутствие в нём публичных членов.
Вот, собственно, и весь перечислитель-переходник. Можно протестировать его приведённым выше кодом. У меня он запускается в IDE только в режиме "Start With Full Compile" (Ctrl-F5). Скомпилированный же код работает без ограничений.
Заключение
Надеюсь, теперь вы получили представление об основном предназначении фиберов в Windows: не суррогатная многопоточность, а поддержание двух одновременно выполняющихся, согласованных процедур. Кроме того, вы научились использовать CustEnum для реализации собственных коллекций, перечислимых оператором For Each. Надеюсь, что и то и другое было интересным и полезным.
Приведённый в этой статье код можно использовать как заготовку объектной библиотеки для работы с окнами Windows. Если вы добавите к классам Window и Windows нужную вам функциональность, то вы, например, сможете из VBScript выполнять над всеми окнами в системе определённые действия. Это уже будет практически ценная вещь: например, так можно автоматизировать ввод однообразных данных, или даже написать на VBScript программу-шпион, сохраняющую в файл текст всех открываемых окон. Одним словом, применения такой библиотеке найти будет нетрудно. Желаю удачи в её построении!
А. Скробов
E-Mail: tyomitch@gmail.com
WWW: http://cs.usu.edu.ru/home/skrobov/