TN058 : implémentation de l'état du module MFC

Remarque

La note technique suivante n'a pas été mise à jour depuis son inclusion initiale dans la documentation en ligne. Par conséquent, certaines procédures et rubriques peuvent être obsolètes ou incorrectes. Pour obtenir les informations les plus récentes, il est recommandé de rechercher l'objet qui vous intéresse dans l'index de la documentation en ligne.

Cette note technique décrit l’implémentation des constructions « état du module » MFC. Une compréhension de l’implémentation de l’état du module est essentielle pour l’utilisation des DLL partagées MFC à partir d’une DLL (ou d’un serveur ole in-process).

Avant de lire cette note, reportez-vous à « Gestion des données d’état des modules MFC » dans la création de documents, windows et vues. Cet article contient des informations importantes sur l’utilisation et des informations de vue d’ensemble sur ce sujet.

Vue d’ensemble

Il existe trois types d’informations d’état MFC : État du module, État du processus et État du thread. Parfois, ces types d’état peuvent être combinés. Par exemple, les cartes de handle MFC sont à la fois locales de module et de thread local. Cela permet à deux modules différents d’avoir des mappages différents dans chacun de leurs threads.

L’état du processus et l’état du thread sont similaires. Ces éléments de données sont des éléments qui ont traditionnellement été des variables globales, mais qui doivent être spécifiques à un processus ou un thread donné pour une prise en charge appropriée de Win32s ou pour une prise en charge multithreading appropriée. La catégorie dans laquelle un élément de données donné s’intègre dépend de cet élément et de sa sémantique souhaitée en ce qui concerne les limites de processus et de thread.

L’état du module est unique dans lequel il peut contenir un état véritablement global ou un état qui est le processus local ou le thread local. En outre, il peut être basculé rapidement.

Changement d’état du module

Chaque thread contient un pointeur vers l’état du module « actuel » ou « actif » (pas surprenant, le pointeur fait partie de l’état local du thread de MFC). Ce pointeur est modifié lorsque le thread d’exécution transmet une limite de module, telle qu’une application appelant dans un contrôle OLE ou une DLL, ou lorsqu’un contrôle OLE revient dans une application.

L’état actuel du module est basculé en appelant AfxSetModuleState. Pour la plupart, vous ne traiterez jamais directement avec l’API. MFC, dans de nombreux cas, l’appellera pour vous (à WinMain, points d’entrée OLE, AfxWndProcetc.). Cela s’effectue dans n’importe quel composant que vous écrivez en liant statiquement dans un élément spécial WndProcet un (ouDllMain) spécial WinMain qui sait quel état de module doit être actif. Vous pouvez voir ce code en examinant DLLMODUL. CPP ou APPMODUL. CPP dans le répertoire MFC\SRC.

Il est rare que vous souhaitiez définir l’état du module, puis ne pas le définir. La plupart du temps, vous souhaitez « envoyer » votre propre état de module en tant qu’état actuel, puis, une fois que vous avez terminé, « pop » le contexte d’origine. Cette opération est effectuée par la macro AFX_MANAGE_STATE et la classe AFX_MAINTAIN_STATEspéciale .

CCmdTarget dispose de fonctionnalités spéciales pour prendre en charge le changement d’état du module. En particulier, il CCmdTarget s’agit de la classe racine utilisée pour les points d’entrée OLE Automation et OLE COM. Comme tout autre point d’entrée exposé au système, ces points d’entrée doivent définir l’état correct du module. Comment une donnée CCmdTarget sait-elle quel est l’état du module « correct » doit être La réponse est qu’elle « mémorise » ce que l’état du module « actuel » est lorsqu’il est construit, de sorte qu’il peut définir l’état du module actuel sur cette valeur « mémorisée » quand elle est appelée ultérieurement. Par conséquent, l’état du module auquel un objet donné CCmdTarget est associé est l’état du module qui était actuel lorsque l’objet a été construit. Prenez un exemple simple de chargement d’un serveur INPROC, de la création d’un objet et de l’appel de ses méthodes.

  1. La DLL est chargée par OLE à l’aide LoadLibraryde .

  2. RawDllMain est appelé en premier. Il définit l’état du module sur l’état du module statique connu pour la DLL. Pour cette raison RawDllMain , il est lié statiquement à la DLL.

  3. Le constructeur de la fabrique de classes associée à notre objet est appelé. COleObjectFactory est dérivé CCmdTarget et en conséquence, il se rappelle dans quel état de module il a été instancié. Cela est important : lorsque la fabrique de classes est invitée à créer des objets, elle sait maintenant quel état de module rendre actuel.

  4. DllGetClassObject est appelé pour obtenir la fabrique de classe. MFC recherche la liste de fabriques de classes associée à ce module et la retourne.

  5. COleObjectFactory::XClassFactory2::CreateInstance est appelée. Avant de créer l’objet et de le renvoyer, cette fonction définit l’état du module sur l’état du module qui était actif à l’étape 3 (celui qui était actuel lors de l’instanciation COleObjectFactory ). Cela s’effectue à l’intérieur de METHOD_PROLOGUE.

  6. Lorsque l’objet est créé, il s’agit également d’un CCmdTarget dérivé et, de la même façon COleObjectFactory , rappelez-vous que l’état du module était actif. Ainsi, ce nouvel objet est-il actif. À présent, l’objet connaît l’état du module auquel basculer chaque fois qu’il est appelé.

  7. Le client appelle une fonction sur l’objet OLE COM qu’il a reçu de son CoCreateInstance appel. Lorsque l’objet est appelé, il utilise METHOD_PROLOGUE pour changer l’état du module comme COleObjectFactory c’est le cas.

Comme vous pouvez le voir, l’état du module est propagé de l’objet à l’objet lors de leur création. Il est important que l’état du module soit correctement défini. S’il n’est pas défini, votre DLL ou votre objet COM peut interagir mal avec une application MFC qui l’appelle, ou peut être incapable de trouver ses propres ressources, ou peut échouer d’autres façons misérables.

Notez que certains types de DLL, en particulier les DLL « Extension MFC » ne changent pas l’état du module dans leur RawDllMain (en fait, ils n’ont généralement pas de RawDllMainDLL). Cela est dû au fait qu’ils sont destinés à se comporter comme s’ils étaient réellement présents dans l’application qui les utilise. Elles font très partie de l’application en cours d’exécution et il s’agit de leur intention de modifier l’état global de cette application.

Les contrôles OLE et d’autres DLL sont très différents. Ils ne souhaitent pas modifier l’état de l’application appelante ; l’application qui les appelle peut même ne pas être une application MFC et il peut donc n’y avoir aucun état à modifier. C’est la raison pour laquelle le changement d’état du module a été inventé.

Pour les fonctions exportées à partir d’une DLL, comme une boîte de dialogue qui lance une boîte de dialogue dans votre DLL, vous devez ajouter le code suivant au début de la fonction :

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Cela échange l’état actuel du module avec l’état retourné par AfxGetStaticModuleState jusqu’à la fin de l’étendue actuelle.

Les problèmes liés aux ressources dans les DLL se produisent si la macro AFX_MODULE_STATE n’est pas utilisée. Par défaut, MFC utilise le handle de ressource d'application principale pour charger le modèle de ressources. Ce modèle est réellement enregistré dans la DLL. La cause racine est que les informations d’état du module MFC n’ont pas été basculées par la macro AFX_MODULE_STATE. Le handle de ressources est récupéré de l'état du module MFC. Ne pas afficher l'état du module provoque l'utilisation du mauvais handle de ressource.

AFX_MODULE_STATE n’a pas besoin d’être placé dans chaque fonction de la DLL. Par exemple, InitInstance vous pouvez appeler le code MFC dans l’application sans AFX_MODULE_STATE, car MFC déplace automatiquement l’état du module avant InitInstance , puis le réactive après InitInstance le retour. Il en va de même pour tous les gestionnaires de mappage de messages. Les DLL MFC standard ont en fait une procédure de fenêtre maître spéciale qui bascule automatiquement l’état du module avant de router un message.

Traiter les données locales

Le traitement des données locales ne serait pas aussi préoccupant si ce n’était pas pour la difficulté du modèle DLL Win32s. Dans Win32s, toutes les DLL partagent leurs données globales, même lorsqu’elles sont chargées par plusieurs applications. Ceci est très différent du modèle de données WIN32 DLL « réel », où chaque DLL obtient une copie distincte de son espace de données dans chaque processus qui s’attache à la DLL. Pour ajouter à la complexité, les données allouées sur le tas dans une DLL Win32s sont en fait spécifiques au processus (au moins en ce qui concerne la propriété). Tenez compte des données et du code suivants :

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

Considérez ce qui se passe si le code ci-dessus se trouve dans une DLL et que la DLL est chargée par deux processus A et B (il peut en fait s’agir de deux instances de la même application). Un appel SetGlobalString("Hello from A"). Par conséquent, la mémoire est allouée pour les CString données dans le contexte du processus A. Gardez à l’esprit que la CString mémoire elle-même est globale et est visible à la fois par A et B. Maintenant B appelle GetGlobalString(sz, sizeof(sz)). B pourra voir les données définies par A. Cela est dû au fait que Win32s n’offre aucune protection entre les processus comme Win32. C’est le premier problème ; dans de nombreux cas, il n’est pas souhaitable qu’une application affecte les données globales considérées comme appartenant à une autre application.

Il y a également des problèmes supplémentaires. Supposons que A quitte maintenant. Quand A se ferme, la mémoire utilisée par la chaîne «strGlobal » est mise à disposition pour le système, autrement dit, toute la mémoire allouée par le processus A est libérée automatiquement par le système d’exploitation. Il n’est pas libéré parce que le CString destructeur est appelé ; il n’a pas encore été appelé. Il est libéré simplement parce que l’application qui l’a allouée a quitté la scène. Maintenant, si B a appelé GetGlobalString(sz, sizeof(sz)), il peut ne pas obtenir de données valides. Une autre application peut avoir utilisé cette mémoire pour autre chose.

Il existe clairement un problème. MFC 3.x a utilisé une technique appelée stockage local thread(TLS). MFC 3.x alloue un index TLS qui, sous Win32s, agit vraiment comme un index de stockage local de processus, même s’il n’est pas appelé, puis référence toutes les données basées sur cet index TLS. Ceci est similaire à l’index TLS utilisé pour stocker des données locales de thread sur Win32 (voir ci-dessous pour plus d’informations sur ce sujet). Cela a provoqué l’utilisation d’au moins deux index TLS par processus par DLL MFC. Lorsque vous comptez charger de nombreuses DLL OLE Control (OCX), vous exécutez rapidement des index TLS (il n’y a que 64 personnes disponibles). En outre, MFC a dû placer toutes ces données à un seul endroit, dans une seule structure. Il n’était pas très extensible et n’était pas idéal en ce qui concerne son utilisation des index TLS.

MFC 4.x traite cela avec un ensemble de modèles de classe que vous pouvez « wrapper » autour des données qui doivent être traitées localement. Par exemple, le problème mentionné ci-dessus peut être résolu en écrivant :

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 implémente cela en deux étapes. Tout d’abord, il existe une couche sur les API Win32 Tls* (TlsAlloc, TlsSetValue, TlsGetValue, etc.) qui utilisent uniquement deux index TLS par processus, quel que soit le nombre de DLL dont vous disposez. Deuxièmement, le CProcessLocal modèle est fourni pour accéder à ces données. Il remplace l’opérateur,> qui permet la syntaxe intuitive que vous voyez ci-dessus. Tous les objets encapsulés doivent être dérivés CProcessLocal de CNoTrackObject. CNoTrackObject fournit un allocateur de niveau inférieur (LocalAlloc/LocalFree) et un destructeur virtuel de sorte que MFC puisse détruire automatiquement les objets locaux de processus lorsque le processus est arrêté. Ces objets peuvent avoir un destructeur personnalisé si d’autres propre up sont nécessaires. L’exemple ci-dessus n’en nécessite pas, car le compilateur génère un destructeur par défaut pour détruire l’objet incorporé CString .

Cette approche présente d’autres avantages intéressants. Non seulement tous les CProcessLocal objets sont détruits automatiquement, ils ne sont pas construits tant qu’ils ne sont pas nécessaires. CProcessLocal::operator-> instancie l’objet associé la première fois qu’il est appelé, et pas plus tôt. Dans l’exemple ci-dessus, cela signifie que la chaîne «strGlobal » ne sera pas construite tant que la première fois SetGlobalString ou GetGlobalString qu’elle n’est pas appelée. Dans certains cas, cela peut aider à réduire le temps de démarrage de la DLL.

Données locales de thread

Comme pour traiter les données locales, les données locales de thread sont utilisées lorsque les données doivent être locales dans un thread donné. Autrement dit, vous avez besoin d’une instance distincte des données pour chaque thread qui accède à ces données. Cela peut être utilisé plusieurs fois au lieu de mécanismes de synchronisation étendus. Si les données n’ont pas besoin d’être partagées par plusieurs threads, ces mécanismes peuvent être coûteux et inutiles. Supposons que nous avions un CString objet (comme l’exemple ci-dessus). Nous pouvons le rendre local de thread en les encapsulant avec un CThreadLocal modèle :

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

S’il MakeRandomString a été appelé à partir de deux threads différents, chacun d’eux « démêle » la chaîne de différentes manières sans interférer avec l’autre. Cela est dû au fait qu’il existe une strThread instance par thread au lieu d’une seule instance globale.

Notez comment une référence est utilisée pour capturer l’adresse CString une seule fois au lieu d’une seule itération par boucle. Le code de boucle a pu être écrit partout où threadData->strThread «str » est utilisé, mais le code serait beaucoup plus lent dans l’exécution. Il est préférable de mettre en cache une référence aux données lorsque de telles références se produisent dans des boucles.

Le CThreadLocal modèle de classe utilise les mêmes mécanismes que ceux qui CProcessLocal effectuent et les mêmes techniques d’implémentation.

Voir aussi

Notes techniques par numéro
Notes techniques par catégorie