Delen via


TN058: Implementatie van de MFC-moduletoestand

Opmerking

De volgende technische opmerking is niet bijgewerkt sinds deze voor het eerst is opgenomen in de onlinedocumentatie. Als gevolg hiervan zijn sommige procedures en onderwerpen mogelijk verouderd of onjuist. Voor de meest recente informatie is het raadzaam om te zoeken naar het onderwerp waarin u geïnteresseerd bent in de onlinedocumentatie-index.

In deze technische opmerking wordt de implementatie van MFC -modulestatusconstructies beschreven. Een goed begrip van de implementatie van de modulestatus is essentieel voor het gebruik van de gedeelde MFC-DLL's van een DLL (of OLE-in-process server).

Voordat u deze opmerking leest, raadpleegt u 'De statusgegevens van MFC-modules beheren' in Het maken van nieuwe documenten, Windows en weergaven. Dit artikel bevat belangrijke gebruiksgegevens en overzichtsinformatie over dit onderwerp.

Overzicht

Er zijn drie soorten MFC-statusinformatie: modulestatus, processtatus en threadstatus. Soms kunnen deze statustypen worden gecombineerd. De handle-kaarten voor MFC zijn bijvoorbeeld zowel module-lokaal als draad-lokaal. Hierdoor kunnen twee verschillende modules verschillende kaarten hebben in elk van hun threads.

Processtatus en threadstatus zijn vergelijkbaar. Deze gegevensitems zijn van oudsher globale variabelen, maar moeten specifiek zijn voor een bepaald proces of thread voor de juiste Win32s-ondersteuning of voor de juiste ondersteuning voor multithreading. Welke categorie een bepaald gegevensitem past, is afhankelijk van dat item en de gewenste semantiek met betrekking tot proces- en threadgrenzen.

Moduletoestand is uniek omdat het een werkelijk globale toestand kan bevatten of een toestand die proces-lokaal of draad-lokaal is. Bovendien kan het snel worden omgeschakeld.

Modulestatuswisseling

Elke thread bevat een aanwijzer naar de status "huidige" of "actieve" module (niet verrassend maakt de aanwijzer deel uit van de lokale status van de thread van MFC). Deze aanwijzer wordt gewijzigd wanneer de thread van uitvoering een modulegrens doorgeeft, zoals een toepassing die een OLE-besturingselement of DLL aanroept, of een OLE-besturingselement dat terugroept naar een toepassing.

De huidige modulestatus wordt gewijzigd door aan te roepen AfxSetModuleState. Voor het grootste deel zult u nooit rechtstreeks met de API omgaan. MFC zal het in veel gevallen voor u oproepen (bij WinMain, OLE-invoerpunten, AfxWndProc, enzovoort). Dit wordt gedaan in elk onderdeel dat u schrijft door statisch te koppelen in een speciale WndProc, en een speciale WinMain (of DllMain) die weet welke modulestatus actueel moet zijn. U kunt deze code zien door naar DLLMODUL te kijken. CPP of APPMODUL. CPP in de map MFC\SRC.

Het is zeldzaam dat u de modulestatus wilt instellen en deze vervolgens niet opnieuw wilt instellen. Meestal wilt u de status van uw eigen module als de huidige instellen en vervolgens, nadat u klaar bent, de oorspronkelijke context herstellen. Dit wordt gedaan door de macro AFX_MANAGE_STATE en de speciale klasse AFX_MAINTAIN_STATE.

CCmdTarget heeft speciale functies voor het ondersteunen van modulestatuswisselingen. Met name is CCmdTarget de hoofdklasse die wordt gebruikt voor OLE-automatisering en OLE COM-toegangspunten. Net als elk ander toegangspunt dat aan het systeem wordt blootgesteld, moeten deze toegangspunten de juiste modulestatus instellen. Hoe weet een gegeven CCmdTarget wat de 'juiste' modulestatus moet zijn. Het antwoord is dat deze de status van de huidige module 'onthoudt' wanneer deze wordt samengesteld, zodat de huidige modulestatus kan worden ingesteld op die 'onthouden'-waarde wanneer deze later wordt aangeroepen. Als gevolg hiervan is de modulestatus waaraan een bepaald CCmdTarget object is gekoppeld, de modulestatus die actueel was toen het object werd gemaakt. Neem een eenvoudig voorbeeld van het laden van een INPROC-server, het maken van een object en het aanroepen van de methoden.

  1. Het DLL-bestand wordt geladen door OLE met behulp van LoadLibrary.

  2. RawDllMain wordt eerst genoemd. De modulestatus wordt ingesteld op de status van de bekende statische module voor het DLL-bestand. Om deze reden is RawDllMain statisch gelinkt aan de DLL.

  3. De constructor voor de klassefactory die aan ons object is gekoppeld, wordt aangeroepen. COleObjectFactory is afgeleid van CCmdTarget en als gevolg hiervan wordt onthouden in welke modulestatus deze is geïnstantieerd. Dit is belangrijk — wanneer de klassefabriek wordt gevraagd om objecten te maken, weet deze nu welke modulestatus actueel moet worden gemaakt.

  4. DllGetClassObject wordt aangeroepen om de klassefactory te verkrijgen. MFC doorzoekt de klassefactorylijst die aan deze module is gekoppeld en retourneert deze.

  5. COleObjectFactory::XClassFactory2::CreateInstance wordt aangeroepen. Voordat u het object maakt en retourneert, stelt deze functie de modulestatus in op de modulestatus die actueel was in stap 3 (de status die actueel was toen de COleObjectFactory instantie werd geïnstantieerd). Dit gebeurt in METHOD_PROLOGUE.

  6. Wanneer het object wordt gemaakt, is het ook een CCmdTarget afgeleide en op dezelfde wijze COleObjectFactory onthouden welke modulestatus actief was, net als dit nieuwe object. Nu weet het object naar welke modulestatus moet worden overgeschakeld wanneer het wordt aangeroepen.

  7. De client roept een functie aan op het OLE COM-object dat het van zijn CoCreateInstance-aanroep heeft ontvangen. Wanneer het object wordt aangeroepen, gebruikt het METHOD_PROLOGUE om de module status te wijzigen, net zoals COleObjectFactory doet.

Zoals u kunt zien, wordt de modulestatus doorgegeven van object naar object terwijl ze worden gemaakt. Het is belangrijk dat de modulestatus juist is ingesteld. Als het niet is ingesteld, kan uw DLL- of COM-object slecht communiceren met een MFC-toepassing die het aanroept, of kan het zijn eigen resources niet vinden of op andere ellendige manieren mislukken.

Houd er rekening mee dat bepaalde soorten DLL's, met name 'MFC-extensie'-DLL's, de modulestatus niet wijzigen in hun RawDllMain (eigenlijk hebben ze meestal niet eens een RawDllMain). Dit komt doordat ze zijn bedoeld om zich te gedragen alsof ze daadwerkelijk aanwezig waren in de toepassing die ze gebruikt. Ze maken heel veel deel uit van de toepassing die wordt uitgevoerd en het is de bedoeling om de globale status van die toepassing te wijzigen.

OLE-besturingselementen en andere DLL's zijn heel anders. Ze willen de status van de aanroepende toepassing niet wijzigen; de toepassing die hen aanroept, is mogelijk niet eens een MFC-toepassing en er is dus mogelijk geen status om te wijzigen. Dit is de reden dat modulestatuswisseling is uitgevonden.

Voor geëxporteerde functies uit een DLL, zoals een functie waarmee een dialoogvenster in uw DLL wordt gestart, moet u de volgende code toevoegen aan het begin van de functie:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Hiermee wordt de huidige modulestatus verwisseld met de status die wordt geretourneerd door AfxGetStaticModuleState tot het einde van het huidige bereik.

Problemen met resources in DLL's treden op als de AFX_MODULE_STATE macro niet wordt gebruikt. MFC gebruikt standaard de resource-handle van de hoofdtoepassing om de resource-sjabloon te laden. Deze sjabloon wordt daadwerkelijk opgeslagen in het DLL-bestand. De hoofdoorzaak is dat de modulestatusgegevens van MFC niet zijn gewijzigd door de AFX_MODULE_STATE macro. De resourcehandler wordt hersteld van de modulestatus van MFC. Als u niet overschakelt naar de modulestatus, wordt de verkeerde resource-handle gebruikt.

AFX_MODULE_STATE hoeft niet in elke functie van de DLL te worden geplaatst. Bijvoorbeeld, InitInstance kan worden aangeroepen door de MFC-code in de toepassing zonder AFX_MODULE_STATE, omdat MFC de modulestatus automatisch wijzigt voordat InitInstance en daarna terugzet nadat InitInstance is geretourneerd. Hetzelfde geldt voor alle berichtafhandelingshandlers. Reguliere MFC-DLL's hebben eigenlijk een speciale mastervensterprocedure waarmee de modulestatus automatisch wordt gewijzigd voordat een bericht wordt gerouterd.

Lokale gegevens verwerken

Lokale gegevens verwerken zou niet van zo'n grote zorg zijn geweest als het niet was voor de moeilijkheid van het Win32s DLL-model. In Win32s delen alle DLL's hun globale gegevens, zelfs wanneer ze door meerdere toepassingen worden geladen. Dit verschilt van het 'echte' Win32 DLL-gegevensmodel, waarbij elke DLL een afzonderlijke kopie van de gegevensruimte krijgt in elk proces dat aan het DLL-bestand wordt gekoppeld. Om de complexiteit verder te vergroten, is data die aan de heap in een Win32s DLL is toegewezen in feite proces-specifiek, althans in termen van eigendomsrechten. Houd rekening met de volgende gegevens en code:

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

Bedenk wat er gebeurt als de bovenstaande code zich in een DLL bevindt en dat DLL wordt geladen door twee processen A en B (het kan in feite twee exemplaren van dezelfde toepassing zijn). Een telefoontje SetGlobalString("Hello from A"). Hierdoor wordt geheugen toegewezen aan de CString gegevens in de context van proces A. Houd er rekening mee dat het CString zelf globaal is en zichtbaar is voor zowel A als B. Nu B-oproepen GetGlobalString(sz, sizeof(sz)). B kan de gegevens zien die door A zijn ingesteld. Dit komt doordat Win32s geen bescherming biedt tussen processen zoals Win32 dat wel doet. Dat is het eerste probleem; In veel gevallen is het niet wenselijk dat één toepassing invloed heeft op globale gegevens die als eigendom van een andere toepassing worden beschouwd.

Er zijn ook extra problemen. Stel dat A nu vertrekt. Wanneer A wordt afgesloten, wordt het geheugen dat wordt gebruikt door de tekenreeks 'strGlobal' beschikbaar gesteld voor het systeem, dat wil gezegd, wordt alle geheugen die door proces A wordt toegewezen, automatisch vrijgemaakt door het besturingssysteem. Het is niet vrijgemaakt omdat de CString destructor wordt aangeroepen; het is nog niet aangeroepen. Het is vrijgegeven simpelweg omdat de toepassing die deze heeft toegewezen, is gestopt met functioneren. Als B GetGlobalString(sz, sizeof(sz)) aanroept, kan het zijn dat er ongeldige gegevens worden verkregen. Sommige andere toepassingen hebben dat geheugen mogelijk gebruikt voor iets anders.

Er bestaat duidelijk een probleem. MFC 3.x gebruikte een techniek genaamd thread-local storage (TLS). MFC 3.x zou een TLS-index toewijzen die onder Win32s echt fungeert als een proces-lokale opslagindex, ook al wordt dit niet genoemd en zou vervolgens verwijzen naar alle gegevens op basis van die TLS-index. Dit is vergelijkbaar met de TLS-index die is gebruikt voor het opslaan van thread-lokale gegevens op Win32 (zie hieronder voor meer informatie over dat onderwerp). Hierdoor heeft elke MFC DLL ten minste twee TLS-indexen per proces gebruikt. Wanneer u rekening houdt met het laden van veel OLE-besturings-DLL's (OCX's), hebt u snel geen TLS-indexen meer (er zijn slechts 64 beschikbaar). Daarnaast moest MFC al deze gegevens op één plaats in één structuur plaatsen. Het was niet erg uitbreidbaar en was niet ideaal met betrekking tot het gebruik van TLS-indexen.

MFC 4.x behandelt dit met een set klassesjablonen die u kunt 'verpakken' rond de gegevens die lokaal moeten worden verwerkt. Het hierboven genoemde probleem kan bijvoorbeeld worden opgelost door te schrijven:

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 implementeert dit in twee stappen. Ten eerste is er een laag boven op de Win32 Tls* -API's (TlsAlloc, TlsSetValue, TlsGetValue, enzovoort) die slechts twee TLS-indexen per proces gebruiken, ongeacht het aantal DLL's dat u hebt. Ten tweede wordt de CProcessLocal sjabloon verstrekt voor toegang tot deze gegevens. Het overschrijft operator> waardoor de intuïtieve syntaxis die u hierboven ziet mogelijk wordt gemaakt. Alle objecten die zijn verpakt door CProcessLocal , moeten worden afgeleid van CNoTrackObject. CNoTrackObject biedt een allocator op lager niveau (LocalAlloc/LocalFree) en een virtuele destructor, zodat MFC het proces lokale objecten automatisch kan vernietigen wanneer het proces wordt beëindigd. Dergelijke objecten kunnen een aangepaste destructor hebben als extra opschoning vereist is. In het bovenstaande voorbeeld is er geen vereiste, omdat de compiler een standaarddestructor genereert om het ingesloten CString object te vernietigen.

Er zijn andere interessante voordelen voor deze aanpak. Niet alleen worden alle CProcessLocal objecten automatisch vernietigd, ze worden pas gebouwd als ze nodig zijn. CProcessLocal::operator-> zal het gekoppelde object voor de eerste keer instantiëren wanneer het wordt aangeroepen, en niet eerder. In het bovenstaande voorbeeld betekent dat de tekenreeks strGlobal pas na de eerste keer dat SetGlobalString of GetGlobalString wordt aangeroepen, wordt samengesteld. In sommige gevallen kan dit helpen bij het verminderen van de opstarttijd van DLL.

Lokale threadgegevens

Net als bij het verwerken van lokale gegevens worden lokale threadgegevens gebruikt wanneer de gegevens lokaal moeten zijn voor een bepaalde thread. Dat wil gezegd dat u een afzonderlijk exemplaar van de gegevens nodig hebt voor elke thread die toegang heeft tot die gegevens. Dit kan vaak worden gebruikt in plaats van uitgebreide synchronisatiemechanismen. Als de gegevens niet door meerdere threads hoeven te worden gedeeld, kunnen dergelijke mechanismen duur en onnodig zijn. Stel dat we een CString object hadden (net als in de bovenstaande steekproef). We kunnen de thread lokaal maken door deze te verpakken met een CThreadLocal sjabloon:

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

Als MakeRandomString vanuit twee verschillende threads werd aangeroepen, zou elke thread de tekenreeks op een andere manier 'door elkaar husselen' zonder elkaar te verstoren. Dit komt doordat er eigenlijk een strThread exemplaar per thread is in plaats van slechts één globaal exemplaar.

U ziet hoe een verwijzing wordt gebruikt om het CString adres eenmaal vast te leggen in plaats van één keer per lusiteratie. De luscode kan zijn geschreven met threadData->strThread overal waar 'str' wordt gebruikt, maar de code zou veel trager in de uitvoering zijn. Het is raadzaam om een verwijzing naar de gegevens op te cachen wanneer dergelijke verwijzingen in lussen voorkomen.

De CThreadLocal klassesjabloon maakt gebruik van dezelfde mechanismen die CProcessLocal wel en dezelfde implementatietechnieken gebruiken.

Zie ook

Technische notities per nummer
Technische Aantekeningen Per Categorie