TN058: Implementación de estado del módulo MFC

Nota:

La nota técnica siguiente no se ha actualizado desde que se incluyó por primera vez en la documentación en línea. Como resultado, algunos procedimientos y temas podrían estar obsoletos o ser incorrectos. Para obtener información más reciente, se recomienda buscar el tema de interés en el índice de la documentación en línea.

En esta nota técnica se describe la implementación de las construcciones del "estado del módulo" de MFC. Una reconocimiento de la implementación del estado del módulo es fundamental para usar los archivos DLL compartidos de MFC desde un archivo DLL (o un servidor en proceso OLE).

Antes de leer esta nota, consulte "Administrar los datos de estado de los módulos MFC" en Crear nuevos documentos, Windows y vistas. Este artículo contiene información de utilización importante e información general sobre este tema.

Información general

Hay tres tipos de información del estado de MFC: Estado del módulo, Estado del proceso y Estado del subproceso. A veces, estos tipos de estado se pueden combinar. Por ejemplo, las asignaciones de los manipuladores de MFC son locales del módulo y locales del subproceso. Esto permite que dos módulos diferentes tengan asignaciones diferentes en cada uno de sus subprocesos.

El estado del proceso y el estado del subproceso son similares. Estos elementos de datos son elementos que tradicionalmente han sido variables globales, pero que deben ser específicos de un proceso o subproceso determinado para una compatibilidad adecuada con Win32s o para la compatibilidad correcta con multithreading. La categoría en la que se ajusta un elemento de datos determinado depende de ese elemento y de su semántica deseada con respecto a los límites de los procesos y subprocesos.

El estado del módulo es único en el sentido de que puede contener un estado realmente global o un estado que sea local o local del subproceso. Además, se puede cambiar rápidamente.

Conmutación de estado del módulo

Cada subproceso contiene un puntero al estado del módulo "actual" o "activo" (no sorprendentemente, el puntero forma parte del estado local del subproceso de MFC). Este puntero se cambia cuando el subproceso de ejecución pasa un límite de módulo, como una aplicación que llama a un control OLE o DLL, o un control OLE que vuelve a llamar a una aplicación.

El estado del módulo actual se cambia al llamar a AfxSetModuleState. Por lo general, nunca tratará directamente con la API. MFC, en muchos casos, se llamará automáticamente (en WinMain, puntos de entrada OLE, AfxWndProc, etc.). Esto se hace en cualquier componente que se escriba mediante la vinculación estática en un WndProc especial y un WinMain especial (o DllMain) que sepa qué estado del módulo debe ser actual. Para ver este código, consulte DLLMODUL. CPP o APPMODUL. CPP en el directorio MFC\SRC.

Es raro que se quiera establecer el estado del módulo y después no volver a establecerlo. La mayoría de las veces se deseará "insertar" el estado del propio módulo como el actual y luego, una vez que se haya terminado, "sacar" el contexto original. Esto se realiza mediante la macro AFX_MANAGE_STATE y la clase especial AFX_MAINTAIN_STATE.

CCmdTarget tiene características especiales para admitir el cambio de estado del módulo. En concreto, un CCmdTarget es la clase raíz usada para la automatización OLE y los puntos de entrada OLE COM. Al igual que cualquier otro punto de entrada expuesto al sistema, estos puntos de entrada deben establecer el estado correcto del módulo. ¿Cómo sabe un determinado CCmdTarget cuál debe ser el estado "correcto" del módulo? La respuesta es que "recuerda" cuál es el estado "actual" del módulo cuando se construye, de modo que puede establecer el estado del módulo actual en ese valor "recordado" cuando se llama posteriormente. Como resultado, el estado del módulo al que está asociado un objeto determinado CCmdTarget es el estado del módulo que estaba activo cuando se construyó el objeto. Tome un ejemplo sencillo de cargar un servidor INPROC, crear un objeto y llamar a sus métodos.

  1. La DLL se carga mediante OLE al usar LoadLibrary.

  2. Se llama primero a RawDllMain. Establece el estado del módulo en el estado del módulo estático conocido para la DLL. Por este motivo RawDllMain está vinculado estáticamente a la DLL.

  3. Se llama al constructor del generador de clases asociado a nuestro objeto. COleObjectFactory se deriva de CCmdTarget y como resultado recuerda en qué estado del módulo se creó una instancia. Esto es importante: cuando se le pide al generador de clases que cree objetos, ahora sabe qué estado del módulo debe hacer actual.

  4. Se llama a DllGetClassObject para obtener el generador de clases. MFC busca en la lista del generadores de clases asociada a este módulo y la devuelve.

  5. Se llama a COleObjectFactory::XClassFactory2::CreateInstance. Antes de crear el objeto y devolverlo, esta función establece el estado del módulo al estado del módulo que estaba actual en el paso 3 (el que era actual cuando se creó una instancia de COleObjectFactory). Esto se realiza dentro de METHOD_PROLOGUE.

  6. Cuando se crea el objeto, también es un derivado de CCmdTarget y de la misma manera que COleObjectFactory, recordaba qué estado del módulo estaba activo, por lo que este nuevo objeto también lo hace. Ahora el objeto sabe a qué estado del módulo se va a cambiar cada vez que se llame.

  7. El cliente llama a una función en el objeto OLE COM que recibió de su llamada CoCreateInstance. Cuando se llama al objeto, se usa METHOD_PROLOGUE para cambiar el estado del módulo de la misma manera que lo hace COleObjectFactory.

Como se puede ver, el estado del módulo se propaga desde el objeto al objeto a medida que se van creando. Es importante que el estado del módulo se establezca correctamente. Si no se establece, la DLL o el objeto COM pueden interactuar mal con una aplicación MFC que la llame, o puede no encontrar sus propios recursos, o se pueden producir errores de otras maneras.

Tenga en cuenta que ciertos tipos de DLL, específicamente las DLL de "extensión MFC" no cambian el estado del módulo en su RawDllMain (en realidad, normalmente ni siquiera tienen un RawDllMain). Esto se debe a que están diseñados para comportarse "como si" estuvieran realmente presentes en la aplicación que los usa. Forman parte de la aplicación que se está ejecutando y su intención es modificar el estado global de esa aplicación.

Los controles OLE y otras DLL son muy diferentes. No quieren modificar el estado de la aplicación que realiza la llamada; es posible que la aplicación que los llame no sea incluso una aplicación MFC, por lo que puede que no haya ningún estado que modificar. Esta es la razón por la que se inventó el cambio del estado del módulo.

Para las funciones exportadas desde una DLL, como una que inicia un cuadro de diálogo en su DLL, se debe agregar el código siguiente al principio de la función:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Esto intercambia el estado del módulo actual con el estado devuelto desde AfxGetStaticModuleState hasta el final del ámbito actual.

Si no se usa la macro AFX_MODULE_STATE, se producirán problemas con los recursos de las DLL. De forma predeterminada, MFC usa el manipulador de recursos de la aplicación principal para cargar la plantilla de recursos. Esta plantilla se almacena realmente en la DLL. La causa principal es que la macro de AFX_MODULE_STATE no ha cambiado la información de estado del módulo de MFC. El manipulador de recursos se recupera del estado del módulo de MFC. No cambiar el estado del módulo hace que se use el manipulador de recursos incorrecto.

No es necesario poner AFX_MODULE_STATE en cada función de la DLL. Por ejemplo, se puede llamar a InitInstance mediante el código MFC de la aplicación sin AFX_MODULE_STATE porque MFC desplaza automáticamente el estado del módulo antes de InitInstance y luego lo vuelve a cambiar después de que InitInstance vuelva. Lo mismo sucede con todos los manipuladores de la asignación de mensajes. Las DLL de MFC normales tienen realmente un procedimiento especial de ventana maestra que cambia automáticamente el estado del módulo antes de enrutar cualquier mensaje.

Proceso de datos locales

El proceso de datos locales no sería tan importante si no fuera por la dificultad del modelo DLL de Win32s. En Win32, todas las DLL comparten sus datos globales, incluso cuando se cargan en varias aplicaciones. Esto es muy diferente del modelo de datos de DLL win32 "real", donde cada DLL obtiene una copia independiente de su espacio de datos en cada proceso que se adjunta a la DLL. Para aumentar la complejidad, los datos asignados en el montón de una DLL de Win32s son, de hecho, específicos del proceso (al menos en lo que respecta a la propiedad). Considere los siguientes datos:

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

Tenga en cuenta lo que sucede si el código anterior se encuentra en una DLL y esa DLL se carga mediante dos procesos A y B (podría, de hecho, ser dos instancias de la misma aplicación). A llama a SetGlobalString("Hello from A"). Como resultado, la memoria se asigna a los datos CString en el contexto del proceso A. Tenga en cuenta que el propio CString es global y es visible para A y B. Ahora B llama a GetGlobalString(sz, sizeof(sz)). B podrá ver los datos que A establece. Esto se debe a que Win32s no ofrece protección entre los procesos como Win32. Ese es el primer problema; en muchos casos, no es conveniente que una aplicación afecte a los datos globales que se consideran propiedad de una aplicación diferente.

También hay problemas adicionales. Supongamos que ahora sale A. Cuando sale A, la memoria usada por la cadena "strGlobal" está disponible para el sistema, es decir, el sistema operativo libera automáticamente toda la memoria asignada por el proceso A. No se libera porque se llama al destructor CString; aún no se ha llamado. Se libera simplemente porque la aplicación que le asignó ha dejado la escena. Ahora, si B llamó a GetGlobalString(sz, sizeof(sz)), es posible que no obtenga datos válidos. Es posible que alguna otra aplicación haya usado esa memoria para otro propósito.

Existe claramente un problema. MFC 3.x usó una técnica denominada almacenamiento local para el subproceso (TLS). MFC 3.x asignaría un índice TLS que en Win32s actúa realmente como un índice de almacenamiento local para el proceso, aunque no se llame a él y luego haría referencia a todos los datos basados en ese índice TLS. Esto es similar al índice TLS que se usó para almacenar datos locales de subprocesos en Win32 (consulte a continuación para obtener más información sobre ese asunto). Esto provocó que cada DLL de MFC use al menos dos índices TLS por proceso. Cuando se tiene en cuenta que se cargan muchas DLL de control OLE (OCX), se agotan rápidamente los índices TLS (solo hay 64 disponibles). Además, MFC tenía que colocar todos estos datos en un solo lugar, en una sola estructura. No era muy extensible y no era ideal con respecto a su uso de índices TLS.

MFC 4.x aborda esto con un conjunto de plantillas de clase que se pueden "encapsular" alrededor de los datos que se deben procesar localmente. Por ejemplo, el problema mencionado anteriormente podría corregirse al escribir:

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 lo implementa en dos pasos. En primer lugar, hay una capa sobre las API Tls* de Win32 (TlsAlloc, TlsSetValue, TlsGetValue, etc.) que usan solo dos índices TLS por proceso, independientemente del número de DLL que tenga. En segundo lugar, se proporciona la plantilla CProcessLocal para acceder a estos datos. Invalida el operador >, que es lo que permite la sintaxis intuitiva que se ve anteriormente. Todos los objetos que se encapsulan mediante CProcessLocal deben derivarse de CNoTrackObject. CNoTrackObject proporciona un asignador de nivel inferior (LocalAlloc/LocalFree) y un destructor virtual para que MFC pueda destruir automáticamente los objetos locales del proceso cuando finalice el proceso. Estos objetos pueden tener un destructor personalizado si se requiere una limpieza adicional. El ejemplo anterior no requiere uno, ya que el compilador generará un destructor predeterminado para destruir el objeto incrustado CString.

Este enfoque tiene otras ventajas interesantes. No solo se destruyen automáticamente todos los objetos CProcessLocal, sino que no se construyen hasta que sean necesarios. CProcessLocal::operator-> creará una instancia del objeto asociado la primera vez que se llama y no antes. En el ejemplo anterior, esto significa que la cadena "strGlobal" no se construirá hasta la primera vez que se llame a SetGlobalString o GetGlobalString. En algunos casos, esto puede ayudar a reducir el tiempo de inicio de la DLL.

Datos locales de subprocesos

De forma similar al procesamiento de datos locales, los datos locales del subproceso se usan cuando los datos deben ser locales para un subproceso determinado. Es decir, se necesita una instancia independiente de los datos para cada subproceso que tenga acceso a esos datos. Esto puede usarse muchas veces en lugar de los mecanismos de sincronización extensos. Si varios subprocesos no necesitan compartir los datos, estos mecanismos pueden ser costosos e innecesarios. Supongamos que teníamos un objeto CString (al igual que el ejemplo anterior). Podemos hacer que el subproceso sea local envolviéndolo con una plantilla 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
        }
    }
}

Si MakeRandomString se llama desde dos subprocesos diferentes, cada uno "ordena" la cadena de maneras diferentes sin interferir con la otra. Esto se debe a que realmente hay una instancia strThread por subproceso en lugar de solo una instancia global.

Observe cómo se usa una referencia para capturar la dirección CString una vez en lugar de una vez por iteración del bucle. Se podría haber escrito el código del bucle con threadData->strThread en cualquier lugar en donde se use "str", pero el código sería mucho más lento en la ejecución. Es mejor almacenar en caché una referencia a los datos cuando estas referencias se producen en bucles.

La plantilla de clase CThreadLocal usa los mismos mecanismos que la CProcessLocal y las mismas técnicas de implementación.

Consulte también

Notas técnicas por número
Notas técnicas por categoría