TN058: implementação de estado do módulo MFC

Observação

A nota técnica a seguir não foi atualizada desde que foi incluída pela primeira vez na documentação online. Como resultado, alguns procedimentos e tópicos podem estar desatualizados ou incorretos. Para obter as informações mais recentes, é recomendável que você pesquise o tópico de interesse no índice de documentação online.

Essa nota técnica descreve a implementação de constructos de "estado do módulo" do MFC. Uma compreensão da implementação do estado do módulo é fundamental para usar as DLLs compartilhadas MFC de uma DLL (ou servidor em processo OLE).

Antes de ler esta nota, confira "Gerenciando os dados de estado dos módulos MFC" em Criando novos documentos, janelas e exibições. Este artigo contém informações de uso importantes e informações de visão geral sobre este assunto.

Visão geral

Há três tipos de informações de estado MFC: estado do módulo, estado do processo e estado do thread. Às vezes, esses tipos de estado podem ser combinados. Por exemplo, os mapas de identificador do MFC são locais do módulo e locais do thread. Isso permite que dois módulos diferentes tenham mapas diferentes em cada um dos threads deles.

O Estado do Processo e o Estado do Thread são semelhantes. Esses itens de dados são coisas que tradicionalmente têm sido variáveis globais, mas precisam ser específicos para um determinado processo ou thread para suporte adequado a Win32s ou para suporte adequado a multithreading. A categoria em que um determinado item de dados se encaixa depende desse item e da semântica desejada em relação aos limites de processo e thread.

O Estado do Módulo é exclusivo, pois pode conter um estado verdadeiramente global ou um estado que é local do processo ou local do thread. Além disso, ele pode ser alternado rapidamente.

Alternância de Estado do Módulo

Cada thread contém um ponteiro para o estado do módulo "atual" ou "ativo" (não surpreendentemente, o ponteiro faz parte do estado local do thread do MFC). Esse ponteiro é alterado quando o thread de execução passa um limite de módulo, como uma chamada de aplicativo para um controle OLE ou DLL, ou um retorno de chamada de controle OLE para um aplicativo.

O estado atual do módulo é alternado chamando AfxSetModuleState. Na maior parte do tempo, você nunca lidará diretamente com a API. O MFC, em muitos casos, a chamará para você (em WinMain, pontos de entrada OLE, AfxWndProc etc.). Isso é feito em qualquer componente que você escreve vinculando estaticamente em um WndProc especial e um WinMain especial (ou DllMain) que sabe qual estado do módulo deve ser atual. Você pode ver esse código examinando DLLMODUL.CPP ou APPMODUL.CPP no diretório MFC\SRC.

É raro que você queira definir o estado do módulo e, em seguida, não defini-lo novamente. Na maioria das vezes, você deseja definir seu estado de módulo como o atual e, depois de terminar, restaurar o contexto original. Isso é feito pela macro AFX_MANAGE_STATE e pela classe especial AFX_MAINTAIN_STATE.

CCmdTarget tem recursos especiais para dar suporte à alternância de estado do módulo. Em particular, uma CCmdTarget é a classe raiz usada para automação OLE e pontos de entrada OLE COM. Como qualquer outro ponto de entrada exposto ao sistema, esses pontos de entrada precisam definir o estado correto do módulo. Como um determinado CCmdTarget sabe qual deve ser o estado do módulo "correto"? A resposta é que ele "lembra" qual é o estado do módulo "atual" quando ele é construído, de modo que ele poderá definir o estado do módulo atual como esse valor "lembrado" quando for chamado posteriormente. Como resultado, o estado do módulo com o qual um determinado objeto CCmdTarget está associado é o estado do módulo que era atual quando o objeto foi construído. Veja o exemplo simples de carregar um servidor INPROC, criar um objeto e chamar os métodos dele.

  1. A DLL é carregada pelo OLE usando LoadLibrary.

  2. RawDllMain é chamado primeiro. Ele define o estado do módulo como o estado do módulo estático conhecido para a DLL. Por esse motivo, RawDllMain está estaticamente vinculado à DLL.

  3. O construtor da fábrica de classes associada ao nosso objeto é chamado. COleObjectFactory é derivado de CCmdTarget e, como resultado, ele se lembra em qual estado de módulo ele foi instanciado. Isso é importante – quando a fábrica de classes é solicitada a criar objetos, ela sabe agora qual estado de módulo tornar atual.

  4. DllGetClassObject é chamada para obter a fábrica de classes. O MFC pesquisa a lista de fábricas de classes associada a este módulo e a retorna.

  5. COleObjectFactory::XClassFactory2::CreateInstance é chamado. Antes de criar o objeto e devolvê-lo, essa função define o estado do módulo como o estado do módulo atual na etapa 3 (aquele que era atual quando COleObjectFactory foi instanciado). Isso é feito dentro de METHOD_PROLOGUE.

  6. Quando o objeto é criado, ele também é um derivado de CCmdTarget e, da mesma forma que COleObjectFactory lembrou qual estado do módulo estava ativo, o mesmo ocorreu com esse novo objeto. Agora, o objeto sabe para qual estado do módulo alternar sempre que for chamado.

  7. O cliente chama uma função no objeto OLE COM que recebeu da própria chamada a CoCreateInstance. Quando o objeto é chamado, ele usa METHOD_PROLOGUE para alternar o estado do módulo da mesma forma que COleObjectFactory faz.

Como você pode ver, o estado do módulo é propagado de objeto para objeto conforme eles são criados. É importante definir adequadamente o estado do módulo. Se ele não estiver definido, seu objeto DLL ou COM poderá interagir mal com um aplicativo MFC que o esteja chamando, não conseguir encontrar os próprios recursos ou falhar de outras maneiras terríveis.

Observe que determinados tipos de DLLs, especificamente DLLs de "Extensão MFC", não alternam o estado do módulo nos próprios RawDllMain (na verdade, eles geralmente nem têm um RawDllMain). Isso ocorre porque elas se destinam a se comportar "como se" estivessem realmente presentes no aplicativo que as usa. Elas fazem parte do aplicativo que está em execução e destinam-se a modificar o estado global desse aplicativo.

Controles OLE e outras DLLs são muito diferentes. Eles não querem modificar o estado do aplicativo de chamada; o aplicativo que os está chamando pode nem ser um aplicativo MFC e, portanto, pode não haver estado para modificar. Esse é o motivo pelo qual a alternância de estado do módulo foi inventada.

Para funções exportadas de uma DLL, como uma que inicia uma caixa de diálogo em sua DLL, você precisa adicionar o seguinte código ao início da função:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Isso troca o estado do módulo atual pelo estado retornado de AfxGetStaticModuleState até o final do escopo atual.

Problemas com recursos em DLLs ocorrerão se a macro AFX_MODULE_STATE não for usada. Por padrão, o MFC usa o identificador de recurso do aplicativo principal para carregar o modelo de recurso. Esse modelo é armazenado na DLL. A causa raiz é que as informações de estado do módulo do MFC não foram alternadas pela macro AFX_MODULE_STATE. O identificador de recurso é recuperado do estado do módulo do MFC. Não alternar o estado do módulo faz com que o identificador de recurso incorreto seja usado.

AFX_MODULE_STATE não precisa ser colocada em todas as funções na DLL. Por exemplo, InitInstance pode ser chamado pelo código MFC no aplicativo sem AFX_MODULE_STATE porque o MFC desloca automaticamente o estado do módulo antes de InitInstance e então o alterna de volta depois do retorno de InitInstance. O mesmo vale para todos os manipuladores de mapa de mensagens. As DLLs MFC na verdade têm um procedimento de janela mestre especial que alterna automaticamente o estado do módulo antes de rotear qualquer mensagem.

Processar dados locais

Processar dados locais não seria uma grande preocupação se não fosse pela dificuldade do modelo de DLL do Win32s. No Win32s, todas as DLLs compartilham os próprios dados globais, mesmo quando carregadas por vários aplicativos. Isso é muito diferente do modelo de dados "real" da DLL Win32, em que cada DLL obtém uma cópia separada do próprio espaço de dados cada vez que um processo é anexado à DLL. Para aumentar a complexidade, os dados alocados no heap em uma DLL do Win32s são na verdade específicos a cada processo (pelo menos no que diz respeito à propriedade). Considere os seguintes dados e código:

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);
}

Considere o que acontece se o código acima estiver localizado em uma DLL e se a DLL for carregada por dois processos A e B (poderia, na verdade, ser duas instâncias do mesmo aplicativo). A chama SetGlobalString("Hello from A"). Como resultado, a memória é alocada para os dados de CString no contexto do processo A. Tenha em mente que o CString propriamente dito é global e está visível para A e B. Agora, B chama GetGlobalString(sz, sizeof(sz)). B poderá ver os dados que A definiu. Isso ocorre porque o Win32s não oferece proteção entre processos como o Win32. Esse é o primeiro problema; em muitos casos, não é desejável que um aplicativo afete dados globais considerados pertencentes a um aplicativo diferente.

Também há problemas adicionais. Digamos que A saia agora. Quando A sai, a memória usada pela cadeia de caracteres 'strGlobal' é disponibilizada para o sistema – ou seja, toda a memória alocada pelo processo A é liberada automaticamente pelo sistema operacional. Ela não é liberada porque o destruidor CString está sendo chamado; ele ainda não foi chamado. Ela é liberada simplesmente porque o aplicativo que a alocou deixou a cena. Agora, se B chamasse GetGlobalString(sz, sizeof(sz)), ele poderia não obter dados válidos. Algum outro aplicativo pode ter usado essa memória para outra coisa.

Claramente, há um problema. O MFC 3.x usava uma técnica chamada TLS (armazenamento local de thread). O MFC 3.x alocaria um índice TLS que, no Win32s, na realidade atua como um índice de armazenamento local de processo, embora não seja chamado assim, e referenciaria todos os dados com base nesse índice TLS. Isso é semelhante ao índice TLS que era usado para armazenar dados locais de thread no Win32 (confira abaixo mais informações sobre esse assunto). Isso fazia com que cada DLL do MFC utilizasse pelo menos dois índices TLS por processo. Quando você contabiliza o carregamento de muitas DLLs de Controle OLE (OCXs), você rapidamente esgota os índices TLS (há apenas 64 disponíveis). Além disso, o MFC precisou colocar todos esses dados em um só lugar, em apenas uma estrutura. O uso que ele fazia de índices TLS não era muito extensível e não era ideal.

O MFC 4.x aborda isso com um conjunto de modelos de classe que você pode "encapsular" em torno dos dados que devem ser processados localmente. Por exemplo, o problema mencionado acima poderia ser corrigido escrevendo:

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);
}

O MFC implementa isso em duas etapas. Primeiro, há uma camada sobre as APIs Tls* do Win32 (TlsAlloc, TlsSetValue, TlsGetValue etc) que usam apenas dois índices TLS por processo, independentemente de quantas DLLs você tenha. Em segundo lugar, o modelo CProcessLocal é fornecido para acessar esses dados. Ele substitui o operador >, que é o que permite a sintaxe intuitiva que você vê acima. Todos os objetos que são encapsulados por CProcessLocal precisam ser derivados de CNoTrackObject. CNoTrackObject fornece um alocador de nível inferior (LocalAlloc/LocalFree) e um destruidor virtual de modo que o MFC possa destruir automaticamente os objetos locais do processo quando o processo for encerrado. Esses objetos poderão ter um destruidor personalizado se uma limpeza adicional for necessária. O exemplo acima não exige uma, pois o compilador gerará um destruidor padrão para destruir o objeto CString inserido.

Essa abordagem tem várias vantagens. Os objetos CProcessLocal não são apenas destruídos automaticamente, eles não são construídos até que sejam necessários. CProcessLocal::operator-> criará uma instância do objeto associado na primeira vez que ele for chamado, não antes disso. No exemplo acima, isso significa que a cadeia de caracteres 'strGlobal' não será construída até a primeira vez que SetGlobalString ou GetGlobalString for chamada. Em alguns casos, isso pode ajudar a diminuir o tempo de inicialização da DLL.

Dados locais do thread

Semelhante ao processo de dados locais, os dados locais do thread são usados quando os dados precisam ser locais para um determinado thread. Ou seja, você precisa de uma instância separada dos dados para cada thread que acessa esses dados. Isso pode muitas vezes ser usado em vez de mecanismos amplos de sincronização. Se os dados não precisarem ser compartilhados por vários threads, esses mecanismos poderão ser caros e desnecessários. Suponha que tenhamos um objeto CString (muito parecido com o exemplo acima). Podemos torná-lo local para cada thread, encapsulando-o com um modelo 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
        }
    }
}

Se MakeRandomString fosse chamado de dois threads diferentes, cada um "embaralharia" a cadeia de caracteres de maneiras diferentes sem interferir com o outro. Isso ocorre porque, na verdade, há uma instância de strThread por thread em vez de apenas uma instância global.

Observe como uma referência é usada para capturar o endereço CString apenas uma vez, ao invés de uma vez por iteração de loop. O código de loop poderia ter sido escrito com threadData->strThread em todos os lugares em que 'str' é usado, mas o código seria muito mais lento na execução. Quando essas referências ocorrem em loops, é melhor armazenar em cache uma referência aos dados.

O modelo de classe CThreadLocal usa os mesmos mecanismos que CProcessLocal e as mesmas técnicas de implementação.

Confira também

Observações técnicas por número
Observações técnicas por categoria