Al pasar referencia al vector STL sobre el límite dll

Tengo una buena biblioteca para administrar archivos que necesita devolver listas específicas de cadenas. Como el único código con el que voy a usarlo será C ++ (y Java, pero eso es usar C ++ a través de JNI), decidí usar el vector de las bibliotecas estándar. Las funciones de la biblioteca se parecen un poco a esto (donde FILE_MANAGER_EXPORT es un requisito de exportación definido por la plataforma):

extern "C" FILE_MANAGER_EXPORT void get_all_files(vector &files) { files.clear(); for (vector::iterator i = file_structs.begin(); i != file_structs.end(); ++i) { files.push_back(i->full_path); } } 

La razón por la que utilicé el vector como referencia en lugar de devolver el valor es un bash de mantener las asignaciones de memoria sanas y porque Windows realmente no estaba contento de tener una “C” externa alrededor de un tipo de retorno de c ++ (quién sabe por qué, entiendo que todo extern ” C “does es evitar el cambio de nombre en el comstackdor). De todos modos, el código para usar esto con otros c ++ es generalmente el siguiente:

 #if defined _WIN32 #include  #define GET_METHOD GetProcAddress #define OPEN_LIBRARY(X) LoadLibrary((LPCSTR)X) #define LIBRARY_POINTER_TYPE HMODULE #define CLOSE_LIBRARY FreeLibrary #else #include  #define GET_METHOD dlsym #define OPEN_LIBRARY(X) dlopen(X, RTLD_NOW) #define LIBRARY_POINTER_TYPE void* #define CLOSE_LIBRARY dlclose #endif typedef void (*GetAllFilesType)(vector &files); int main(int argc, char **argv) { LIBRARY_POINTER_TYPE manager = LOAD_LIBRARY("library.dll"); //Just an example, actual name is platform-defined too GetAllFilesType get_all_files_pointer = (GetAllFilesType) GET_METHOD(manager, "get_all_files"); vector files; (*get_all_files_pointer)(files); // ... Do something with files ... return 0; } 

La biblioteca se comstack a través de cmake utilizando add_library (file_manager SHARED file_manager.cpp). El progtwig se comstack en un proyecto separado de cmake utilizando add_executable (file_manager_command_wrapper command_wrapper.cpp). No hay indicadores de comstackción especificados para ninguno, solo esos comandos.

Ahora el progtwig funciona perfectamente bien tanto en mac como en linux. El problema es Windows. Cuando se ejecuta, obtengo este error:

¡Depuración de aserción!

Expresión: _pFirstBlock == _pHead

Esto, lo he descubierto y comprendo, se debe a montones de memoria separados entre ejecutables y dlls cargados. Creo que esto ocurre cuando la memoria se asigna en un montón y desasignado en el otro. El problema es que, por mi vida, no puedo entender qué está pasando mal. La memoria se asigna en el ejecutable y se pasa como una referencia a la función dll, los valores se agregan a través de la referencia, y luego esos se procesan y se desasignan de nuevo en el ejecutable.

Si pudiera, revelaría más código, pero la propiedad intelectual de mi empresa establece que no puedo, por lo que todo el código anterior es meramente ejemplos.

¿Alguien con más conocimiento del tema puede ayudarme a entender este error y apuntarme en la dirección correcta para depurarlo y solucionarlo? Lamentablemente, no puedo usar una máquina de Windows para la depuración ya que desarrollo en Linux, luego confirmo cualquier cambio en un servidor de Gerrit que desencadena comstackciones y pruebas a través de jenkins. Tengo acceso a la consola de salida luego de comstackr y probar.

Consideré usar tipos no stl, copiando el vector en c ++ a un char **, pero la asignación de memoria fue una pesadilla y estaba luchando para que funcionara bien en linux y menos en windows y es horrible montones múltiples.

EDITAR: definitivamente falla tan pronto como el vector de archivos sale del scope. Mi pensamiento actual es que las cadenas colocadas en el vector se asignan en el montón dll y se desasignan en el montón ejecutable. Si este es el caso, ¿alguien puede iluminarme en cuanto a una mejor solución?

Su problema principal es que pasar tipos de C ++ a través de los límites de la DLL es difícil. Necesitas lo siguiente

  1. Mismo comstackdor
  2. La misma biblioteca estándar
  3. Misma configuración para excepciones
  4. En Visual C ++ necesita la misma versión del comstackdor
  5. En Visual C ++ necesita la misma configuración de depuración / liberación
  6. En Visual C ++ necesita el mismo nivel de depuración de iterador

Y así

Si eso es lo que quieres, escribí una biblioteca de solo encabezado llamada cppcomponents https://github.com/jbandela/cppcomponents que proporciona la manera más fácil de hacerlo en C ++. Necesita un comstackdor con gran soporte para C ++ 11. Gcc 4.7.2 o 4.8 funcionará. La vista previa de Visual C ++ 2013 también funciona.

Lo guiaré a través del uso de cppcomponents para resolver su problema.

  1. git clone https://github.com/jbandela/cppcomponents.git en el directorio de su elección. Nos referiremos al directorio donde ejecutó este comando como localgit

  2. Crea un archivo llamado interfaces.hpp . En este archivo, definirá la interfaz que se puede usar en los comstackdores.

Introduzca la siguiente

 #include  using cppcomponents::define_interface; using cppcomponents::use; using cppcomponents::runtime_class; using cppcomponents::use_runtime_class; using cppcomponents::implement_runtime_class; using cppcomponents::uuid; using cppcomponents::object_interfaces; struct IGetFiles:define_interface>{ std::vector GetFiles(); CPPCOMPONENTS_CONSTRUCT(IGetFiles,GetFiles); }; inline std::string FilesId(){return "Files!Files";} typedef runtime_class> Files_t; typedef use_runtime_class Files; 

Luego crea una implementación. Para hacer esto, crea Files.cpp .

Agregue el siguiente código

 #include "interfaces.h" struct ImplementFiles:implement_runtime_class{ std::vector GetFiles(){ std::vector ret = {"samplefile1.h", "samplefile2.cpp"}; return ret; } ImplementFiles(){} }; CPPCOMPONENTS_DEFINE_FACTORY(); 

Finalmente aquí está el archivo para usar lo anterior. Crear UseFiles.cpp

Agregue el siguiente código

 #include "interfaces.h" #include  int main(){ Files f; auto vec_files = f.GetFiles(); for(auto& name:vec_files){ std::cout << name << "\n"; } } 

Ahora puedes comstackr. Para mostrar que somos compatibles en todos los comstackdores, usaremos cl el comstackdor de Visual C ++ para comstackr UseFiles.cpp en UseFiles.exe . Usaremos Mingw Gcc para comstackr Files.cpp en Files.dll

cl /EHsc UseFiles.cpp /I localgit\cppcomponents

donde localgit es el directorio en el que ejecutó git clone como se describió anteriormente

g++ -std=c++11 -shared -o Files.dll Files.cpp -I localgit\cppcomponents

No hay paso de enlace Solo asegúrese de que Files.dll y UseFiles.exe estén en el mismo directorio.

Ahora ejecuta el ejecutable con UseFiles

Los componentes de cpp también funcionarán en Linux. El cambio principal es cuando comstack el exe, necesita agregar -ldl al -ldl , y cuando comstack el archivo .so, necesita agregar -fPIC a los indicadores.

Si tiene más preguntas, hágamelo saber.

La memoria se asigna en el ejecutable y se pasa como una referencia a la función dll, los valores se agregan a través de la referencia, y luego esos se procesan y se desasignan de nuevo en el ejecutable.

Agregar valores si no queda espacio (capacidad) significa una reasignación, por lo que se desasignará el antiguo y se asignará uno nuevo. Eso se hará mediante la función std :: vector :: push_back de la biblioteca, que utilizará el asignador de memoria de la biblioteca.

Aparte de eso, tienes la obvia configuración de comstackción, debe coincidir exactamente y, por supuesto, dependen de los comstackdores específicos. Lo más probable es que tengas que mantenerlos sincronizados en términos de comstackciones.

Todo el mundo parece estar pendiente del infame problema de incompatibilidad DLL-comstackdor aquí, pero creo que tiene razón acerca de que esto esté relacionado con las asignaciones de montón. Sospecho que lo que está sucediendo es que el vector (asignado en el espacio de montón del exe principal) contiene cadenas asignadas en el espacio de almacenamiento dynamic de la DLL. Cuando el vector sale del scope y se desasigna, también intenta desasignar las cadenas, y todo esto sucede en el lado de .exe, lo que provoca el colapso.

Tengo dos sugerencias instintivas:

  1. Envuelva cada cadena en std::unique_ptr . Incluye un ‘eliminador’ que maneja la desasignación de sus contenidos cuando el unique_ptr queda fuera del scope. Cuando unique_ptr se crea en el lado de DLL, también lo es su eliminador. Por lo tanto, cuando el vector sale del scope y se invocan los destructores de su contenido, las cadenas serán desasignadas por sus modificadores vinculados a DLL y no se producirá ningún conflicto de montón.

     extern "C" FILE_MANAGER_EXPORT void get_all_files(vector>& files) { files.clear(); for (vector::iterator i = file_structs.begin(); i != file_structs.end(); ++i) { files.push_back(unique_ptr(new string(i->full_path))); } } 
  2. Mantenga el vector en el lado de DLL y simplemente devuelva una referencia al mismo. Puede pasar la referencia a través del límite de la DLL:

     vector files; extern "C" FILE_MANAGER_EXPORT vector& get_all_files() { files.clear(); for (vector::iterator i = file_structs.begin(); i != file_structs.end(); ++i) { files.push_back(i->full_path); } return files; } 

unique_ptr : ” unique_ptrunique_ptr a unique_ptr (a través del límite de DLL) :

El problema se produce porque las bibliotecas dinámicas (compartidas) en lenguajes MS usan un montón diferente del ejecutable principal. Crear una cadena en la DLL o actualizar el vector que causa una reasignación causará este problema.

La solución más simple para ESTE problema es cambiar la biblioteca a una lib estática (no es cierto cómo uno hace que CMAKE lo haga) porque entonces todas las asignaciones ocurrirán en el ejecutable y en un solo montón. Por supuesto, entonces tiene todos los problemas de compatibilidad de biblioteca estática de MS C ++ que hacen que su biblioteca sea menos atractiva.

Los requisitos en la parte superior de la respuesta de John Bandela son todos similares a los de la implementación de la biblioteca estática.

Otra solución es implementar la interfaz en el encabezado (comstackda de ese modo en el espacio de la aplicación) y hacer que esos métodos llamen a funciones puras con una interfaz C proporcionada en el archivo DLL.

Probablemente te encuentres con problemas de compatibilidad binaria. En Windows, si desea utilizar interfaces C ++ entre DLL, debe asegurarse de que muchas cosas estén en orden, por ej.

  • Todas las DLL involucradas deben comstackrse con la misma versión del comstackdor de Visual Studio.
  • Todas las DLL deben tener un enlace con la misma versión del tiempo de ejecución de C ++ (en la mayoría de las versiones de VS esta es la configuración de la Biblioteca en tiempo de ejecución en Configuración -> C ++ -> Generación de código en las propiedades del proyecto)
  • La configuración de depuración del iterador debe ser la misma para todas las comstackciones (esta es parte de la razón por la que no se pueden mezclar los DLL Release y Debug)

Esa no es una lista exhaustiva de ninguna manera por desgracia 🙁

El vector allí usa el std :: allocator predeterminado, que usa :: operator new para su asignación.

El problema es que cuando el vector se usa en el contexto de la DLL, se comstack con el código vectorial de esa DLL, que conoce el operador :: new proporcionado por esa DLL.

El código en el EXE intentará usar el :: operador nuevo del EXE.

Apuesto a que la razón por la que esto funciona en Mac / Linux y no en Windows es porque Windows requiere que todos los símbolos se resuelvan en tiempo de comstackción.

Por ejemplo, es posible que haya visto Visual Studio dar un error al decir algo así como “Símbolo externo sin resolver”. Significa “Me dijiste que esta función llamada foo () existe, pero no puedo encontrarla en ningún lado”.

Esto no es lo mismo que lo que hace Mac / Linux. Requiere que todos los símbolos se resuelvan en el momento de la carga. Lo que esto significa es que puedes comstackr un .so con un operador :: desconocido nuevo. Y su progtwig puede cargar en su .so y proporcionar su operador :: nuevo al .so, lo que permite que se resuelva. Por defecto, todos los símbolos se exportan en GCC, por lo que el operador exportará el operador :: new y su .so lo cargará.

Aquí hay algo interesante, donde Mac / Linux permite dependencias circulares. El progtwig podría basarse en un símbolo proporcionado por .so, y ese mismo .so podría depender de un símbolo proporcionado por el progtwig. Las dependencias circulares son algo terrible, así que me gusta mucho que el método de Windows te obligue a no hacer esto.

Pero, dicho esto, el verdadero problema es que estás tratando de usar objetos C ++ a través de los límites. Eso es definitivamente un error. SÓLO funcionará si el comstackdor utilizado en el DLL y el EXE es el mismo, con la misma configuración. El ” extern ” C ” ” puede intentar prevenir el cambio de nombre (no estoy seguro de lo que hace para los tipos que no son C, como std :: vector). Pero no cambia el hecho de que el otro lado puede tener una implementación totalmente diferente de std :: vector.

En términos generales, si se pasa a través de límites como ese, quiere que esté en un tipo simple de C antiguo. Si se trata de cosas como Ints y tipos simples, las cosas no son tan difíciles. En su caso, es probable que desee pasar una matriz de char *. Lo que significa que todavía debe tener cuidado con la administración de la memoria.

La DLL / .so debería administrar su propia memoria. Entonces la función podría ser así:

 Foo *bar = nullptr; int barCount = 0; getFoos( bar, &barCount ); // use your foos releaseFoos(bar); 

El inconveniente es que tendrá un código adicional para convertir las cosas a tipos C-compartibles en los límites. Y a veces esto se filtra en su implementación para acelerar la implementación.

Pero el beneficio ahora es que las personas pueden usar cualquier idioma y cualquier versión de comstackdor y cualquier configuración para escribir un archivo DLL para usted. Y tiene más cuidado con la gestión de la memoria y las dependencias.

Sé que es un trabajo extra. Pero esa es la forma correcta de hacer las cosas más allá de los límites.

Mi – parcial – solución ha sido implementar todos los constructores por defecto en el marco dll, por lo que explícitamente agregar (impelemento) copia, operador de asignación e incluso mover constructores, dependiendo de su progtwig. Esto hará que se llame a la función :: :: new correcta (suponiendo que especifique __declspec (dllexport)). Incluye implementaciones de destructor también para hacer coincidir las eliminaciones. No incluya ningún código de implementación en un archivo de encabezado (dll). Aún recibo advertencias sobre el uso de clases no conectadas a dll (con contenedores stl) como base para las clases dll-interface, pero funciona. Esto está usando VS2013 RC para el código nativo, obviamente en Windows.