In C++, una keyword indica che una funzione deve impiegare la convenzione di linking (il termine comunemente usato è "binding" - N.d.T.) usata in C: extern "C". Una funzione dichiarata extern "C" viene rappresentata da un simbolo coincidente con il nome della funzione, esattamente come in C. Per questa ragione, solo funzioni che non sono metodi di una classe possono essere dichiarate extern "C", e non possono essere overloaded.
Nonostante le serie limitazioni, funzioni dichiarate extern "C"
sono estremamente utili nel nostro scenario perché possono essere caricate
dinamicamente utilizzando dlopen
esattamente come una funzione C.
Questo non significa che una funzione dichiarata come extern "C" non può contenere codice C++. Tale funzione è una funzione C++ a tutti gli effetti, che può utilizzare le funzionalità del linguaggio C++ e ricevere qualunque tipo di argomento il programmatore desideri.
In C++ le funzioni vengono caricate esattamente
come in C, con una chiamata a dlsym
, ma le funzioni che
saranno caricate devono essere qualificate con extern "C" per
evitare che i simboli vengano decorati.
Esempio 1. Caricare una Funzione
main.cpp:
#include <iostream> #include <dlfcn.h> int main() { using std::cout; using std::cerr; cout << "C++ dlopen demo\n\n"; // open the library cout << "Opening hello.so...\n"; void* handle = dlopen("./hello.so", RTLD_LAZY); if (!handle) { cerr << "Cannot open library: " << dlerror() << '\n'; return 1; } // load the symbol cout << "Loading symbol hello...\n"; typedef void (*hello_t)(); // reset errors dlerror(); hello_t hello = (hello_t) dlsym(handle, "hello"); const char *dlsym_error = dlerror(); if (dlsym_error) { cerr << "Cannot load symbol 'hello': " << dlsym_error << '\n'; dlclose(handle); return 1; } // use it to do the calculation cout << "Calling hello...\n"; hello(); // close the library cout << "Closing library...\n"; dlclose(handle); }
hello.cpp:
#include <iostream> extern "C" void hello() { std::cout << "hello" << '\n'; }
La funzione hello
, definita in
hello.cpp come extern "C",
viene caricata in main.cpp con la chiamata
a dlsym
. La funzione deve essere dichiarata come
extern "C" perché altrimenti non sapremmo come
ricostruire il simbolo corrispondente al suo nome.
Esistono due versioni differenti della dichiarazione extern "C": la summenzionata versione extern "C", e la corrispondente extern "C" { … } con le dichiarazioni in parentesi graffe. La prima versione, detta inline, indica linking esterno secondo la convenzione C, mentre la seconda versione ha effetto solo sul linking del linguaggio. Le due dichiarazioni seguenti sono quindi equivalenti: e Dato che non c'è differenza tra una funzione dichiarata con extern ed una dichiarata senza, la differenza tra i due stili non è rilevante, purché non si stiano dichiarando variabili. Se si dichiarano delle variabili, non dimenticare che e non sono la stessa cosa.Per ulteriori chiarimenti, si faccia riferimento a [ISO14882] 7.5, prestando particolare attenzione al paragrafo 7, oppure si consulti [STR2000] al paragrafo 9.2.4. Prima di far cose troppo fantasiose con variabili extern, dare una scorsa ai documenti elencati nella sezione approfondimento. |
Caricare classi è un compito più complesso perché non ci serve solo un puntatore a funzione, ci serve un'istanza della classe.
Non possiamo istanziare la classe utilizzando new perché la classe non è definita nell'eseguibile, e perché, in certe circostanze, non ne conosciamo nemmeno il nome.
La soluzione è nell'usare le proprietà del polimorfismo: definiamo una classe interfaccia di base con metodi dichiarati virtuali nell'eseguibile e una classe di implementazione derivata da questa nel modulo che vogliamo caricare. Solitamente la classe che dichiara l'interfaccia è astratta (una classe è astratta se tutti i suoi metodi sono dichiarati virtuali).
Siccome il caricamento dinamico di classi viene generalmente usato per la creazione di plug-in - che devono definire una interfaccia chiaramente definita - si sarebbe dovuto definire una classe interfaccia ed una classe di implementazione in ogni caso.
A questo punto, si devono includere nel modulo altre due funzioni che ci assistano nel caricamento, dette funzioni class factory (letteralmente "fabbrica di classi" - N.d.T.). La prima di queste funzioni crea un istanza della classe e ritorna un puntatore a tale istanza, mentre l'altra si occupa di distruggere le classi che le vengono passate. Queste due funzioni sono dichiarate extern "C".
Per utilizzare la classe definita nel modulo, si carichino le due funzioni factory
per mezzo di dlsym
esattamente come si è già
illustrato nel precedente esempio.
Con questo meccanismo, si possono istanziare e distruggere tante
istanze della classe quante si desideri avere a disposizione.
Esempio 2. Caricare una Classe
In questo esempio una generica classe polygon
dichiara l'interfaccia che la classe derivata triangle
a sua volta implementa.
main.cpp:
#include "polygon.hpp" #include <iostream> #include <dlfcn.h> int main() { using std::cout; using std::cerr; // load the triangle library void* triangle = dlopen("./triangle.so", RTLD_LAZY); if (!triangle) { cerr << "Cannot load library: " << dlerror() << '\n'; return 1; } // reset errors dlerror(); // load the symbols create_t* create_triangle = (create_t*) dlsym(triangle, "create"); const char* dlsym_error = dlerror(); if (dlsym_error) { cerr << "Cannot load symbol create: " << dlsym_error << '\n'; return 1; } destroy_t* destroy_triangle = (destroy_t*) dlsym(triangle, "destroy"); dlsym_error = dlerror(); if (dlsym_error) { cerr << "Cannot load symbol destroy: " << dlsym_error << '\n'; return 1; } // create an instance of the class polygon* poly = create_triangle(); // use the class poly->set_side_length(7); cout << "The area is: " << poly->area() << '\n'; // destroy the class destroy_triangle(poly); // unload the triangle library dlclose(triangle); }
polygon.hpp:
#ifndef POLYGON_HPP #define POLYGON_HPP class polygon { protected: double side_length_; public: polygon() : side_length_(0) {} virtual ~polygon() {} void set_side_length(double side_length) { side_length_ = side_length; } virtual double area() const = 0; }; // the types of the class factories typedef polygon* create_t(); typedef void destroy_t(polygon*); #endif
triangle.cpp:
#include "polygon.hpp" #include <cmath> class triangle : public polygon { public: virtual double area() const { return side_length_ * side_length_ * sqrt(3) / 2; } }; // the class factories extern "C" polygon* create() { return new triangle; } extern "C" void destroy(polygon* p) { delete p; }
Alcune osservazioni sul processo di caricamento di classi:
È necessario definire sia una funzione factory per creare istanze della classe che per distruggerle: si deve evitare l'uso dell'operatore delete nell'eseguibile e sempre affidarsi al modulo per distruggere la classe. Questo è dovuto al fatto che in C++ gli operatori new e delete possono essere overloaded, il che può condurre ad una situazione in cui le chiamate a new e delete invocano versioni reciprocamente non corrispondenti, risultando in problemi come memory leak e segmentation fault (o in assolutamente nessun problema in certe circostanze). Lo stesso problema può verificarsi se diverse versioni delle librerie standard vengono usate per il linking del modulo e dell'eseguibile.
Il destructor della classe interfaccia deve solitamente essere virtuale. In rari casi questo può non essere necessario, ma il vantaggio che risulta dalla riduzione dell'overhead è così ridotto da non meritare il rischio.
Se la propria interfaccia di base non necessita di un destructor, se ne definisca comunque una non contenente istruzioni (e dichiararla virtual), altrimenti prima o poi si avranno problemi, posso garantirvelo. Ulteriori informazioni possono essere ottenute nella sezione 20 della FAQ della newsgroup comp.lang.c++ (http://www.parashift.com/c++-faq-lite/).