TN058. Реализация состояния модуля MFC

Примечание.

Следующее техническое примечание не было обновлено, поскольку сначала оно было включено в электронную документацию. В результате некоторые процедуры и разделы могут быть устаревшими или неверными. Для получения последних сведений рекомендуется выполнить поиск интересующей темы в алфавитном указателе документации в Интернете.

Эта техническая заметка описывает реализацию конструкций MFC "состояние модуля". Понимание реализации состояния модуля имеет решающее значение для использования общих библиотек DLL MFC из библиотек DLL (или ole in-process server).

Перед чтением этой заметки см. статью "Управление данными о состоянии модулей MFC" в разделе "Создание новых документов, Windows и представлений". В этой статье содержатся важные сведения об использовании и общие сведения об этом вопросе.

Обзор

Существует три типа сведений о состоянии MFC: состояние модуля, состояние процесса и состояние потока. Иногда эти типы состояний можно объединить. Например, карты дескрипторов MFC являются локальными и локальными потоками модуля. Это позволяет двум модулям иметь разные карты в каждом из их потоков.

Состояние процесса и состояние потока похожи. Эти элементы данных являются вещами, которые традиционно были глобальными переменными, но должны быть характерными для заданного процесса или потока для правильной поддержки Win32s или для надлежащей поддержки многопоточных операций. Какая категория, в которую вписывается заданный элемент данных, зависит от этого элемента и его требуемой семантики в отношении границ процессов и потоков.

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

Переключение состояния модуля

Каждый поток содержит указатель на состояние текущего или активного модуля (не удивительно, указатель является частью локального состояния потока MFC). Этот указатель изменяется, когда поток выполнения передает границу модуля, например приложение, вызывающее OLE Control или DLL, или OLE Control, возвращающееся в приложение.

Текущее состояние модуля переключается путем вызова AfxSetModuleState. В большинстве случаев вы никогда не будете работать непосредственно с API. MFC, во многих случаях, будет вызывать его для вас (в WinMain, OLE входных точек, AfxWndProcи т. д.). Это делается в любом компоненте, который вы пишете путем статического связывания в специальном WndProcи специальном (илиDllMain) состоянии WinMain модуля, который должен быть текущим. Этот код можно увидеть, просмотрев DLLMODUL. CPP или A система УПП ODUL. CPP в каталоге MFC\SRC.

Редко необходимо задать состояние модуля, а затем не задать его обратно. Большую часть времени, когда вы хотите "отправить" собственное состояние модуля в качестве текущего, а затем, после завершения, всплывающее окно исходного контекста назад. Это делается макросом AFX_MANAGE_STATE и специальным классом AFX_MAINTAIN_STATE.

CCmdTarget имеет специальные функции для поддержки переключения состояния модуля. В частности, это корневой класс, CCmdTarget используемый для автоматизации OLE и точек входа OLE COM. Как и любая другая точка входа, доступная системе, эти точки входа должны задать правильное состояние модуля. Как знать CCmdTarget , какое состояние модуля должно быть правильным, ответ заключается в том, что он "запоминает" состояние "текущего" модуля при построении, таким образом, что он может задать текущее состояние модуля для этого "запоминаемого" значения при последующем вызове. В результате состояние модуля, с которым связан заданный CCmdTarget объект, является состояние модуля, которое было текущим при создании объекта. Пример загрузки сервера INPROC, создания объекта и вызова его методов.

  1. Библиотека DLL загружается с помощью LoadLibraryOLE.

  2. RawDllMain вызывается первым. Он задает состояние модуля в известном состоянии статического модуля для библиотеки DLL. По этой причине RawDllMain статически связан с библиотекой DLL.

  3. Вызывается конструктор фабрики классов, связанной с нашим объектом. COleObjectFactory является производным от CCmdTarget и в результате он запоминает, в каком состоянии модуля он был создан. Это важно: когда фабрика классов запрашивает создание объектов, теперь он знает, какое состояние модуля необходимо сделать текущим.

  4. DllGetClassObject вызывается для получения фабрики классов. MFC выполняет поиск списка фабрики классов, связанного с этим модулем, и возвращает его.

  5. Вызывается метод COleObjectFactory::XClassFactory2::CreateInstance. Прежде чем создать объект и вернуть его, эта функция задает состояние модуля в состояние модуля, которое было текущим на шаге 3 (текущим COleObjectFactory при создании экземпляра). Это делается внутри METHOD_PROLOGUE.

  6. Когда объект создается, он также является производным CCmdTarget и таким же образом COleObjectFactory запоминается, какое состояние модуля было активным, поэтому делает этот новый объект. Теперь объект знает, на какое состояние модуля переключаться при каждом вызове.

  7. Клиент вызывает функцию в объекте OLE COM, полученном от его CoCreateInstance вызова. Когда объект вызывается, он используется METHOD_PROLOGUE для переключения состояния модуля так же, как COleObjectFactory это делает.

Как видно, состояние модуля распространяется от объекта к объекту по мере их создания. Важно правильно задать состояние модуля. Если он не задан, объект DLL или COM может плохо взаимодействовать с приложением MFC, вызывающим его, или может не найти собственные ресурсы или может завершиться ошибкой в других несчастных способах.

Обратите внимание, что некоторые типы библиотек DLL, в частности "Расширение MFC", не переключают состояние модуля в их RawDllMain (на самом деле они, как правило, даже не имеют RawDllMain). Это связано с тем, что они предназначены для поведения "как если бы" они были на самом деле присутствуют в приложении, которое использует их. Они являются очень частью запущенного приложения, и это их намерение изменить глобальное состояние этого приложения.

Элементы управления OLE и другие библиотеки DLL отличаются. Они не хотят изменять состояние вызывающего приложения; Вызываемое приложение может даже не быть приложением MFC, поэтому не может быть изменено состояние. Это причина, по которой было изобретено переключение состояния модуля.

Для экспортированных функций из библиотеки DLL, например для запуска диалогового окна в библиотеке DLL, необходимо добавить следующий код в начало функции:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Это переключает текущее состояние модуля на состояние, возвращаемое из AfxGetStaticModuleState до конца текущего область.

Проблемы с ресурсами в библиотеках DLL возникают, если макрос AFX_MODULE_STATE не используется. По умолчанию MFC использует дескриптор ресурсов основного приложения для загрузки шаблона ресурса. Этот шаблон фактически хранится в библиотеке DLL. Основная причина заключается в том, что сведения о состоянии модуля MFC не были переключены макросом AFX_MODULE_STATE. Дескриптор ресурса восстанавливается из состояния модуля MFC. Не переключение состояния модуля приводит к неправильному использованию дескриптора ресурсов.

AFX_MODULE_STATE не требуется помещать каждую функцию в библиотеку DLL. Например, InitInstance можно вызывать с помощью кода MFC в приложении без AFX_MODULE_STATE, так как MFC автоматически перемещает состояние модуля до InitInstance , а затем переключает его обратно после InitInstance возврата. То же самое верно для всех обработчиков карты сообщений. Обычные библиотеки DLL MFC фактически имеют специальную процедуру главного окна, которая автоматически переключает состояние модуля перед маршрутизацией любого сообщения.

Обработка локальных данных

Обработка локальных данных не была бы такой большой проблемой, если это не было для сложности модели DLL Win32s. Во всех библиотеках DLL Win32s используются глобальные данные, даже если они загружаются несколькими приложениями. Это очень отличается от "реальной" модели данных DLL Win32, где каждая библиотека DLL получает отдельную копию своего пространства данных в каждом процессе, который подключается к библиотеке DLL. Чтобы добавить сложность, данные, выделенные в куче в библиотеке DLL Win32s, фактически являются конкретными процессами (по крайней мере до того, как идет владение). Рассмотрим следующие данные и код:

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

Рассмотрим, что происходит, если приведенный выше код находится в библиотеке DLL и загружается двумя процессами A и B (это может быть два экземпляра одного приложения). Вызовы SetGlobalString("Hello from A"). В результате память выделяется для CString данных в контексте процесса А. Помните, что CString сам по себе является глобальным и видимым как A, так и B. Теперь B звонит GetGlobalString(sz, sizeof(sz)). B сможет просмотреть данные, которые набор A. Это связано с тем, что Win32s не обеспечивает защиту между процессами, такими как Win32. Это первая проблема; Во многих случаях не рекомендуется использовать одно приложение, влияющее на глобальные данные, которые считаются принадлежащими другому приложению.

Кроме того, существуют дополнительные проблемы. Предположим, что теперь A выходит. При выходе из системы память, используемая строкой 'strGlobal', становится доступной для системы, т. е. для всей памяти, выделенной процессом A, автоматически освобождается операционной системой. Он не освобождается, потому что CString вызывается деструктор; он еще не был вызван. Он освобождается просто потому, что приложение, выделенное им, оставило сцену. Теперь, если вызов B вызван GetGlobalString(sz, sizeof(sz)), он может не получить допустимые данные. Возможно, некоторые другие приложения использовали память для чего-то другого.

Очевидно, что существует проблема. MFC 3.x использовал метод, называемый локальным хранилищем потоков (TLS). MFC 3.x выделяет индекс TLS, который в win32s действительно выступает в качестве индекса локального хранения процесса, даже если он не вызывается, а затем будет ссылать все данные на основе этого индекса TLS. Это аналогично индексу TLS, который использовался для хранения локальных данных потока в Win32 (см. ниже дополнительные сведения об этой теме). Это привело к тому, что каждая библиотека DLL MFC использует по крайней мере два индекса TLS для каждого процесса. При загрузке большого количества БИБЛИОТЕК DLL управления OLE (OCXs) можно быстро выйти из индексов TLS (доступно только 64). Кроме того, MFC должен был разместить все эти данные в одном месте в одной структуре. Это было не очень расширяемым и не было идеальным в отношении его использования индексов TLS.

MFC 4.x обращается к этому с набором шаблонов классов, которые можно "упаковать" вокруг данных, которые должны обрабатываться локально. Например, проблема, упоминание выше, может быть исправлена путем записи:

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC реализует это в двух шагах. Во-первых, поверх API Win32 Tls* (TlsAlloc, TlsSetValue, TlsGetValue и т. д.), который использует только два индекса TLS для каждого процесса, независимо от количества библиотек DLL. Во-вторых, CProcessLocal шаблон предоставляется для доступа к этим данным. Он переопределяет оператор,> который позволяет интуитивно понятный синтаксис, который вы видите выше. Все объекты, которые упаковываются в CProcessLocal оболочку, должны быть производными от CNoTrackObject. CNoTrackObjectпредоставляет низкоуровневый распределитель (LocalAlloc/LocalFree) и виртуальный деструктор, который MFC может автоматически уничтожить локальные объекты процесса при завершении процесса. Такие объекты могут иметь пользовательский деструктор, если требуется дополнительная очистка. В приведенном выше примере не требуется один, так как компилятор создаст деструктор по умолчанию для уничтожения внедренного CString объекта.

Существуют и другие интересные преимущества этого подхода. Не только все CProcessLocal объекты, уничтоженные автоматически, они не создаются до тех пор, пока они не нужны. CProcessLocal::operator-> создает экземпляр связанного объекта при первом вызове и не раньше. В приведенном выше примере это означает, что строка 'strGlobal' не будет создана до первого раза SetGlobalString или GetGlobalString вызывается. В некоторых случаях это может помочь уменьшить время запуска БИБЛИОТЕК DLL.

Локальные данные потока

Аналогично обработке локальных данных, локальные потоки используются, когда данные должны быть локальными для заданного потока. То есть требуется отдельный экземпляр данных для каждого потока, который обращается к этим данным. Это может быть использовано во многих случаях вместо широких механизмов синхронизации. Если данные не должны совместно использоваться несколькими потоками, такие механизмы могут быть дорогостоящими и ненужными. Предположим, у нас был CString объект (как и в приведенном выше примере). Мы можем сделать поток локальным CThreadLocal , упаковав его с помощью шаблона:

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

Если MakeRandomString вызов был вызван из двух разных потоков, каждая из них будет "перемешивает" строку разными способами, не вмешиваясь в другую. Это связано с тем, что на каждом потоке фактически strThread существует экземпляр, а не только один глобальный экземпляр.

Обратите внимание, как ссылка используется для записи CString адреса один раз вместо одного итерации цикла. Код цикла может быть написан вездеstr, где threadData->strThread "" используется, но код будет гораздо медленнее в выполнении. Рекомендуется кэшировать ссылку на данные при возникновении таких ссылок в циклах.

Шаблон CThreadLocal класса использует те же механизмы, что CProcessLocal и те же методы реализации.

См. также

Технические примечания по номеру
Технические примечания по категории