La carga dinámica es un mecanismo por el cual un programa de computadora puede, en tiempo de ejecución , cargar una biblioteca (u otro binario ) en la memoria, recuperar las direcciones de funciones y variables contenidas en la biblioteca, ejecutar esas funciones o acceder a esas variables y descargar la biblioteca de la memoria. Es uno de los tres mecanismos por los cuales un programa de computadora puede usar algún otro software dentro del programa; los otros son el enlace estático y el enlace dinámico . A diferencia del enlace estático y el enlace dinámico, la carga dinámica permite que un programa de computadora se inicie en ausencia de estas bibliotecas, descubra bibliotecas disponibles y potencialmente obtenga funcionalidad adicional. [1] [2]
La carga dinámica era una técnica común para los sistemas operativos de IBM para System/360 , como OS/360 , en particular para subrutinas de E/S y para bibliotecas de ejecución COBOL y PL/I , y continúa utilizándose en los sistemas operativos de IBM para z/Architecture , como z/OS . En lo que respecta al programador de aplicaciones, la carga es en gran medida transparente, ya que la gestiona principalmente el sistema operativo (o su subsistema de E/S). Las principales ventajas son:
El sistema de procesamiento de transacciones estratégicas de IBM , CICS (de la década de 1970 en adelante), utiliza la carga dinámica de forma extensiva tanto para su núcleo como para la carga normal de programas de aplicación . Las correcciones a los programas de aplicación se podían realizar sin conexión y las nuevas copias de los programas modificados se podían cargar dinámicamente sin necesidad de reiniciar CICS [3] [4] (que puede funcionar, y con frecuencia lo hace, las 24 horas del día, los 7 días de la semana ).
Las bibliotecas compartidas se agregaron a Unix en la década de 1980, pero inicialmente sin la capacidad de permitir que un programa cargue bibliotecas adicionales después del inicio. [5]
La carga dinámica se utiliza con mayor frecuencia en la implementación de complementos de software . [1] Por ejemplo, los archivos de complemento de "objeto compartido dinámico" del servidor web Apache son bibliotecas que se cargan en tiempo de ejecución con carga dinámica. [6] La carga dinámica también se utiliza en la implementación de programas informáticos donde varias bibliotecas diferentes pueden proporcionar la funcionalidad necesaria y donde el usuario tiene la opción de seleccionar qué biblioteca o bibliotecas proporcionar.*.dso
No todos los sistemas admiten la carga dinámica. Los sistemas operativos tipo Unix, como macOS , Linux y Solaris , ofrecen carga dinámica con la biblioteca "dl" del lenguaje de programación C. El sistema operativo Windows ofrece carga dinámica a través de la API de Windows .
Nombre | API estándar POSIX/Unix | API de Microsoft Windows |
---|---|---|
Inclusión de archivo de encabezado | #include <dlfcn.h> | #include <windows.h> |
Definiciones de encabezado | dl ( | kernel32.dll |
Cargando la biblioteca | dlopen | LoadLibrary LoadLibraryEx |
Extrayendo contenidos | dlsym | GetProcAddress |
Descargando la biblioteca | dlclose | FreeLibrary |
La carga de la biblioteca se realiza con LoadLibrary
o LoadLibraryEx
en Windows y con dlopen
en sistemas operativos tipo Unix . A continuación se muestran algunos ejemplos:
void * sdl_library = dlopen ( "libSDL.so" , RTLD_LAZY ); if ( sdl_library == NULL ) { // informar error ... } else { // usar el resultado en una llamada a dlsym }
Como biblioteca Unix :
void * sdl_library = dlopen ( "libSDL.dylib" , RTLD_LAZY ); if ( sdl_library == NULL ) { // informar error ... } else { // usar el resultado en una llamada a dlsym }
Como marco de macOS :
void * sdl_library = dlopen ( "/Library/Frameworks/SDL.framework/SDL" , RTLD_LAZY ); if ( sdl_library == NULL ) { // informar error ... } else { // usar el resultado en una llamada a dlsym }
O si el marco o paquete contiene código Objective-C:
NSBundle * bundle = [ NSBundle bundleWithPath : @"/Library/Plugins/Plugin.bundle" ]; NSError * err = nil ; if ([ bundle loadAndReturnError :& err ]) { // Utilizar las clases y funciones en el paquete. } else { // Manejar el error. }
HMODULE sdl_library = LoadLibrary ( TEXT ( "SDL.dll" )); if ( sdl_library == NULL ) { // informar error ... } else { // usar el resultado en una llamada a GetProcAddress }
La extracción del contenido de una biblioteca cargada dinámicamente se logra con GetProcAddress
en Windows y con dlsym
en sistemas operativos tipo Unix .
void * initializer = dlsym ( sdl_library , "SDL_Init" ); if ( initializer == NULL ) { // informar error ... } else { // convertir initializer a su tipo apropiado y usarlo }
En macOS, al utilizar paquetes Objective-C, también se puede:
Clase rootClass = [ bundle principalClass ]; // Alternativamente, se puede utilizar NSClassFromString() para obtener una clase por nombre. if ( rootClass ) { id object = [[ rootClass alloc ] init ]; // Utilizar el objeto. } else { // Informar error. }
Inicializador FARPROC = GetProcAddress ( sdl_library , "SDL_Init" ); if ( inicializador == NULL ) { // informar error ... } else { // convertir el inicializador a su tipo y uso adecuados }
El resultado de dlsym()
o GetProcAddress()
debe convertirse en un puntero del tipo apropiado antes de poder usarse.
En Windows, la conversión es sencilla, ya que FARPROC es esencialmente un puntero de función :
tipo definido INT_PTR ( * FARPROC )( vacío );
Esto puede resultar problemático cuando se desea recuperar la dirección de un objeto en lugar de una función. Sin embargo, normalmente se desean extraer funciones de todos modos, por lo que esto no suele ser un problema.
typedef void ( * sdl_init_function_type )( void ); sdl_init_function_type init_func = ( sdl_init_function_type ) inicializador ;
Según la especificación POSIX, el resultado de dlsym()
es un void
puntero. Sin embargo, no es necesario que un puntero de función tenga el mismo tamaño que un puntero de objeto de datos y, por lo tanto, una conversión válida entre un tipo void*
y un puntero a una función puede no ser fácil de implementar en todas las plataformas.
En la mayoría de los sistemas que se utilizan hoy en día, los punteros a funciones y objetos son convertibles de facto . El siguiente fragmento de código demuestra una solución alternativa que permite realizar la conversión de todos modos en muchos sistemas:
typedef void ( * sdl_init_function_type )( void ); sdl_init_function_type init_func = ( sdl_init_function_type ) inicializador ;
El fragmento anterior emitirá una advertencia sobre algunos compiladores: warning: dereferencing type-punned pointer will break strict-aliasing rules
Otra solución alternativa es:
typedef void ( * sdl_init_function_type )( void ); unión { sdl_init_function_type func ; void * obj ; } alias ; alias . obj = inicializador ; sdl_init_function_type init_func = alias . func ;
que desactiva la advertencia incluso si el alias estricto está en vigor. Esto hace uso del hecho de que la lectura de un miembro de la unión diferente al que se escribió más recientemente (llamado " juego de palabras de tipos ") es común y se permite explícitamente incluso si el alias estricto está en vigor, siempre que se acceda a la memoria a través del tipo de unión directamente. [7] Sin embargo, este no es estrictamente el caso aquí, ya que el puntero de función se copia para usarse fuera de la unión. Tenga en cuenta que este truco puede no funcionar en plataformas donde el tamaño de los punteros de datos y el tamaño de los punteros de función no es el mismo.
El hecho es que cualquier conversión entre punteros de funciones y objetos de datos debe considerarse como una extensión de implementación (inherentemente no portable), y que no existe una forma "correcta" para una conversión directa, ya que en este sentido los estándares POSIX e ISO se contradicen entre sí.
Debido a este problema, la documentación POSIX dlsym()
para la versión obsoleta 6 declaró que "una versión futura puede agregar una nueva función para devolver punteros de función, o la interfaz actual puede quedar obsoleta en favor de dos nuevas funciones: una que devuelve punteros de datos y la otra que devuelve punteros de función". [8]
Para la versión posterior del estándar (número 7, 2008), se discutió el problema y la conclusión fue que los punteros de función deben ser convertibles para void*
cumplir con el estándar POSIX. [8] Esto requiere que los creadores de compiladores implementen una conversión funcional para este caso.
Si el contenido de la biblioteca se puede modificar (es decir, en el caso de una biblioteca personalizada), además de la función en sí, se puede exportar un puntero a ella. Dado que un puntero a un puntero de función es en sí mismo un puntero de objeto, este puntero siempre se puede recuperar legalmente mediante una llamada a dlsym()
y una conversión posterior. Sin embargo, este enfoque requiere mantener punteros separados a todas las funciones que se van a utilizar externamente y los beneficios suelen ser pequeños.
La carga de una biblioteca hace que se asigne memoria; la biblioteca debe desasignarse para evitar una fuga de memoria . Además, no descargar una biblioteca puede impedir las operaciones del sistema de archivos en el archivo que contiene la biblioteca. La descarga de la biblioteca se realiza con FreeLibrary
en Windows y con en sistemas operativosdlclose
tipo Unix . Sin embargo, la descarga de una DLL puede provocar fallos del programa si los objetos de la aplicación principal hacen referencia a la memoria asignada dentro de la DLL. Por ejemplo, si una DLL introduce una nueva clase y se cierra, las operaciones posteriores en instancias de esa clase desde la aplicación principal probablemente provocarán una violación de acceso a la memoria. Del mismo modo, si la DLL introduce una función de fábrica para instanciar clases cargadas dinámicamente, llamar o desreferenciar esa función después de que se cierre la DLL conduce a un comportamiento indefinido.
dlclose ( sdl_library );
Biblioteca gratuita ( sdl_library );
Las implementaciones de carga dinámica en sistemas operativos tipo Unix y Windows permiten a los programadores extraer símbolos del proceso que se está ejecutando actualmente.
Los sistemas operativos tipo Unix permiten a los programadores acceder a la tabla de símbolos global, que incluye tanto el ejecutable principal como las bibliotecas dinámicas cargadas posteriormente.
Windows permite a los programadores acceder a los símbolos exportados por el ejecutable principal. Windows no utiliza una tabla de símbolos global y no tiene una API para buscar en varios módulos un símbolo por su nombre.
void * este_proceso = dlopen ( NULL , 0 );
HMODULE este_proceso = GetModuleHandle ( NULL ); HMODULE este_proceso_de_nuevo ; GetModuleHandleEx ( 0 , 0 , & este_proceso_de_nuevo );
En el lenguaje de programación Java , las clases se pueden cargar dinámicamente mediante el ClassLoader
objeto. Por ejemplo:
Tipo de clase = ClassLoader . getSystemClassLoader (). loadClass ( nombre ); Objeto obj = tipo . newInstance ();
El mecanismo Reflection también proporciona un medio para cargar una clase si aún no está cargada. Utiliza el cargador de clases de la clase actual:
Tipo de clase = Clase . forName ( nombre ); Objeto obj = tipo . newInstance ();
Sin embargo, no existe una forma sencilla de descargar una clase de forma controlada. Las clases cargadas solo se pueden descargar de forma controlada, es decir, cuando el programador así lo desea, si el cargador de clases utilizado para cargar la clase no es el cargador de clases del sistema y se descarga a su vez. Al hacerlo, es necesario observar varios detalles para garantizar que la clase se descargue realmente. Esto hace que la descarga de clases sea tediosa.
La descarga implícita de clases, es decir, de forma no controlada por el recolector de basura, ha cambiado algunas veces en Java. Hasta Java 1.2, el recolector de basura podía descargar una clase siempre que considerara que necesitaba el espacio, independientemente del cargador de clases que se utilizara para cargar la clase. A partir de Java 1.2, las clases cargadas a través del cargador de clases del sistema nunca se descargaban y las clases cargadas a través de otros cargadores de clases solo cuando se descargaba este otro cargador de clases. A partir de Java 6, las clases pueden contener un marcador interno que indique al recolector de basura que pueden descargarse si el recolector de basura desea hacerlo, independientemente del cargador de clases utilizado para cargar la clase. El recolector de basura es libre de ignorar esta sugerencia.
De manera similar, las bibliotecas que implementan métodos nativos se cargan dinámicamente mediante el System.loadLibrary
método. No hay ningún System.unloadLibrary
método.
A pesar de su promulgación en la década de 1980 a través de Unix y Windows, algunos sistemas aún optaron por no agregar, o incluso eliminar, la carga dinámica. Por ejemplo, Plan 9 de Bell Labs y su sucesor 9front evitan intencionalmente el enlace dinámico, ya que lo consideran "dañino". [9] El lenguaje de programación Go , de algunos de los mismos desarrolladores de Plan 9, tampoco admitía el enlace dinámico, pero la carga de complementos está disponible desde Go 1.8 (febrero de 2017). El entorno de ejecución de Go y cualquier función de biblioteca se vinculan estáticamente al binario compilado. [10]