Mangiare Senza Glutine disponibile su App Store

Per altre informazioni scrivi a fabriziocaldarelli@negusweb.it

DLL di oggetti: implementazione ed uso

Da Programmazione Software.

Descrizione

Scrivere DLL di oggetti è un pò come reinventare COM (http://it.wikipedia.org/wiki/Component_Object_Model) con lo svantaggio di non aver a disposizione le stesse caratteristiche di gestione del sistema che consente la semplicità di distribuzione degli oggetti a cui COM ci ha abituato.

Ma veniamo a noi. Abbiamo detto nell’articolo precedente riguardante le DLL di funzioni (http://www.negusweb.it/c_cpp/panoramica_dll/dll_funzioni.php) che le DLL altro non sono che una raccolta di funzioni. Gli oggetti possiamo immaginarli come contenitori di funzioni, opportunamente denominati.

Nel caso di DLL di oggetti non possiamo parlare di implementazioni implicite o esplicite, ma di un solo tipo di implementazione che tra poco vedremo, che altro non è che la fusione necessaria delle due (per ovvii motivi che tra qualche righe capirete).

Ma passiamo subito al progetto DLLOggetti, che definisce appunto la nostra DLL di oggetti.


Progetto DLLOggetti

Questo progetto contiene il sorgente della nostra DLL che sarà costituita da una sola classe CProva che ha al suo interno due costruttori. Andiamo ad analizzare i 3 files che la compongono: stdafx.h, DllOggetti.h, DllOggetti.cpp.

stdafx.h
 
#define DLLOGGETTI_EXPORTS

Come per le DLL di funzioni si definisce DLLOGGETTI_EXPORTS, che ci consentirà di distinguere nel successivo file DllOggetti.h l’ambito di inclusione delle singole funzioni (se siamo in importazione o esportazione). Infatti quando DllOggetti.h viene chiamato in fase di compilazione della DLL parleremo di esportazione delle funzioni della DLL, mentre quando viene chiamato dal progetto che farà uso della DLL in modo implicito parleremo di importazione delle funzioni della DLL.


DllOggetti.h

#include "stdafx.h"
 
#ifdef DLLOGGETTI_EXPORTS
	#define EXPORT __declspec (dllexport)
#else
	#define EXPORT __declspec (dllimport)
#endif
 
class EXPORT CProva
{
private:
	int x,y,z;
public :
	CProva() { x=10; y=20; z=30; }
	CProva(int x1, int y1, int z1) { x=x1; y=y1; z=z1; }
	int getX() { return this->x; }
};
 
extern "C" EXPORT CProva* DLL_CProva()
{
	CProva *c = new CProva();
	return c;
}
extern "C" EXPORT CProva* DLL_CProva_int_int_int(int x1, int y1, int z1)
{
	CProva *c = new CProva(x1, y1, z1);
	return c;
}


Header delle funzioni e classi che costituiscono la DLL.

La #ifdef … #endif serve a distinguere l’ambito di compilazione delle funzioni della DLL. Infatti, come abbiamo detto poc’anzi, se siamo in fase di compilazione della DLL (e quindi esportazione delle funzioni incluse nella DLL), la definizione DLLOGGETTI_EXPORTS esisterà (perchè dichiarata nel file sfdafx.h). Se, invece, siamo in ambito di utilizzo della DLL, allora la definizione DLLOGGETTI_EXPORTS non sarà stata dichiarata e quindi siamo in fase di importazione delle funzioni della DLL.

Segue la definizione della classe, provvista di due costruttori. Quello di “default”, senza parametri, inizializza i tre attributi (x,y,z) con i valori 10,20 e 30. Da notare la diversa definizione della classe, rispetto allo standard, che comprende la definizione EXPORT che serve, come anche nelle funzioni della DLL, a distinguere l’ambito di definizione della classe, se siamo in importazione o esportazione della DLL.

Adesso arriva la parte di codice veramente interessante. Proiettiamoci sull’utilizzo della DLL. Per poter utilizzare una DLL abbiamo a disposizione soltanto delle funzioni (GetProcAddress per intenderci) che ci consentono di richiamare le funzioni all’interno della DLL stessa. E per gli oggetti? Come facciamo richiamare gli oggetti che sono all’interno della DLL? Tramite GetProcAddress non possiamo, anche volendo fare una prova, richiamare direttamente il costruttore CProva, perchè il metodo (costruttore) CProva è incapsulato direttamente dentro la classe e quindi non è direttamente visibile.

A questo punto visto che possiamo soltanto chiamare funzioni “direttamente” visibili, ci viene in mente che possiamo scrivere una funzione “wrapper” che ha l’unico scopo di creare l’oggetto e di rilasciarlo, proprio come fa la funzione DLL_CProva.

Nel nostro caso ho scritto due funzioni wrapper per i due costruttori dell’oggetto CProva, ovvero DLL_CProva e DLL_CProva_int_int_int. Molto semplicemente il nome della seconda funzione wrapper ha come suffisso i tipi di parametri che accetta (int, int, int) per distinguerlo dalla prima funzione wrapper. Il codice contenuto all’interno di queste due funzioni è semplicemente costruire l’oggetto e rilasciarlo.

Ricordiamoci che tutte le funzioni messe a disposizione dalla DLL sono precedute necessariamente dal prefisso EXPORT che stabilisce la modalità di compilazione delle funzioni (esportazione se siamo in fase di compilazione della DLL, importazione se siamo in fase di compilazione del programma che utilizzarà la DLL).

Gli occhi più attenti al codice avranno già notato almeno due debolezze di questo sistema. Il primo è che necessariamente dobbiamo almeno definire tante funzioni wrapper (del costruttore vero e proprio dell’oggetto) tante quante sono le classi contenute nella DLL. Il secondo è che dobbiamo fornire all’utilizzatore la struttura delle classi contenute nella DLL per poterle utilizzare, perchè altrimenti l’utilizzare non saprebbe chiaramente come usufruire del puntatore CProva* rilasciato da entrambe le funzioni DLL_CProva e DLL_CProva_int_int_int.

DllOggetti.cpp

#include "stdafx.h"
#include 
#include “DllOggetti.h”
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
    // Perform actions based on the reason for calling.
    switch( ul_reason_for_call )
    {
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
			printf(”DLL_PROCESS_ATTACH\n”);
            break;
 
        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
			printf(”DLL_THREAD_ATTACH\n”);
            break;
 
        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
			printf(”DLL_THREAD_DETACH\n”);
            break;
 
        case DLL_PROCESS_DETACH:
         // Perform any necessary cleanup.
			printf(”DLL_PROCESS_DETACH\n”);
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}


Questo files contiene il punto d’entrata della DLL (la funzione DllMain) e la dichiarazione del corpo di tutte le funzioni dichiarate nel file DllOggetti.h.

La funzione DllMain è chiamata in fase di caricamento della DLL. Per il parametro “ul_reason_for_call ” esistono 4 valori: DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, DLL_PROCESS_DETACH che stanno ad indicare rispettivamente l’evento di caricamento in memoria della DLL, la chiamata della DLL da un altro thread, il termine dell’uso della DLL da un thread e lo scaricamento dalla memoria della DLL.

Le printf all’interno dei vari case dello switch sono utili solo in fase di debugging a scopo educativo. Potete tranquillamente cancellarle in ambiente di produzione.

Passiamo al progetto utilizzatore


Progetto DLLOggettiTest

Questo progetto si aggancerà alla DLL denominata DLLOggetti facendo necessariamente riferimento al file header (per conoscere la struttura della class CProva richiamata successivamente), ed al file .dll della libreria stessa, che saranno requisiti necessari ed indispensabili per la compilazione del vostro programma.

I parametri necessari per utilizzare la libreria in modalità implicita sono:

  • il file header (.h) della libreria;
  • il file dll (.dll) della libreria;

DllOggettiTest.cpp

#include "stdafx.h"
#include "..\DllOggetti\DllOggetti.h"
#include 
#include 
 
typedef CProva* (__cdecl *MYPROC)();
typedef CProva* (__cdecl *MYPROC2)(int x,int y, int z); 
 
int _tmain(int argc, _TCHAR* argv[])
{
	CProva *p1, *p2;
	HINSTANCE hDll = LoadLibrary(_T(”DllOggetti.dll));
	if (hDll != NULL)
	{
		MYPROC procAddr = (MYPROC) GetProcAddress(hDll, “DLL_CProva”);
		MYPROC2 procAddr2 = (MYPROC2) GetProcAddress(hDll, “DLL_CProva_int_int_int”);
 
		p1 = (procAddr)();
		p2 = (procAddr2)(1,2,3);
 
		printf(”p1_x = %d\n”,p1->getX());
		printf(”p2_x = %d\n”,p2->getX());
 
		delete p1;
		delete p2;
 
		FreeLibrary(hDll);
		getch();
	}
	return 0;
}


Analizziamo questo semplice codice riga per riga.

Il primo #include, dopo sfdafx.h, serve per definire le funzioni e la struttura delle classi che verranno importate dalla DLL e che sono definite in DllOggetti.h. Vi ricordo che questo file header, questa volta, verrà utilizzato senza la direttiva DLLOGGETTI_EXPORTS e quindi le funzioni della DLL saranno in modalità importazione.

Dopodichè definiamo due tipi che saranno due puntatori di funzioni per richiamare i costruttori della Classe CProva. Infatti entrambi MYPROC e MYPROC2 restituiscono il tipo CProva (definito in DllOggetti.h, opportunamente richiamato con #include) però il primo non prevede parametri, mentre il secondo prevede tre parametri.

Entriamo nella funzione principale. Prima di tutto carichiamo la nostra libreria DllOggetti.dll con la funzione LoadLibrary, proprio come avevamo fatto nelle DLL di funzioni. Se tutto è andato a buon fine (hDll != NULL) ricaviamo gli indirizzi delle due funzioni, DLL_CProva e DLL_CProva_int_int_int.

A questo punto chiamiamo i due puntatori a funzione e assegnamo i risultati a p1 e p2. p1 è l’oggetto costruito da CProva senza parametri e p2 è l’oggetto costruito da CProva passando 3 parametri.

Non ci resta che visualizzare, come test, i valori del parametri X di ciascuno dei due oggetti, p1 e p2. I valori restituiti sono correttamente 10 e 1.

Non dimentichiamoci ora, in chiusura di programma, di rimuovere gli oggetti p1 e p2 dalla memoria per evitare memory leak.

Rilasciamo la libreria con FreeLibrary e mettiamo un getch() per poter leggere il debug del codice.

Se dopo aver letto questo articolo andrete a documentarvi su COM capirete quanto il funzionamento di quest’ultimo è a grandi linee simile a quanto descritto in questo articolo. Solo che COM dispone di un sistema di funzionamento ad interfacce, completamente diverso, più scalabile, più facilmente distribuibili (basti pensare che sono utilizzabili molto semplicente con molto linguaggi di programmazione, sicuramente con i più utilizzati) più pulito e meno macchinoso. Del resto le DLL, intese in senso stretto, sono più propriamente contenitori di funzioni e sono meno adatte a gestire oggetti nativamente.


Allegati