Compartilhar via


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.

Esta 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 do MFC de um servidor em processo DLL (ou OLE).

Antes de ler esta nota, consulte "Gerenciando os dados de estado de módulos MFC" na criação de novos documentos, Windows 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 do 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 do thread local. Isso permite que dois módulos diferentes tenham mapas diferentes em cada um de seus threads.

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 o suporte adequado do Win32s ou para o suporte adequado para multithreading. A categoria em que um determinado item de dados se encaixa depende desse item e de sua semântica desejada em relação aos limites de processo e thread.

O Estado do Módulo é exclusivo, pois pode conter estado ou estado verdadeiramente global que é local de processo ou thread local. 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 é surpresa que o ponteiro faça parte do estado local do thread do MFC). Esse ponteiro é alterado quando o thread de execução passa um limite de módulo, como um aplicativo chamando um controle OLE ou DLL, ou uma chamada de controle OLE de volta para um aplicativo.

O estado atual do módulo é alternado chamando AfxSetModuleState. Na maioria das vezes, você nunca lidará diretamente com a API. O MFC, em muitos casos, chamará para você (em WinMain, pontos de entrada OLE, AfxWndProcetc.). Isso é feito em qualquer componente que você escreve vinculando estaticamente em um especial WndProce um especial WinMain (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 você querer definir o estado do módulo e, em seguida, não defini-lo de volta. Na maioria das vezes, você deseja "efetuar push" do seu próprio estado de módulo como o atual e, depois de terminar, "pop" o contexto original de volta. Isso é feito pela macro AFX_MANAGE_STATE e pela classe AFX_MAINTAIN_STATEespecial.

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 devem definir o estado correto do módulo. Como um determinado CCmdTarget usuário 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 pode definir o estado do módulo atual como esse valor "lembrado" quando for chamado posteriormente. Como resultado, o módulo informa que um determinado CCmdTarget objeto está associado é o estado do módulo que era atual quando o objeto foi construído. Use um exemplo simples de carregar um servidor INPROC, criar um objeto e chamar seus métodos.

  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á vinculado estaticamente à 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 do 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 é chamado para obter a fábrica de classes. O MFC pesquisa a lista de fábrica 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 o COleObjectFactory foi instanciado). Isso é feito dentro de METHOD_PROLOGUE.

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

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

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

Observe que determinados tipos de DLLs, especificamente DLLs de "Extensão MFC", não alternam o estado do módulo em seus RawDllMain (na verdade, eles geralmente nem têm um RawDllMain). Isso ocorre porque eles se destinam a se comportar "como se" estivessem realmente presentes no aplicativo que os usa. Eles fazem parte do aplicativo que está em execução e é sua intenção 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 até não ser um aplicativo MFC e, portanto, pode não haver nenhum estado a ser modificado. 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 é realmente 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 colocado 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 alterna automaticamente o estado do módulo antes InitInstance e o alterna de volta após InitInstance o retorno. O mesmo é verdadeiro para todos os manipuladores de mapa de mensagens. As DLLs MFC regulares realmente 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 de grande preocupação se não fosse pela dificuldade do modelo de DLL do Win32s. No Win32s, todas as DLLs compartilham seus dados globais, mesmo quando carregadas por vários aplicativos. Isso é muito diferente do modelo de dados DLL Win32 "real", em que cada DLL obtém uma cópia separada de seu espaço de dados em cada processo anexado à DLL. Para adicionar à complexidade, os dados alocados no heap em uma DLL do Win32s são de fato específicos do 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 essa DLL for carregada por dois processos A e B (pode, de fato, ser duas instâncias do mesmo aplicativo). Uma chamada SetGlobalString("Hello from A"). Como resultado, a memória é alocada para os CString dados no contexto do processo A. Tenha em mente que o CString próprio é global e está visível para A e B. Agora B chama GetGlobalString(sz, sizeof(sz)). B poderá ver os dados definidos por A. 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.

Há problemas adicionais também. Digamos que A saia agora. Quando A é encerrado, 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. Ele não é liberado porque o CString destruidor está sendo chamado; ele ainda não foi chamado. Ele é liberado simplesmente porque o aplicativo que o alocou deixou a cena. Agora, se b chamado GetGlobalString(sz, sizeof(sz)), ele pode não obter dados válidos. Algum outro aplicativo pode ter usado essa memória para outra coisa.

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

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 pode 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 CProcessLocal modelo é 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 devem CProcessLocal 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 forem necessárias limpezas adicionais. O exemplo acima não requer um, pois o compilador gerará um destruidor padrão para destruir o objeto inserido CString .

Há outras vantagens interessantes para essa abordagem. Não só todos os CProcessLocal objetos são destruídos automaticamente, como 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 e não mais cedo. No exemplo acima, isso significa que a cadeia de caracteres 'strGlobal' não será construída até a primeira vez SetGlobalString ou GetGlobalString será 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 de thread são usados quando os dados devem 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 ser usado muitas vezes em vez de amplos mecanismos 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 tínhamos um CString objeto (muito parecido com o exemplo acima). Podemos torná-lo local, encapsulando-o com um CThreadLocal modelo:

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 strThread instância por thread em vez de apenas uma instância global.

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

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

Consulte também

Notas técnicas por número
Notas técnicas por categoria