Hasta ahora, en varios artículos referentes a este tema: Introducción a dynamic linking, Carga dinámica de bibliotecas, hemos visto como, en lenguaje C, podemos escribir bibliotecas de funciones y, sin necesidad de conocer su código fuente, ejecutar funciones encerradas dentro de esas bibliotecas en nuestros proyectos, así como cargar dicho código al vuelo.
Pero todo se complica un poco cuando queremos hacer lo mismo con C++, y en concreto, cuando estamos exportando clases completas, ya que de primeras necesitamos una forma de traernos algo parecido a un tipo, el nombre de la clase, y con dlopen() y dlsym(), nos podemos traer sólo una referencia de memoria, por lo que junto con la clase que programemos en C++ tendremos que incluir una función que llame al constructor. Por otra parte, cuando compilamos en C++, las referencias a las funciones se guardan de una forma especial y tendremos que exportarlos como símbolos de C (en realidad siempre que hagamos shared objects en C++ debemos hacerlo.
Por otra parte, cuando estemos enlazando clases dinámicamente, debemos conocer qué tiene la clase, a la hora de acceder a sus métodos y atributos, antes de la compilación se debe conocer qué contiene la clase. Esto, en principio puede limitarnos mucho: podremos ahorrar memoria, estaremos limitando la estructura de la clase.
Desarrollaremos un ejemplo de una agenda personal, en la que aceptamos el nombre y el mail de una persona. La clase Person se desarrollará en un shared object que el programa principal instanciará, este shared object será capaz de exportar la información para su salida en pantalla (luego crearemos otro shared object que la exporte en XML).
Primero vamos a construir una interfaz de Person (la llamaremos _Person) con los métodos del programa principal que vamos a necesitar en nuestra aplicación (en el caso de querer que otras personas desarrollen bibliotecas que nuestro programa vaya a aceptar, esta interfaz sería la parte que debemos dar a los desarrolladores).
_person.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* @(#)_person.h */ #ifndef _PPERSON_H #define _PPERSON_H 1 #include <string> using namespace std; class _Person { public: virtual ~_Person() { }; // Importante no poner =0; Este método tiene que implementarse, aunque sea vacío virtual void setNombre(string nombre)=0; virtual void setEmail(string email)=0; virtual string getData()=0; }; #endif /* _PPERSON_H */ |
Como interfaz, todos los métodos que encontramos en esta clase son virtuales. Esta clase no podrá ser instanciada, sólo podrá ser utilizada como punto de partida para la construcción de otras clases.
Ahora procederemos creando la clase Person (que exportará a pantalla el nombre y el mail de la persona introducida)
person.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /* @(#)person.h */ #ifndef _PERSON_H #define _PERSON_H 1 #include "_person.h" #include <string> using namespace std; class Person: public _Person { public: Person(string nombre, string email); ~Person(); void setNombre(string nombre); void setEmail(string email); string getData(); private: string nombre; string email; }; #endif /* _PERSON_H */ |
Ahora procederemos a la creación del archivo cpp que incluirá dos funciones en C (una instanciará la clase y nos devolverá la referencia de memoria de dicho objeto, y otra destruirá el objeto)
person.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #include "person.h" using namespace std; Person::Person(string nombre, string email):nombre(nombre),email(email) { } Person::~Person() { } void Person::setNombre(string nombre) { this->nombre=nombre; } void Person::setEmail(string email) { this->email=email; } string Person::getData() { return "Nombre: "+nombre+"\nEmail: "+email+"\n\n"; } extern "C" { Person* create(string nombre, string email) { return new Person(nombre, email); } void destroy(Person *p) { delete p; } } |
En este archivo vemos, que para exportar en formato C, utilizamos la palabra clave extern «C». Nuestras funciones se llamarán create() y destroy(), y éstos son los métodos a los que llamará nuestro programa principal.
Para compilar la clase Person, hacemos lo siguiente:
$ g++ -fPIC -shared -o person.so person.cpp
El programa principal, conocerá el prototipo _Person, el prototipo de la función create(string, string) y el prototipo de la función destroy(_Person*), con lo cual ya tenemos lo suficiente para crear una instancia, destruirla y utilizarla:
agenda.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <cstdlib> #include <iostream> #include "_person.h" #include <dlfcn.h> #include <string> using namespace std; typedef _Person* (*_PersonCreate)(string nombre, string email); typedef void (*_PersonDestroy)(_Person* p); int main(int argc, char *argv[]) { void *personlib = dlopen("./person.so", RTLD_LAZY); _PersonCreate personCreate = (_PersonCreate)dlsym(personlib, "create"); if (dlerror()!=NULL) { cerr <<"ERROR"<<endl; exit(1); } _PersonDestroy personDestroy = (_PersonDestroy)dlsym(personlib, "destroy"); if (dlerror()!=NULL) { cerr <<"ERROR"<<endl; exit(1); } _Person *persona = personCreate("Gaspar Fernandez", "email@midominio.com"); cout << persona->getData(); personDestroy(persona); dlclose(personlib); return EXIT_SUCCESS; } |
Para compilar agenda.cpp hacemos lo siguiente:
$ g++ -o agenda agenda.cpp -ldl
Ahora sólo queda imaginar una forma de poder hacer el programa principal más sencillo, aunque complicaremos un poco una parte del programa, en concreto vamos a hacer una clase estática llamada PersonLoader que nos ayudará cada vez que tengamos que crear una instancia de _Person; si sólo vamos a crear una instancia, una vez no pasa nada, pero si complicamos el programa, sí que nos será útil:
personloader.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | /* @(#)personloader.h */ #ifndef _PERSONLOADER_H #define _PERSONLOADER_H 1 #include "_person.h" #include <dlfcn.h> #include <string> using namespace std; class PersonLoader { public: typedef enum { ERROR_EXCEPTION, ERROR_EXIT, ERROR_RETURN, ERROR_IGNORE } ErrorAction; /* Error handling */ static void setErrorAction(ErrorAction a); /* Library loading */ static void setLib(string libFile); static bool loadLib(string libFile); static bool loadLib(); static bool unloadLib(); /* Object creation/destruction */ static _Person *create(string nombre, string email); static bool destroy(_Person *p); /* Debugging */ static void setDebugging(int value); private: static bool error (string msg); static void exitError(string msg); static void debugMsg(string msg); static string libFile; static void *libperson; static int debugging; static ErrorAction errorAction; }; #endif /* _PERSONLOADER_H */ |
personloader.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | #include "personloader.h" #include <iostream> #include <cstdlib> #include <stdexcept> using namespace std; void *PersonLoader::libperson = NULL; PersonLoader::ErrorAction PersonLoader::errorAction=ERROR_EXCEPTION; string PersonLoader::libFile = "./person.so"; int PersonLoader::debugging=1; void PersonLoader::setErrorAction(PersonLoader::ErrorAction a) { PersonLoader::errorAction=a; } void PersonLoader::setLib(string libFile) { PersonLoader::libFile=libFile; PersonLoader::debugMsg("Library file set to "+libFile); } bool PersonLoader::loadLib() { if (libFile!="") { if (libperson!=NULL) return true; // Library already loaded libperson = dlopen(libFile.c_str(), RTLD_LAZY); if (libperson==NULL) return PersonLoader::error("Library "+libFile+" could not be loaded"); PersonLoader::debugMsg("Library file "+libFile+" loaded successfully."); } else return PersonLoader::error("Empty library filename"); return true; } bool PersonLoader::loadLib(string libFile) { PersonLoader::setLib(libFile); return PersonLoader::loadLib(); } bool PersonLoader::unloadLib() { if (libperson==NULL) return true; int err = dlclose(libperson); libperson=NULL; if (err!=0) return error("Couldn't unload "+libFile+" properly"); PersonLoader::debugMsg("Library file "+libFile+" unloaded successfully."); return true; } _Person* PersonLoader::create(string nombre, string email) { char *err; if (!loadLib()) { PersonLoader::error("Library not loaded"); return NULL; } _PersonCreate personCreate = (_PersonCreate)dlsym(libperson, "create"); if ((err=dlerror())!=NULL) { PersonLoader::error("Error finding create Symbol: "+(string)err); return NULL; } PersonLoader::debugMsg("Symbol create loaded."); return personCreate(nombre, email); } bool PersonLoader::destroy(_Person *p) { char *err; if (!loadLib()) return (PersonLoader::error("Library not loaded"))?true:false; _PersonDestroy personDestroy = (_PersonDestroy)dlsym(libperson, "destroy"); if ((err=dlerror())!=(char*)NULL) return (PersonLoader::error("Error finding destroy Symbol: "+(string)err))?true:false; PersonLoader::debugMsg("Symbol destroy loaded."); personDestroy(p); return true; } bool PersonLoader::error(string msg) { switch (errorAction) { case ERROR_EXCEPTION: throw runtime_error(msg); break; case ERROR_EXIT : exitError(msg); break; case ERROR_RETURN : return false; break; case ERROR_IGNORE : return true; break; default: return false; } } void PersonLoader::exitError(string msg) { cerr << "ERROR: "<<msg<<endl; exit(1); } void PersonLoader::setDebugging(int value) { PersonLoader::debugging=(value>0)?1:0; } void PersonLoader::debugMsg(string msg) { if (debugging) cerr << msg << endl; } |
De esta clase hay muchas cosas que podemos quitar, pero me ha parecido interesante incluir:
- Un modo depuración (debugging)
- Elección del tipo de respuesta a los errores (desde una excepción, a salir del programa o hasta ignorarlo)
- Posibilidad de carga y descarga de la biblioteca a mano
Por otra parte, esta clase nos puede servir como base para otros cargadores de otras bibliotecas compartidas, podremos fácilmente hacer que los métodos create y destroy sean abstractos, dependiendo de los parámetros que les especifiquemos o haya que especificarle a la función create() de la biblioteca compartida. Por supuesto podemos, para un programa definitivo, poner debugging a 0 para no mostrar mensajes en pantalla.
Ahora, el programa principal agenda.cpp quedará así:
agenda.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <cstdlib> #include <iostream> #include <string> #include "personloader.h" using namespace std; int main(int argc, char *argv[]) { PersonLoader::setErrorAction(PersonLoader::ERROR_IGNORE); _Person *persona=PersonLoader::create("Gaspar Fernandez", "email@midominio.com"); cout << persona->getData(); PersonLoader::destroy(persona); PersonLoader::unloadLib(); } |
Y para compilarlo hacemos:
$ gcc -o agenda agenda.cpp personloader.cpp -ldl
Como vemos, ahora es mucho más sencillo trabajar con los shared objects desde uno de nuestros programas.
Foto: Hey Paul (Flickr) CC-A a 02/03/2013
Leave a Reply