Usare DLL in C++

« Older   Newer »
  Share  
Jakub1996
view post Posted on 28/2/2012, 18:41




Usare DLL [GUIDA C++] by MrWebMaster

Una DLL, per definizione, è una libreria a caricamento dinamico (Dynamic Link Library ). In quanto libreria è un file che esporta dati, risorse e codice, visibili al programmatore nel momento in cui questi vengono importati.
Il caricamento effettivo degli elementi importati avviene sempre a runtime (tempo di esecuzione), in due modi fondamentali:
il caricamento (loading) è effettuato prima di utilizzare la risorsa importata per la prima volta
il caricamento avviene subito dopo che la risorsa è stata importata, dunque all'avvio del programma
L'uso delle librerie dinamiche offre diversi vantaggi operativi. Si pensi ad una libreria di esempio che contenga la semplice funzione.

CODICE
int fattoriale(int base);


Il codice efficiente che svolge il calcolo in questione è ben noto, e la funzione è di uso relativamente frequente. Senza utilizzare librerie, ogni programma che impiega la funzione dovrebbe contenere al suo interno il prototipo ed il codice in forma esplicita.
L'uso della libreria permette la semplice importazione del prototipo, mentre il codice sorgente che implementa la funzione è oscuro al programmatore (la libreria è un file compilato, analogamente al file eseguibile).
Saltano all'occhio diverse proprietà vantaggiose offerte dalle DLL: in primo luogo la modularità, un contenuto della libreria è indipendente da qualsiasi programma; si garantisce quindi la massima portabilità (qualunque programma può utilizzarla semplicemente importando le risorse offerte). Una DLL è importabile anche da un programma scritto con un linguaggio di programmazione diverso da quello con cui è stata complilata, si guadagna quindi in flessibilità.
Una DLL è sempre aggiornabile, se si riscontrano problemi nell'implementazione, errori o banalmente implementazioni migliori, si può riscrivere e ricompilare la sola libreria; per aggiornare ognuno dei programmi che la utilizzano sarà necessario sostituire il solo file DLL chiamato.
Le DLL sono librerie condivise; capita spesso che alcune funzioni molto frequenti, ad esempio le funzioni di sistema, siano richiamate da una parte rilevante di programmi. In questo caso si ha un notevole risparmio di spazio fisico perchè il codice importato è presente in memoria secondaria una sola volta. Senza librerie sarebbe stato obbligatorio avere una copia delle risorse per ognuno dei programmi utilizzatori.
Un programma può richiedere l'esecuzione di un servizio solo per una fase limitata del suo funzionamento. Il caricamento dinamico permette che una funzione di libreria sia caricata e rilasciata a tempo di esecuzione, in questo modo si può razionalizzare ed ottimizzare l'utilizzo delle risorse, in particolar modo nei casi più critici in cui si dispone di risorse limitate.
Infine si noti la protezione del codice: il fatto che la DLL sia compilata garantisce al programmatore la possibilità di diffondere il suo prodotto, ad esempio una funzione, facendo in modo che l'utilizzatore possa impiegarlo senza conoscere l'implementazione, che può essere quindi tenuta segreta.
Struttura di una DLL
Una DLL ha una struttura analoga ad un file eseguibile, suddivisibile in tre sezioni principali come esposto in figura.
Quando la libreria viene caricata è eseguito immediatamente il codice relativo all' Entry Point: la funzione DllMain (un file EXE ha come entry point la funzione Main).
Il resto del file è costituito da elementi esportati, comunemente funzioni, che il programmatore può importare direttamente ed indipendentemente dal programma principale.
Contenuto di una DLL
Una DLL può contenere:
L' Entry Point
In Win32 tutte le librerie di collegamento dinamico (DLL) possono contenere una funzione di punto di ingresso facoltativa, generalmente denominata DllMain, chiamata per inizializzazione e terminazione. Ciò consente l'opportunità di assegnare o rilasciare risorse aggiuntive secondo le necessità. Windows chiama la funzione di punto di ingresso in quattro situazioni: aggancio e disconnessione da un processo, connessione e disconnessione dai thread. (Ref: L'inizializzazione di una DLL tramite DLLMain punto di ingresso facoltativa) Tramite il valore di un parametro (fdwReason) è possibile risalire a quale degli eventi citati ha scatenato l'attivazione dell'entry point.
Variabili e funzioni esportate
Elementi il cui accesso è consentito ai processi e alle funzioni che importano la libreria. Si tende ad esportare soprattutto le funzioni.
Una DLL C++, se si usa l'ambiente VisualStudio, è esportata ed importata tramite le direttive __declspec(xxx), ma questo verrà visto nel dettaglio in seguito.
Variabili e funzioni private
Elementi il cui accesso è consentito solo a membri interni alla stessa libreria. Solitamente le variabili di libreria sono private.
Risorse Solitamente accodate alla fine del file, le risorse sono elementi di supporto all'applicazione come immagini, icone, video, e file html. Si includono all'interno del file compilato per mantenere una certa compattezza dell'applicazione (pochi file talvolta sono meglio di tanti file, specie nelle applicazioni semplici). Oltre questo si possono nascondere sotto la compilazione file che non si intende distribuire separatamente.
Non sono affatto rari i casi in cui la DLL è un file che contiene solo risorse (si pensi alle librerie di icone di sistema di Windows).
Ogni processo che utilizza la stessa DLL dispone di una propria copia delle variabili globali esportate.
Oltre questo fatto, di norma in una libreria è buona cosa che il codice sia condiviso in "read only" e che le variabili non siano condivise (questo implica l'esistenza di una copia delle variabili per ognuno dei processi utilizzatori).
Questo fatto non è obbligatorio; un programmatore con esigenze particolari può effettuare (a suo rischio) scelte differenti.
Esportazione di una DLL in Microsoft VisualStudio
All'interno di una DLL le funzioni esportate si distinguono dalla normali funzioni per via della specifica dichiarazione.
Il modo "standard" (proposto da Microsoft) per la semplice esportazione di una DLL prevede la creazione di una define condizionale come indicato nell'esempio.
Una volta creata la direttiva si possono dichiarare le variabili e le funzioni che si intendono esportare grazie alla stessa.

CODICE
#ifndef NOME_API
       #define NOME_API extern "C" __declspec(dllexport)
#endif

NOME_API int funzione(const wchar_t* parametro1, const wchar_t* parametro2);


In VisualStudio, per indicare il tipo d'uso delle funzioni si usa la direttiva __declspec(dllexport), la quale non è standard C++.
Similarità e differenze tra DLL e applicazioni

Le DLL e le applicazioni hanno in comune tra loro il fatto che entrambi sono moduli eseguibili di un programma. Nonostante questo differiscono tecnicamente in vari aspetti. L'utente medio noterà senza dubbio la differenza più evidente: le DLL non sono programmi direttamente eseguibili mentre le applicazione EXE si.
Il sistema ed il programmatore invece osservano, oltre che certamente la prima, altre due differenze fondamentali:
Un'applicazione può essere eseguita contemporaneamente in più istanze, una stessa libreria è istanziata una sola volta;
Un'applicazione può contenere uno stack, una memoria globale, una coda di messaggi ed handle a file.
Creazione di una DLL in Microsoft VisualStudio
Una DLL come gia visto ha una struttura simile al file eseguibile. I contenuti (in sostanza codice e risorse) sono di tipo analogo.
Un progetto DLL, la cui creazione è possibile attraverso la voce di menù new avrà dunque un aspetto assolutamente simile al classico progetto di una normale applicazione.
A livello pratico , tralasciando la definizione delle risorse che tra l'altro è gestita per via grafica dall'IDE allo stesso modo dei progetti EXE, riporto un esempio sui principali elementi che i progetti DLL possono contenere:
Entry point:

CODICE
BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID fImpLoad)
{
   TRACE("%­p 0x%­lx %­p\n", hInstDLL, fdwReason, fImpLoad);
   switch (fdwReason) {
       
   case DLL_PROCESS_ATTACH:
       tls_index = TlsAlloc();
       break;
   case DLL_PROCESS_DETACH:
       free_per_thread_data();
       TlsFree( tls_index );
       num_startup = 0;
       break;
   case DLL_THREAD_DETACH:
       free_per_thread_data();
       break;
   }
   return TRUE;
}


Funzione generica esportata:

CODICE
NOME_API int search(const wchar_t* filename, const wchar_t* pattern) {

       HANDLE hFile;

         ... codice omesso per brevità :)        

       CloseHandle(hFile);

       return count;
}


Funzione generica privata:

CODICE
int search(const wchar_t* filename, const wchar_t* pattern) {

       HANDLE hFile;

         ... codice omesso per brevità :)        

       CloseHandle(hFile);

       return count;
}


Importazione di una DLL in Microsoft VisualStudio

Una funzione di libreria si importa mediante la direttiva non standard __declspec(dllimport).

CODICE
#define DllImport   __declspec( dllimport )

DllImport int  j;
DllImport void func();


L'utilizzo di __declspec(dllimport) è facoltativo nelle dichiarazioni di funzioni, ma consente al compilatore di produrre un codice più efficiente. È necessario utilizzare __declspec(dllimport) per consentire all'eseguibile che importa di accedere agli oggetti e ai simboli pubblici della DLL.
Compilazione e caricamento di una DLL

La fase di compilazione di una DLL, la quale prevederebbe diversamente dal caso dei file eseguibili opzioni particolari del compilatore, è preconfigurata automaticamente in VisualStudio che sceglie le opzioni coerentemente col tipo di progetto con cui si lavora. La libreria compilata puo essere caricata in maniere diverse, in modo statico o dinamico.
Caricamento statico (implicito:(
Il collegamento implicito a una DLL, prevede il possesso dei seguenti oggetti da parte del creatore del progetto che intende importare la libreria
La libreria vera e propria compilata (.DLL).
Un file header (.h) contenente le dichiarazioni degli elementi esportati. Tutte le classi, le funzioni e i dati dovrebbero essere caratterizzati dalla direttiva: __declspec(dllimport).
Una libreria di importazione, (.LIB), alla quale effettuare il link statico. Quando viene generata la DLL la libreria di importazione è creata dal linker.
Il programma che deve caricare la libreria lo fa in fase di creazione del processo.
Il file .LIB associato alla DLL è collegato al programma in modo statico. Le funzioni della DLL vengono rese disponibili all'interno del programma che carica la libreria mediante mapping automatico.
Caricamento dinamico (esplicito:(
Le applicazioni devono effettuare una chiamata a funzione per caricare direttamente la DLL in fase di esecuzione.
La procedura standard consiste nel chiamare le seguenti funzioni in modo opportuno:
LoadLibrary: Carica la DLL e permette di ottenere l'handle necessario a manipolare le risorse offerte dalla libreria.
GetProcAddress: Restituisce un puntatore per ciascuna funzione esportata che l'applicazione intende chiamare. Poiché le applicazioni chiamano le funzioni della DLL tramite un puntatore, il compilatore non genera riferimenti esterni.
FreeLibrary: Rilascia la risorsa impegnata dal sistema dopo avere terminato le operazioni relative alla DLL.
Condivisione dei dati
Il problema della condivisione dei dati tra processi che utilizzano la stessa libreria non può essere risolto banalmente dall'impiego di variabili globali. Infatti per default di queste ne è disponibile una copia per ogni processo che importa la libreria.
La soluzione potrebbe essere quella di allocare le variabili globali in un segmento di memoria condivisa, ma questo sistema pare contorto e poco flessibile per il programmatore.
Comunque ne illustriamo un semplice esempio:

CODICE
#pragma data_seg("SHARED")  // Inizio del data segment condiviso.  
// Definizione della parte condivisa
int sharedint = 0; // Inizializzare SEMPRE la variabile pena la non condivisione della stessa..
#pragma data_seg()          // Fine del data segment condiviso.  
#pragma comment(linker, "/section:SHARED,RWS")


Al fine di risolvere la stessa questione viene incontro un particolare tipo di oggetto kernel chiamato FileMapping, il quale viene dichiarato sovente nella funzione di EntryPoint.
L'istanziatore dedicato CreateFileMapping(...) esegue una creazione condizionale del seguente tipo:
Se l'oggetto non è stato ancora creato lo alloca e ne restituisce l'handle
Se l'oggetto è stato gia creato viene semplicemente restituito l'handle

CODICE
HANDLE WINAPI CreateFileMapping(
 __in      HANDLE hFile,
 __in_opt  LPSECURITY_ATTRIBUTES lpAttributes,
 __in      DWORD flProtect,
 __in      DWORD dwMaximumSizeHigh,
 __in      DWORD dwMaximumSizeLow,
 __in_opt  LPCTSTR lpName
);


Quando tutti i chiamanti la libreria rilasciano la stessa anche l'oggetto FileMapping è rilasciato.
 
Top
0 replies since 28/2/2012, 18:41   82 views
  Share