Дата публикации статьи: 22.07.2005 08:02

А. Скробов
Использование фиберов в перечислителях

Скачать исходный код к статье

Перечислители

Очень часто бывает необходимо проделать однообразные действия над семейством объектов, например окнами на экране, файлами в каталоге, или элементами в коллекции. Назовём объект, владеющий таким семейством, "поставщиком", а объект, выполняющий над ним действия – "потребителем". Если эти два объекта создаются и поддерживаются разными программистами, то обычно поставщик даёт потребителю не непосредственный доступ к данным семейства, а опосредованный – каждому объекту присваивается идентификатор (строка или число, уникальное в пределах семейства), и поставщик разрешает потребителю выполнять некоторый набор действий над каждым объектом, указывая его идентификатор.
    Как же поставщик может предоставить потребителю список всех объектов семейства? Первый и наиболее очевидный способ – создать временный массив со всеми их идентификаторами. Но чтобы не тратить время и память на создание этого массива, можно использовать "перечислители" (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/