2010-02-15 15 views
19

Aunque parece ser un problema muy común, no recolecté mucha información: ¿Cómo puedo crear una interfaz segura entre los límites de la DLL con respecto a la asignación de memoria?Administración de memoria/montón en las DLL

Está bastante bien sabido que

// in DLL a 
DLLEXPORT MyObject* getObject() { return new MyObject(); } 
// in DLL b 
MyObject *o = getObject(); 
delete o; 

podría sin duda dará lugar a accidentes. Pero dado que las interacciones como la anterior son, como me atrevo a decir, no infrecuentes, tiene que haber una forma de garantizar una asignación de memoria segura.

Por supuesto, uno podría proporcionar

// in DLL a 
DLLEXPORT void deleteObject(MyObject* o) { delete o; } 

pero tal vez hay mejores maneras (por ejemplo, smart_ptr?). Leí sobre el uso de asignadores personalizados al tratar con contenedores STL también.

Así que mi pregunta es más acerca de sugerencias generales a artículos y/o literatura que trata de este tema. ¿Hay falacias especiales a tener en cuenta (manejo de excepciones?) ¿Y este problema está limitado solo a las DLL o son objetos compartidos de UNIX "infligidos" también?

+2

Las bibliotecas compartidas en Linux también se ven afectadas. – sergiom

Respuesta

12

Como sugirió, puede usar un boost::shared_ptr para manejar ese problema. En el constructor puede pasar una función de limpieza personalizada, que podría ser el método deleteObject del dll que creó el puntero. Ejemplo:

boost::shared_ptr<MyObject> Instance(getObject(), deleteObject); 

Si no necesita un C-Interface para su DLL, puede tener un retorno getObject shared_ptr.

+2

Esto también es lo que se recomienda en C++ efectivo. –

9

sobrecarga operator new, operator delete et. al para todas sus clases DLL y ponerlas en práctica dentro de la DLL:

void* MyClass::operator new(size_t numb) { 
    return ::operator new(num_bytes); 
} 

void MyClass::operator delete(void* p) { 
    ::operator delete(p); 
} 
... 

Esto se puede colocar fácilmente en una clase base común para todas las clases exportadas por la DLL.

De esta manera, la asignación y la desasignación se realizan completamente en el montón de DLL. Honestamente, no estoy seguro de si tiene algún inconveniente grave o problemas de portabilidad, pero funciona para mí.

+0

Probablemente no sea una buena idea: https://stackoverflow.com/questions/11846511/new-and-delete-operator-overloading-for-dll – jaba

2

Otra opción que puede ser aplicable en algunas circunstancias es mantener toda la asignación y desasignar dentro de la DLL y evitar que el objeto cruce ese límite. Puede hacer esto proporcionando un mango para que la creación de un MyObject crea dentro del código DLL y devuelve un identificador sencillo (por ejemplo, un unsigned int) a través del cual se realizan todas las operaciones por parte del cliente:

// Client code 
ObjHandle h=dllPtr->CreateObject(); 
dllPtr->DoOperation(h); 
dllPtr->DestroyObject(h); 

Puesto que toda la asignación ocurre dentro del dll, puedes asegurarte de que se limpia envolviendo un shared_ptr. Este es más o menos el método sugerido por John Lakos en Large Scale C++.

+0

¡Gracias por recordarme que recoja y lea/lea a través de Lakos! Su reputación es aparentemente ensombrecida por los "dos Meyers", pero nuevamente es un tema diferente. – msi

1

En una arquitectura "en capas" (escenario muy común) el componente más profundo es responsable de proporcionar una política sobre la pregunta (podría estar devolviendo shared_ptr<> como se sugiere arriba o "el que llama es responsable de eliminar esto" o "nunca borrar" esto, pero llame al releaseFooObject() cuando termine y no acceda después "o ...) y el componente más cercano al usuario es responsable de seguir esa política.

El flujo de información bidireccional hace que las responsabilidades sean más difíciles de caracterizar.


este problema se limita a sólo DLL o están UNIX objetos compartidos "infligido" también?

En realidad, es peor que eso: puede tener este problema con la misma facilidad con bibliotecas estáticamente vinculadas. Es la existencia de límites de código dentro de un único contexto de ejecución que da la posibilidad de un uso incorrecto o una mala comunicación sobre alguna instalación.

+0

En realidad en Unix no hay tal problema ... – Emiliano

+1

@happy_emi: Sí. Ahi esta. El problema es una consecuencia de los límites del código, no de los sistemas operativos. Unix tiene algunas tradiciones de codificación que hacen que surja con menos frecuencia, pero aún es posible. – dmckee

+0

Ya veo, gracias. – Emiliano

5

Puede indicar que "podría provocar bloqueos". Gracioso: "poder" significa exactamente lo contrario de "ciertamente".

Ahora, la declaración es en su mayoría histórica de todos modos. Hay una solución muy simple: use 1 compilador, 1 configuración de compilador y enlace con el formato DLL del CRT. (Y probablemente pueda escapar omitiendo este último)

No hay artículos específicos para enlazar, ya que esto no es un problema hoy en día. Necesitarías el 1 compilador, 1 regla de configuración de todos modos. Las cosas simples como sizeof(std::string) dependen de ello, y de lo contrario tendría infracciones masivas de ODR.

0

He escrito an article sobre el uso de las funciones de eliminación de caracteres personalizados de C++ 11 de unique_ptr para pasar objetos a través de límites de DLL (o bibliotecas de objetos compartidos en Linux). El método descrito en el artículo no "contamina" la firma unique_ptr con el eliminador.

1

Está bastante bien sabido que

// in DLL a 
DLLEXPORT MyObject* getObject() { return new MyObject(); } 
// in DLL b 
MyObject *o = getObject(); 
delete o; 

podría sin duda dará lugar a accidentes.

Si lo anterior tiene una característica bien definida depende de cómo se defina el tipo MyObject.

Si la clase tiene un destructor virtual (y ese destructor no está definido en línea), no se bloqueará y mostrará un comportamiento bien definido.

La razón normalmente citado por qué esto es que se estrella delete hace dos cosas:

  • llamada al destructor
  • memoria libre (llamando operator delete(void* ...))

Para una clase con un no virtual destructor, puede hacer estas cosas "en línea", lo que lleva a la situación en la que delete dentro de la DLL "b" puede intentar liberar memoria del "a" heap == crash.

Sin embargo, si el destructor de MyObject es virtual a continuación antes de llamar a la función de "libre", el compilador needs to determine the actual run-time class of the pointer antes de que pueda pasar el puntero correcto operator delete():

mandatos

C++ que debe pasar exactamente el mismo dirección al operador borrar como lo que el operador nuevo devuelve. Cuando se está asignando un objeto utilizando nuevo, el compilador implícitamente conoce el tipo concreto de objeto (que es lo que el compilador utiliza para pasar en el tamaño correcto de memoria al nuevo operador, por ejemplo.)

Sin embargo , si su clase tiene una clase base con un destructor virtual, y su objeto se elimina mediante un puntero a la clase base, el compilador no conoce el tipo concreto en el sitio de la llamada y, por lo tanto, no puede calcular la dirección correcta a pase al operador delete(). ¿Por qué, puedes preguntar? Debido a que en presencia de herencia múltiple, la dirección del puntero de la clase base puede ser diferente a la dirección del objeto en la memoria.

Por lo tanto, lo que ocurre en ese caso es que cuando se elimina un objeto que tiene un destructor virtual, el compilador llama a lo que se llama un destructor de eliminación lugar de la secuencia habitual de una llamada al destructor normal, seguida por operador delete() para reclamar la memoria.

Desde el destructor de eliminación es una función virtual, en tiempo de ejecución la aplicación del tipo de hormigón se llamará, y que la aplicación es capaz de calcular la dirección correcta para el objeto en la memoria. Lo que hace esa implementación es llamar al destructor común , calcular la dirección correcta del objeto, y luego llamar al operador delete() en esa dirección.

Parece que tanto GCC (del artículo enlazado) y MSVC lograr esto llamando tanto la dtor, así como la función de "libre" en el contexto de un "destructor de eliminación". Y esta ayuda necesariamente vive dentro de su DLL y siempre usará el montón correcto, incluso si "a" y "b" tienen uno diferente.

Cuestiones relacionadas