Condividi tramite


TN058: implementazione di stato del modulo MFC

Nota

La seguente nota tecnica non è stata aggiornata da quando è stata inclusa per la prima volta nella documentazione online. Di conseguenza, alcune procedure e argomenti potrebbero essere non aggiornati o errati. Per le informazioni più recenti, è consigliabile cercare l'argomento di interesse nell'indice della documentazione online.

Questa nota tecnica descrive l'implementazione di costrutti "stato modulo" MFC. Una conoscenza dell'implementazione dello stato del modulo è fondamentale per l'uso delle DLL condivise MFC da una DLL (o da un server ole in-process).

Prima di leggere questa nota, fare riferimento a "Gestione dei dati sullo stato dei moduli MFC" in Creazione di nuovi documenti, Finestre e visualizzazioni. Questo articolo contiene informazioni importanti sull'utilizzo e informazioni generali su questo argomento.

Panoramica

Esistono tre tipi di informazioni sullo stato MFC: Stato del modulo, Stato processo e Stato del thread. In alcuni casi questi tipi di stato possono essere combinati. Ad esempio, le mappe di handle MFC sono sia locali del modulo che locali del thread. In questo modo, due moduli diversi possono avere mappe diverse in ognuno dei relativi thread.

Lo stato del processo e lo stato del thread sono simili. Questi elementi di dati sono elementi che tradizionalmente sono variabili globali, ma devono essere specifici di un determinato processo o thread per il supporto corretto di Win32s o per il supporto di multithreading appropriato. La categoria in cui si inserisce un determinato elemento di dati dipende dall'elemento e dalla semantica desiderata in relazione ai limiti di processo e thread.

Lo stato del modulo è univoco in quanto può contenere uno stato veramente globale o uno stato che è locale del processo o del thread. Inoltre, può essere cambiato rapidamente.

Cambio di stato del modulo

Ogni thread contiene un puntatore allo stato del modulo "corrente" o "attivo" (non sorprendentemente, il puntatore fa parte dello stato locale del thread MFC). Questo puntatore viene modificato quando il thread di esecuzione passa un limite di modulo, ad esempio un'applicazione che chiama un controllo OLE o una DLL o un controllo OLE che richiama un'applicazione.

Lo stato del modulo corrente viene commutato chiamando AfxSetModuleState. Per la maggior parte, non si affronterà mai direttamente con l'API. MFC, in molti casi, lo chiamerà per te (in WinMain, punti di ingresso OLE, AfxWndProce così via). Questa operazione viene eseguita in qualsiasi componente scritto tramite collegamento statico in uno speciale e uno speciale WndProcWinMain (o DllMain) che conosce lo stato del modulo deve essere corrente. È possibile visualizzare questo codice esaminando DLLMODUL. CPP o APPMODUL. CPP nella directory MFC\SRC.

È raro che si voglia impostare lo stato del modulo e quindi non impostarlo di nuovo. Nella maggior parte dei casi si vuole "eseguire il push" dello stato del modulo come quello corrente e quindi, dopo aver completato, "pop" il contesto originale. Questa operazione viene eseguita dalla macro AFX_MANAGE_STATE e dalla classe AFX_MAINTAIN_STATEspeciale .

CCmdTarget dispone di funzionalità speciali per il cambio di stato del modulo. In particolare, è CCmdTarget la classe radice usata per l'automazione OLE e i punti di ingresso OLE COM. Come qualsiasi altro punto di ingresso esposto al sistema, questi punti di ingresso devono impostare lo stato corretto del modulo. In che modo un dato CCmdTarget sa quale stato del modulo "corretto" deve essere La risposta è che "ricorda" qual è lo stato del modulo "corrente" quando viene costruito, in modo che possa impostare lo stato del modulo corrente su quel valore "memorizzato" quando viene chiamato in un secondo momento. Di conseguenza, lo stato del modulo a cui è associato un determinato CCmdTarget oggetto è lo stato del modulo corrente al momento della costruzione dell'oggetto. Si prenda un semplice esempio di caricamento di un server INPROC, della creazione di un oggetto e della chiamata dei relativi metodi.

  1. La DLL viene caricata da OLE tramite LoadLibrary.

  2. RawDllMain viene chiamato per primo. Imposta lo stato del modulo sullo stato del modulo statico noto per la DLL. Per questo motivo RawDllMain è collegato staticamente alla DLL.

  3. Viene chiamato il costruttore per la class factory associata all'oggetto . COleObjectFactory deriva da CCmdTarget e, di conseguenza, ricorda in quale stato del modulo è stata creata un'istanza. Questo aspetto è importante: quando viene chiesto alla class factory di creare oggetti, ora lo stato del modulo da rendere corrente.

  4. DllGetClassObject viene chiamato per ottenere la class factory. MFC cerca l'elenco di class factory associato a questo modulo e lo restituisce.

  5. Viene chiamato COleObjectFactory::XClassFactory2::CreateInstance. Prima di creare l'oggetto e restituirlo, questa funzione imposta lo stato del modulo sullo stato del modulo corrente nel passaggio 3 (quello corrente quando è stata creata un'istanza COleObjectFactory di ). Questa operazione viene eseguita all'interno di METHOD_PROLOGUE.

  6. Quando l'oggetto viene creato, anche esso è un CCmdTarget derivato e nello stesso modo in cui COleObjectFactory è stato ricordato quale stato del modulo era attivo, quindi questo nuovo oggetto. Ora l'oggetto conosce lo stato del modulo a cui passare ogni volta che viene chiamato.

  7. Il client chiama una funzione sull'oggetto OLE COM ricevuto dalla chiamata CoCreateInstance . Quando l'oggetto viene chiamato viene usato METHOD_PROLOGUE per cambiare lo stato del modulo esattamente come COleObjectFactory accade.

Come si può notare, lo stato del modulo viene propagato dall'oggetto all'oggetto durante la creazione. È importante impostare lo stato del modulo in modo appropriato. Se non è impostata, l'oggetto DLL o COM potrebbe interagire in modo non corretto con un'applicazione MFC che la chiama o potrebbe non essere in grado di trovare le proprie risorse oppure potrebbe non riuscire in altri modi infelice.

Si noti che alcuni tipi di DLL, in particolare le DLL "estensione MFC" non cambiano lo stato del modulo nel relativo RawDllMain (in realtà, in genere non hanno nemmeno un ).RawDllMain Ciò è dovuto al fatto che hanno lo scopo di comportarsi come se fossero effettivamente presenti nell'applicazione che li usa. Sono molto parte dell'applicazione in esecuzione ed è la loro intenzione di modificare lo stato globale dell'applicazione.

I controlli OLE e altre DLL sono molto diversi. Non vogliono modificare lo stato dell'applicazione chiamante; l'applicazione che li chiama potrebbe non essere nemmeno un'applicazione MFC e quindi potrebbe non esserci stato da modificare. Questo è il motivo per cui è stato inventato il cambio di stato del modulo.

Per le funzioni esportate da una DLL, ad esempio una finestra di dialogo che avvia una finestra di dialogo nella DLL, è necessario aggiungere il codice seguente all'inizio della funzione:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

In questo modo lo stato del modulo corrente viene scambiato con lo stato restituito da AfxGetStaticModuleState fino alla fine dell'ambito corrente.

Se la macro AFX_MODULE_STATE non viene utilizzata, si verificheranno problemi con le risorse nelle DLL. Per impostazione predefinita, MFC utilizza il gestore delle risorse dell'applicazione principale per caricare il modello di risorsa. Questo modello viene effettivamente archiviato nella DLL. La causa principale è che le informazioni sullo stato del modulo di MFC non sono state cambiate dalla macro AFX_MODULE_STATE. Il gestore delle risorse viene recuperato dallo stato del modulo di MFC. Non scambiare lo stato del modulo provoca l'utilizzo errato del gestore delle risorse.

AFX_MODULE_STATE non è necessario inserire tutte le funzioni nella DLL. Ad esempio, InitInstance può essere chiamato dal codice MFC nell'applicazione senza AFX_MODULE_STATE perché MFC sposta automaticamente lo stato del modulo prima InitInstance e quindi lo ripristina dopo InitInstance la restituzione. Lo stesso vale per tutti i gestori mappa messaggi. Le DLL MFC regolari hanno effettivamente una routine speciale della finestra master che cambia automaticamente lo stato del modulo prima di instradare qualsiasi messaggio.

Elaborare i dati locali

Elaborare i dati locali non sarebbe di un problema così importante se non fosse stato per la difficoltà del modello DLL Win32s. In Win32 tutte le DLL condividono i dati globali, anche se caricati da più applicazioni. Questo comportamento è molto diverso dal modello di dati DLL Win32 "reale", in cui ogni DLL ottiene una copia separata dello spazio dati in ogni processo che si collega alla DLL. Per aggiungere alla complessità, i dati allocati nell'heap in una DLL Win32s sono infatti specifici del processo (almeno per quanto riguarda la proprietà). Considerare i dati e il codice seguenti:

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

Si consideri cosa accade se il codice precedente si trova in una DLL e tale DLL viene caricata da due processi A e B (potrebbe, infatti, essere due istanze della stessa applicazione). Un oggetto chiama SetGlobalString("Hello from A"). Di conseguenza, la memoria viene allocata per i CString dati nel contesto del processo A. Tenere presente che l'oggetto CString stesso è globale ed è visibile sia a A che A. B. Ora B chiama GetGlobalString(sz, sizeof(sz)). B sarà in grado di visualizzare i dati impostati da A. Ciò è dovuto al fatto che Win32s non offre alcuna protezione tra processi come Win32. Questo è il primo problema; in molti casi non è consigliabile che un'applicazione influisca sui dati globali considerati di proprietà di un'applicazione diversa.

Ci sono anche altri problemi. Diciamo che A ora esce. Quando si esce da A, la memoria utilizzata dalla stringa 'strGlobal' viene resa disponibile per il sistema, ovvero tutta la memoria allocata dal processo A viene liberata automaticamente dal sistema operativo. Non viene liberato perché viene chiamato il CString distruttore, ma non è ancora stato chiamato. Viene liberato semplicemente perché l'applicazione allocata ha lasciato la scena. Ora, se B ha chiamato GetGlobalString(sz, sizeof(sz)), potrebbe non ottenere dati validi. È possibile che un'altra applicazione abbia usato tale memoria per qualcos'altro.

Esiste chiaramente un problema. MFC 3.x ha usato una tecnica denominata thread-local storage (TLS). MFC 3.x alloca un indice TLS che in Win32s funge effettivamente da indice di archiviazione locale del processo, anche se non viene chiamato e quindi fa riferimento a tutti i dati basati su tale indice TLS. Questo è simile all'indice TLS usato per archiviare i dati locali del thread in Win32 (vedere di seguito per altre informazioni sull'oggetto). In questo modo ogni DLL MFC usa almeno due indici TLS per processo. Quando si tiene conto del caricamento di molte DLL di controllo OLE (OCX), si esauriscono rapidamente gli indici TLS (sono disponibili solo 64). Inoltre, MFC doveva inserire tutti questi dati in un'unica posizione, in una singola struttura. Non era molto estendibile e non era ideale per quanto riguarda l'uso di indici TLS.

MFC 4.x risolve questo problema con un set di modelli di classe che è possibile "eseguire il wrapping" dei dati che devono essere elaborati localmente. Ad esempio, il problema menzionato sopra potrebbe essere risolto scrivendo:

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 implementa questa operazione in due passaggi. In primo luogo, esiste un livello sopra le API Tls* Win32 (TlsAlloc, TlsSetValue, TlsGetValue e così via) che usano solo due indici TLS per processo, indipendentemente dal numero di DLL disponibili. In secondo luogo, il CProcessLocal modello viene fornito per accedere a questi dati. Esegue l'override dell'operatore,> che consente la sintassi intuitiva visualizzata in precedenza. Tutti gli oggetti di cui è stato eseguito il wrapping devono CProcessLocal essere derivati da CNoTrackObject. CNoTrackObject fornisce un allocatore di livello inferiore (LocalAlloc/LocalFree) e un distruttore virtuale in modo che MFC possa eliminare automaticamente gli oggetti locali del processo quando il processo viene terminato. Tali oggetti possono avere un distruttore personalizzato se è necessaria una pulizia aggiuntiva. L'esempio precedente non ne richiede uno, perché il compilatore genererà un distruttore predefinito per eliminare definitivamente l'oggetto incorporato CString .

Esistono altri vantaggi interessanti per questo approccio. Non solo tutti gli CProcessLocal oggetti vengono distrutti automaticamente, non vengono costruiti fino a quando non sono necessari. CProcessLocal::operator-> crea un'istanza dell'oggetto associato la prima volta che viene chiamata e non prima. Nell'esempio precedente, significa che la stringa 'strGlobal' non verrà costruita fino alla prima chiamata o GetGlobalString alla prima chiamataSetGlobalString. In alcuni casi, ciò può contribuire a ridurre il tempo di avvio della DLL.

Thread Local Data

Analogamente all'elaborazione dei dati locali, i dati locali del thread vengono usati quando i dati devono essere locali per un determinato thread. Ciò significa che è necessaria un'istanza separata dei dati per ogni thread che accede ai dati. Questa operazione può essere usata molte volte al posto di meccanismi di sincronizzazione estesi. Se i dati non devono essere condivisi da più thread, tali meccanismi possono essere costosi e non necessari. Si supponga di avere un CString oggetto (molto simile all'esempio precedente). È possibile rendere il thread locale eseguendo il wrapping con un CThreadLocal modello:

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 stato chiamato da due thread diversi, ognuna "shuffle" la stringa in modi diversi senza interferire con l'altro. Ciò è dovuto al fatto che in realtà è presente un'istanza strThread per ogni thread anziché una sola istanza globale.

Si noti come viene usato un riferimento per acquisire l'indirizzo CString una sola volta anziché una volta per iterazione del ciclo. Il codice del ciclo potrebbe essere stato scritto con threadData->strThread ovunque venga usato "str", ma il codice sarebbe molto più lento nell'esecuzione. È consigliabile memorizzare nella cache un riferimento ai dati quando tali riferimenti si verificano in cicli.

Il CThreadLocal modello di classe usa gli stessi meccanismi che CProcessLocal e le stesse tecniche di implementazione.

Vedi anche

Note tecniche per numero
Note tecniche per categoria