class vector c++ example
Pasando la referencia al vector STL sobre el lĂmite dll (7)
La memoria se asigna en el archivo ejecutable y se pasa como una referencia a la función dll, los valores se agregan a través de la referencia y luego se procesan y finalmente se vuelven a asignar en el archivo ejecutable.
Agregar valores si no queda espacio (capacidad) significa una reasignación, por lo que los antiguos se desasignarán y se asignará un 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 los ajustes de compilación obvios que deben coincidir exactamente y, por supuesto, son dependientes del tipo de específicos del compilador. Lo más probable es que tenga que mantenerlos sincronizados en términos de compilaciones.
Tengo una biblioteca agradable para administrar archivos que necesita devolver listas específicas de cadenas. Dado que el único código con el que lo voy a usar es C ++ (y Java, pero que usa C ++ a través de JNI), decidí usar vector desde 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<string> &files)
{
files.clear();
for (vector<file_struct>::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 del valor de retorno es un intento de mantener las asignaciones de memoria sanas y porque las ventanas no me gustaron mucho al tener una "C" externa alrededor de un tipo de retorno de C ++ (quién sabe por qué, mi entendimiento es que todo es externo C "hace es evitar la mutilación de nombres en el compilador). De todos modos, el código para usar esto con otro c ++ es generalmente como sigue:
#if defined _WIN32
#include <Windows.h>
#define GET_METHOD GetProcAddress
#define OPEN_LIBRARY(X) LoadLibrary((LPCSTR)X)
#define LIBRARY_POINTER_TYPE HMODULE
#define CLOSE_LIBRARY FreeLibrary
#else
#include <dlfcn.h>
#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<string> &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<string> files;
(*get_all_files_pointer)(files);
// ... Do something with files ...
return 0;
}
La biblioteca se compila a través de cmake usando add_library (file_manager SHARED file_manager.cpp). El programa se compila en un proyecto cmake separado usando add_executable (file_manager_command_wrapper command_wrapper.cpp). No hay indicadores de compilación especificados para ninguno de los dos, solo esos comandos.
Ahora el programa funciona perfectamente bien tanto en mac como en linux. El problema es windows. Cuando se ejecuta, me sale este error:
¡Depuración de aserción!
...
Expresión: _pFirstBlock == _pHead
Esto, que he descubierto y que entiendo, se debe a los montones de memoria separados entre los archivos ejecutables y los dlls cargados. Creo que esto ocurre cuando la memoria se asigna en un montón y se desasigna en el otro. El problema es que, por mi vida, no puedo entender qué está saliendo mal. La memoria se asigna en el archivo ejecutable y se pasa como una referencia a la función dll, los valores se agregan a través de la referencia y luego se procesan y finalmente se vuelven a asignar en el archivo ejecutable.
Revelaría más código si pudiera, pero la propiedad intelectual de mi empresa dice que no puedo, por lo que todo el código anterior es simplemente un ejemplo.
¿Alguien con más conocimiento del tema puede ayudarme a entender este error y señalarme la dirección correcta para depurarlo y solucionarlo? Desafortunadamente, no puedo usar una máquina de Windows para la depuración ya que me desarrollo en Linux, luego confirmo cualquier cambio en un servidor gerrit que active compilaciones y pruebas a través de Jenkins. Tengo acceso a la consola de salida al compilar y probar.
Consideré el uso de 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 mucho menos en Windows y es horrible en varios montones.
EDIT: Definitivamente se bloquea tan pronto como el vector de archivos queda fuera del alcance. Mi pensamiento actual es que las cadenas puestas en el vector se asignan en el montón de dll y se desasignan en el montón ejecutable. Si este es el caso, ¿puede alguien informarme sobre una mejor solución?
El problema se produce porque las bibliotecas dinámicas (compartidas) en los idiomas de MS utilizan un montón diferente al 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 biblioteca estática (sin estar seguro de cómo CMAKE hace eso) porque entonces todas las asignaciones se realizarán en el ejecutable y en un solo montón. Por supuesto, 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 (compilado de ese modo en el espacio de la aplicación) y hacer que esos métodos llamen funciones puras con una interfaz C provista en la DLL.
El vector allí usa el std :: allocator predeterminado, que usa :: operator new para su asignación.
El problema es que cuando el vector se utiliza en el contexto de la DLL, se compila con el código vectorial de esa DLL, que conoce el :: operador nuevo proporcionado por esa DLL.
El código en el EXE intentará usar el nuevo EXE del operador 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 el momento de la compilación.
Por ejemplo, es posible que haya visto a Visual Studio dar un error que dice algo como "Símbolo externo sin resolver". Significa que "me dijiste que existe esta función llamada foo (), pero no puedo encontrarla en ninguna parte".
Esto no es lo mismo que hace Mac / Linux. Requiere que todos los símbolos sean resueltos en el momento de la carga. Lo que esto significa es que puede compilar un .so con un operador :: faltante nuevo. Y su programa puede cargarse en su .so y proporcionar su :: operador nuevo al .so, permitiendo que se resuelva. De forma predeterminada, todos los símbolos se exportan en GCC, por lo que :: su nuevo operador exportará el operador y potencialmente lo cargará su .so.
Hay algo interesante aquí, donde Mac / Linux permite dependencias circulares. El programa podría confiar en un símbolo proporcionado por .so, y ese mismo .so también podría confiar en un símbolo proporcionado por el programa. Las dependencias circulares son una cosa terrible y me gusta mucho que el método de Windows te obligue a no hacer esto.
Pero, dicho esto, el problema real es que está tratando de usar objetos C ++ a través de los límites. Eso es definitivamente un error. SOLO funcionará si el compilador utilizado en la DLL y el EXE es el mismo, con la misma configuración. El ''extern "C"'' puede intentar evitar la mutilación de nombres (no estoy seguro de lo que hace para los tipos no 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, desea que esté en un tipo C antiguo y sencillo. Si son cosas como ints y tipos simples, las cosas no son tan difíciles. En su caso, es probable que desee pasar una serie de caracteres *. Lo que significa que aún debe tener cuidado con la administración de la memoria.
El archivo DLL / .so debe gestionar su propia memoria. Así que 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á código adicional para convertir las cosas a tipos de C compartidos en los límites. Y a veces esto se filtra en su implementación para acelerar la implementación.
Pero el beneficio es que ahora la gente puede usar cualquier idioma, cualquier versión de compilador y cualquier configuración para escribir una DLL para usted. Y usted es más cuidadoso con la gestión y las dependencias adecuadas de la memoria.
Sé que es un trabajo extra. Pero esa es la forma correcta de hacer las cosas a través de las fronteras.
Mi - parcial - solución ha sido implementar todos los constructores predeterminados en el marco dll, por lo que explícitamente agregue (impelement) copia, operador de asignación e incluso mueva constructores, dependiendo de su programa. Esto hará que se llame a la correcta :: nuevo (suponiendo que especifique __declspec (dllexport)). Incluya implementaciones de destructor también para eliminar eliminaciones. No incluya ningún código de implementación en un archivo de encabezado (dll). Todavía recibo advertencias sobre el uso de clases sin interfaz dll (con contenedores stl) como base para clases con interfaz dll, pero funciona. Esto está usando VS2013 RC para el código nativo, en, obviamente, ventanas.
Probablemente te encuentres con problemas de compatibilidad binaria. En Windows, si desea utilizar interfaces C ++ entre DLL, debe asegurarse de que haya muchas cosas en orden, por ejemplo.
- Todas las DLL involucradas deben construirse con la misma versión del compilador de visual studio
- Todas las DLL deben tener 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 Runtime Library 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 compilaciones (esto es parte de la razón por la que no se pueden mezclar las DLL de versión y depuración)
Esa no es una lista exhaustiva por ningún tramo, lamentablemente :(
Su principal problema es que pasar tipos de C ++ a través de los límites de las DLL es difícil. Necesitas lo siguiente
- Mismo compilador
- La misma biblioteca estándar
- La misma configuración para excepciones
- En Visual C ++ necesitas la misma versión del compilador.
- En Visual C ++ necesitas la misma configuración Debug / Release.
- En Visual C ++ necesitas el mismo nivel de depuración Iterator
Y así
Si eso es lo que quieres, escribí una biblioteca de solo encabezado llamada cppcomponents https://github.com/jbandela/cppcomponents que proporciona la forma más sencilla de hacerlo en C ++. Necesitas un compilador con un fuerte soporte para C ++ 11. Gcc 4.7.2 o 4.8 funcionará. Visual C ++ 2013 vista previa también funciona.
Lo guiaré a través del uso de cppcomponents para resolver su problema.
git clone https://github.com/jbandela/cppcomponents.git
en el directorio de su elección. Nos referiremos al directorio donde ejecutó este comando comolocalgit
Crea un archivo llamado
interfaces.hpp
. En este archivo, definirá la interfaz que se puede usar en los compiladores.
Introduzca la siguiente
#include <cppcomponents/cppcomponents.hpp>
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<uuid<0x633abf15,0x131e,0x4da8,0x933f,0xc13fbd0416cd>>{
std::vector<std::string> GetFiles();
CPPCOMPONENTS_CONSTRUCT(IGetFiles,GetFiles);
};
inline std::string FilesId(){return "Files!Files";}
typedef runtime_class<FilesId,object_interfaces<IGetFiles>> Files_t;
typedef use_runtime_class<Files_t> Files;
A continuación crear una implementación. Para hacer esto crea Files.cpp
.
Agregue el siguiente código
#include "interfaces.h"
struct ImplementFiles:implement_runtime_class<ImplementFiles,Files_t>{
std::vector<std::string> GetFiles(){
std::vector<std::string> ret = {"samplefile1.h", "samplefile2.cpp"};
return ret;
}
ImplementFiles(){}
};
CPPCOMPONENTS_DEFINE_FACTORY();
Finalmente aquí está el archivo para utilizar el anterior. Crear UseFiles.cpp
Agregue el siguiente código
#include "interfaces.h"
#include <iostream>
int main(){
Files f;
auto vec_files = f.GetFiles();
for(auto& name:vec_files){
std::cout << name << "/n";
}
}
Ahora puedes compilar. Solo para demostrar que somos compatibles en todos los compiladores, usaremos cl
el compilador de Visual C ++ para compilar UseFiles.cpp
en UseFiles.exe
. Usaremos Mingw Gcc para compilar Files.cpp
en Files.dll
cl /EHsc UseFiles.cpp /I localgit/cppcomponents
donde localgit
es el directorio en el que ejecutó el git clone
como se describe 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
cppcomponents también funcionará en Linux. El cambio principal es cuando compilas el exe, necesitas agregar -ldl
a la bandera, y cuando compilas el archivo .so, necesitas agregar -fPIC
a las banderas.
Si tienes más preguntas, házmelo saber.
Todo el mundo parece estar colgado del infame problema de incompatibilidad de compilador de DLL aquí, pero creo que tienes razón en que esto está relacionado con las asignaciones de pila. Sospecho que lo que está sucediendo es que el vector (asignado en el espacio de almacenamiento principal del exe) contiene cadenas asignadas en el espacio de almacenamiento del DLL. Cuando el vector queda fuera del alcance y se desasigna, también intenta desasignar las cadenas, y todo esto está sucediendo en el lado .exe, lo que provoca el bloqueo.
Tengo dos sugerencias instintivas:
Envuelve cada cadena en un
std::unique_ptr
. Incluye un ''borrado'' que maneja la desasignación de su contenido cuando el unique_ptr está fuera de alcance. Cuando el unique_ptr se crea en el lado de DLL, también lo es su eliminador. Por lo tanto, cuando el vector queda fuera del alcance y se llama a los destructores de su contenido, las cadenas se desasignarán por sus eliminadores vinculados a DLL y no se producirá ningún conflicto de almacenamiento dinámico.extern "C" FILE_MANAGER_EXPORT void get_all_files(vector<unique_ptr<string>>& files) { files.clear(); for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i) { files.push_back(unique_ptr<string>(new string(i->full_path))); } }
Mantenga el vector en el lado de DLL y simplemente devuelva una referencia a él. Puede pasar la referencia a través del límite de DLL:
vector<string> files; extern "C" FILE_MANAGER_EXPORT vector<string>& get_all_files() { files.clear(); for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i) { files.push_back(i->full_path); } return files; }
Semi-relacionado: " unique_ptr<Base>
" unique_ptr<Base>
to unique_ptr<Derived>
(a través del límite de DLL) :