Tworzenie biblioteki DLL.
Czym jest biblioteka dll nie trzeba chyba nikomu wyjaśniać, ja traktuję je
głównie jako rozszerzenie możliwości aplikacji zawierające zbiór obiektów
i funkcji, których z różnych powodów nie chcę umieszczać wewnątrz samej
aplikacji. Zaletą takiej biblioteki jest to, że można zawarte w niej
obiekty i funkcje wykorzystywać wielokrotnie w różnych programach, bez
konieczności ponownego ich pisania, wystarczy po prostu dołączyć
bibliotekę do programu.
Utworzenie takiej biblioteki
wbrew pozorom jest bardzo proste, a tworzenie w niej obiektów i funkcji
przebiega dokładnie tak samo jak w aplikacji. Podłączenie biblioteki do
programu również jest banalnie proste.
Od czego zacząć?
Zaczniemy od utworzenie projektu biblioteki, w tym celu w menu File |
New wybieramy Other. Wyskoczy okno New Items, na
zakładce New wybieramy DLL Wizard i w oknie które wyskoczy
zaznaczamy C++ i Use VCL. W ten sposób zostanie utworzony
plik Unit1.cpp zawierający już pewne wpisy:
Plik biblioteki Unit1.cpp
//---------------------------------------------------------------------------
#include <vcl.h>
#include <windows.h>
#pragma hdrstop
//---------------------------------------------------------------------------
// Important note about DLL memory management when your DLL uses the
// static version of the RunTime Library:
//
// If your DLL exports any functions that pass String objects (or structs/
// classes containing nested Strings) as parameter or function results,
// you will need to add the library MEMMGR.LIB to both the DLL project and
// any other projects that use the DLL. You will also need to use MEMMGR.LIB
// if any other projects which use the DLL will be performing new or delete
// operations on any non-TObject-derived classes which are exported from the
// DLL. Adding MEMMGR.LIB to your project will change the DLL and its calling
// EXE's to use the BORLNDMM.DLL as their memory manager. In these cases,
// the file BORLNDMM.DLL should be deployed along with your DLL.
//
// To avoid using BORLNDMM.DLL, pass string information using "char *" or
// ShortString parameters.
//
// If your DLL uses the dynamic version of the RTL, you do not need to
// explicitly add MEMMGR.LIB as this will be done implicitly for you
//---------------------------------------------------------------------------
#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason,
void* lpReserved)
{
return 1;
}
//--------------------------------------------------------------------------- |
Jest to szkielet biblioteki i nie będziemy nic w nim
zmieniać, można co najwyżej usunąć komentarz. Po zapisaniu projektu
przechodzimy do menu Project | Options i na zakładce Linkier
odznaczamy Use dynamic RTL, ale pozostawiamy zaznaczoną opcję:
Generate import library, ta opcja jest potrzebna do tego, żeby program
automatycznie wygenerował plik *.lib, który będzie potrzebny przy
podłączaniu biblioteki do programu. Następnie przechodzimy na zakładkę
Packages i odznaczamy opcję: Build with runtime packages. Na
koniec przechodzimy na zakładkę Applications i w opcji LIB
version usuwamy wpis 1.0, to nam wiele uprości.
Szkielet biblioteki jest już gotowy do umieszczania w
nim tego co chcemy. Na początek pokażę jak umieścić w bibliotece okno,
żeby nie komplikować sprawy wybieramy w menu File | New | Form zostanie
utworzony formularz, zapisujemy go pod dowolną nazwą, różną oczywiście od
nazwy jaką nosi nasz plik biblioteki czyli inną niż Unit1. Jak widać okno
formularza jest tworzone dokładnie tak samo jak ma to miejsce w przypadku
tworzenie nowego okna w programie. Utworzona jednostkę należy włączyć do
pliku biblioteki w sekcji include (do pliku Unit1.cpp). Załóżmy, że
zapisaliśmy naszą nową jednostkę pod nazwami About.cpp i About.h, więc w
pliku Unit1.cpp będącego głównym plikiem biblioteki umieszczamy
następujący wpis:
Plik biblioteki Unit1.cpp
//-------------------------------- #include <vcl.h>
#include <windows.h>
#include "about.h"
#pragma hdrstop //-------------------------------- |
Utworzona nowo jednostka z oknem będzie w naszym
projekcie wyświetlać informacje o programie, czy też o samej bibliotece,
więc kształtujemy to okno wedle własnego uznania, to jest w tej chwili bez
znaczenia, ale umieśćmy może na tym oknie obiekt Label1, w którym będziemy
umieszczać jakiś tekst, ten tekst zostanie przekazany do biblioteki z
programu i wyświetlony właśnie na tej etykiecie. Gdy już sobie
przygotujemy okienko O Programie, z etykietą Label1 na której pojawi się
nazwa programu, należy w pliku Unit1.cpp utworzyć funkcję, która będzie
eksportowała zawartość biblioteki do programu, na początek utworzymy
deklarację tejże funkcji:
Plik biblioteki Unit1.cpp
//--------------------------------
#include <vcl.h>
#include <windows.h>
#include "about.h"
#pragma hdrstop
//--------------------------------
#pragma argsused
extern
"C"
__declspec(dllexport)
void
About(AnsiString pName); |
Jak widać funkcję nazwałem About i nic ona nie
zwraca, ale pobiera jeden argument typu AnsiString, który to będzie
zawierał nazwę programu. Oczywiście można tutaj przekazać całą masę
różnych argumentów. Po utworzeniu deklaracji funkcji eksportującej należy
utworzyć jej definicję również w pliku głównym biblioteki:
|
Plik
biblioteki Unit1.cpp
//-------------------------------- #include <vcl.h>
#include <windows.h>
#include "about.h"
#pragma hdrstop
//--------------------------------
#pragma argsused
extern
"C"
__declspec(dllexport)
void About(AnsiString pName);
//--------------------------------
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason,
void* lpReserved)
{
return 1;
}
//--------------------------------
|
void About(AnsiString pName)
{
TForm2 *AboutForm = new TForm2(NULL);
AboutForm->Label1->Caption = pName;
AboutForm->ShowModal();
delete AboutForm;
} |
//-------------------------------- |
Wewnątrz funkcji umieściłem kod wywołujący okno, ale
okno to jest tworzone w sposób dynamiczny, w oparciu o okno programu,
czyli formularz nosi nazwę Form2, więc wywołujemy go poprzez dynamiczne
utworzenie nowego okna w oparciu o klasę TForm2. Nazwa programu zawarta w
zmiennej pName również jest przekazywana do dynamicznie utworzonego okna.
W ten sposób należy wywoływać okna z biblioteki.
Mamy gotową bibliotekę z jednym oknem programu i jedną funkcją wywołującą
to okno, teraz należy to skompilować jednak nie za pomocą Run lecz
Build, ponieważ Run próbuje uruchomić kompilowany program, a
biblioteka programem nie jest i po takim skompilowaniu wyskoczy komunikat
z błędem, aczkolwiek biblioteka zostanie utworzona. Po skompilowaniu
przechodzimy do katalogu z naszym projektem i wśród licznych plików
wyszukujemy dwa, pierwszy to Project1.dll (występuje pod nazwą jaką mu
nadano, ale z rozszerzeniem *.dll), a drugi to Project1.lib. Te dwa
pliki należy przekopiować do katalogu, w którym utworzymy za chwilę
program testujący naszą bibliotekę.
Po przekopiowaniu tychże plików do wspomnianego katalogu, tworzymy nowy
projekt, tym razem aplikacji, zapisujemy go w katalogu do którego
przekopiowaliśmy pliki biblioteki, a następnie w menu Project | Add to
project włączamy do naszego projektu aplikacji plik naszej biblioteki
np. Project1.lib, ale UWAGA! Niezależnie od tego
czy tworzymy projekt aplikacji, czy też projekt biblioteki zawsze tworzony
jest plik *.lib i nosi on zawsze taką samą nazwę jak projekt.
Jeżeli więc projekt naszej biblioteki nosi nazwę Project1 to projekt
aplikacji testowej musi nosić inną nazwę np. Project2.
Po włączeniu pliku *.lib biblioteki do programu, umieszczamy w pliku
źródłowym (np. Unit1.cpp) deklarację funkcji z biblioteki, czyli:
Plik aplikacji Unit1.cpp
//-------------------------------- #include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
extern
"C"
__declspec(dllexport)
void About(AnsiString pName); //-------------------------------- |
Jak widać deklaracja funkcji jest dokładnie taka sama
jak ta umieszczona w pliku Unit1.cpp biblioteki. Od tej chwili można już
używać biblioteki. Okno O Programie umieszczone w bibliotece wywołamy z
poziomu programu w zdarzeniu OnClose dla przycisku, a wywołanie tego okna
polega na odwołaniu się do zadeklarowanej funkcji i przekazaniu jej
parametrów, których ona żąda, czyli w przykładzie jest to nazwa programu:
Plik aplikacji Unit1.cpp
//-------------------------------- void __fastcall
TForm1::Button1Click(TObject *Sender)
{
About("MyProgram");
} //-------------------------------- |
No i to wszystko, jeśli nie popełniliśmy błędu to
powinno zadziałać.
Na kolejnym przykładzie pokaże jak wywołać okno dialogowe typu TOpenDialog,
bez tworzenia jakiegokolwiek okna formularza.
Wracamy do projektu biblioteki i umieszczamy w niej deklarację funkcji
AddFile, jednak w odróżnieniu od poprzedniej ta funkcja nie będzie
pobierała żadnych argumentów, ale będzie zwracała wartość typu AnsiString:
Plik biblioteki Unit1.cpp
//-------------------------------- #include <vcl.h>
#include <windows.h>
#include "about.h"
#pragma hdrstop
//--------------------------------
#pragma argsused
extern
"C" __declspec(dllexport) void About(AnsiString pName);
extern
"C"
__declspec(dllexport)
AnsiString AddFile(); //-------------------------------- |
Następnie tworzymy definicję tejże funkcji, przy okazji
tworząc dynamicznie obiekt typu TOpenDialog, oczywiście wewnątrz tej
funkcji umieszczamy kod odpowiedzialny za wywołanie tegoż okienka
dialogowego z pobraniem nazwy pliku wybranego w nim:
Plik biblioteki Unit1.cpp - przykład 1
//-------------------------------- AnsiString AddFile()
{
TOpenDialog *OpenDialog1 = new TOpenDialog(NULL);
OpenDialog1->Filter = "Wszystkie
pliki (*.*)|*.*";
if(OpenDialog1->Execute())
return OpenDialog1->FileName;
delete OpenDialog1;
return "";
} //-------------------------------- |
Jak widać okienko OpenDialog1 jest usuwane z pamięci
gdy przestaje być potrzebne i tak jak to ja tutaj zrobiłem jest
prawidłowo, gdybyśmy jednak zawartość FileName obiektu OpenDialog1 chcieli
przekazać do zmiennej i zwrócić zawartość tej zmiennej:
Plik biblioteki Unit1.cpp - przykład 2
//-------------------------------- AnsiString AddFile()
{
TOpenDialog *OpenDialog1 = new TOpenDialog(NULL);
OpenDialog1->Filter = "Wszystkie
pliki (*.*)|*.*";
String pFileName;
if(OpenDialog1->Execute())
pFileName
= OpenDialog1->FileName;
delete OpenDialog1;
return
pFileName;
} //-------------------------------- |
Taki kod skompiluje się bez żadnych ostrzeżeń, ponieważ
jest jak najbardziej poprawny, jeżeli wywołamy potem tą funkcję w
programie, to również wszystko przebiegnie bez problemu, jeżeli jednak po
wywołaniu tej funkcji spróbujemy wywołać funkcję About z tejże biblioteki
to wyskoczy komunikat o błędzie, żeby uniknąć komunikatu o błędzie w
przypadku tego drugiego kodu, nie należy kasować obiektu OpenDialog1,
dlatego polecam kod z przykładu pierwszego.
Kompilujemy bibliotekę poprzez Build i kopiujemy pliki *.dll i *.lib z
katalogu, z projektem biblioteki do katalogu z projektem aplikacji
testowej. Uruchamiamy projekt aplikacji testującej bibliotekę. Tym razem
nie musimy już włączać do projektu pliku *.lib biblioteki, ponieważ został
on już włączony wcześniej, należy tylko pamiętać o zastąpieniu starych
plików *.dll i *.lib nowymi plikami z biblioteki. W pliku źródłowym
aplikacji umieszczamy deklarację nowej funkcji z biblioteki:
Plik aplikacji Unit1.cpp
//--------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
extern "C"
__declspec(dllexport) void About(AnsiString pName);
extern
"C"
__declspec(dllexport)
AnsiString AddFile(); |
Teraz pozostaje już tylko wywołanie tej funkcji:
Plik aplikacji Unit1.cpp
//-------------------------------- void __fastcall
TForm1::Button1Click(TObject *Sender)
{
Edit1->Text = AddFile();
} //-------------------------------- |
Pliki źródłowe, archiwum RAR,
rozmiar: 949 kb
Opracował: Cyfrowy Baron
DYNAMICZNE PODŁĄCZANIE BIBLIOTEKI DLL.
W artykule Tworzenie biblioteki DLL został opisany sposób statycznego
podłączania biblioteki DLL, czyli biblioteka jest ładowana do pamięci
razem z programem w podczas jego uruchamiania. Taki sposób dołączania
biblioteki jest mało efektywny ponieważ do pamięci zostają załadowane
wszystkie obiekty i funkcje z biblioteki, a przecież nie muszą być w danej
chwili potrzebne, więc tylko niepotrzebnie zajmują pamięć. Tworząc
programy cały czas rezydujące w pamięci, lepiej jest ładować bibliotekę
dopiero gdy jest potrzebna i usuwać ją z pamięci gdy staje się zbędna.
Realizacja tegoż zadania jest bardzo prosta, a sam sposób tworzenia
biblioteki się nie zmienia, dlatego w tym artykule wykorzystam bibliotekę
stworzoną w poprzednim. Przypomnę tylko, że ta biblioteka zawiera dwie
funkcje:
extern
"C" __declspec(dllexport) void About(AnsiString pName);
extern "C" __declspec(dllexport)
AnsiString AddFile(); |
Podłączając dynamicznie bibliotekę do programu nie
potrzebujemy już pliku *.LIB tej biblioteki, w zasadzie program nie wie
czy może skorzystać z funkcji zawartej w bibliotece dopóki nie spróbuje
jej wywołać, co ciekawe jeśli nie dołączymy do programu wymaganej
biblioteki, a program będzie chciał skorzystać z jakiejś funkcji w niej
zawartej, to nie pojawi się komunikat o błędzie, a funkcja nie zostanie
wywołana, oczywiście zawsze można dołączyć jakoś komunikat informujący o
braku biblioteki, lub wywołać inną funkcję, w przypadku stwierdzenia
braku biblioteki.
W celu dynamicznego podłączenia biblioteki do programu należy
skorzystać z funkcji LoadLibrary, następnie trzeba zadeklarować
wskaźnik do funkcji, no a potem trzeba go połączyć z funkcją z
biblioteki, na koniec jeśli funkcja istnieje wystarczy ją wywołać, a po
wszystkim wystarczy zwolnic bibliotekę, a oto przykład podłączenia i
wywołania funkcji About:
Plik aplikacji Unit1.cpp
//--------------------------------
extern "C" __declspec(dllimport)
void About(AnsiString
pName);
//-------------------------------- void CallAbout(AnsiString
pName)
{
HANDLE DLLHandle = LoadLibrary("Library_name.dll");
// Należy podać ścieżkę dostępu do biblioteki, może
być względna lub bezwzględna, nie musi znajdować się w tym samym katalogu co
program
if(DLLHandle != NULL) // jeśli biblioteka
istnieje zostanie wykonane odpowiednie działanie.
{
typedef (*aAbout)(AnsiString); //
Deklaracja wskaźnika do funkcji.
/*
1. Jeśli funkcja, którą wywołujemy jest typu woid, czyli nie zwraca
żadnej wartości, jak w przykładzie
void About(AnsiString pName) to deklarację wskaźnika do tej funkcji
poprzedzamy tylko słowem typedef,
jeśli jest to funkcja zwracająca np. wartość typu int to również
wystarczy tylko typedef, jeśli jednak
mamy taką funkcję AnsiString AddFile()to po słowie typedef należy
podać typ zwracanej przez funkcję
wartości, czyli np: typedef AnsiString(aAddFile)();
2. W nawiasie występującym po deklaracji funkcji podajemy typ
pobieranych przez funkcję argumentów,
czyli jak w przykładzie void About(AnsiString pName) podana zostaje
wartość AnsiString:
typedef (*aAbout)(AnsiString).
*/
aAbout About = (aAbout)GetProcAddress(DLLHandle,
"_About");
if(About != NULL)
// jeśli funkcja istnieje to jest wywoływana.
About(pName);
}
else // jeśli nie znaleziono biblioteki.
{
ShowMessage("Nie znaleziono
wymaganej biblioteki 'Library_name.dll'");
}
FreeLibrary(DLLHandle); // usunięcie biblioteki z
pamięci, gdy jest już zbędna.
} //--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
CallAbout("Jakiś tekst");
// wywołanie funkcji About z biblioteki poprzez
odwołanie się do funkcji CallAbout.
}
//-------------------------------- |
Jak to widać w przykładzie do funkcji About z
biblioteki odwołałem się poprzez dodatkową funkcję, którą nazwałem
CallAbout, wewnątrz tej funkcji został stworzony wskaźnik do
funkcji właściwej (About) i nazwa tego wskaźnika musi być różna od nazwy
funkcji. Funkcja GetProcAddress pobiera dwa argumenty, pierwszy to
odwołanie do biblioteki, drugi to nazwa procesu do którego się
odwołujemy, nazwa procesu jest taka sama jak nazwa wywoływanej funkcji,
z tą różnicą, że jest poprzedzona znakiem _.
Tworzenie funkcji CallAbout nie jest konieczne, ponieważ można by
umieścić cały kod w zdarzeniu OnClick dla przycisku, jednak w ten sposób
kod jest bardziej czytelny.
Na zakończenie jeszcze przykład podłączenia funkcji AddFile:
Plik aplikacji Unit1.cpp
//--------------------------------
extern
"C" __declspec(dllimport)
AnsiString
AddFile();
//--------------------------------
AnsiString __fastcall
CallAddFile()
{
AnsiString result = "";
HANDLE DLLHandle = LoadLibrary("Library_name.dll");
if(DLLHandle != NULL)
{
typedef AnsiString(*aAddFile)();
aAddFile AddFile = (aDodajPlik)GetProcAddress(DLLHandle,
"_AddFile");
if(AddFile != NULL)
result = AddFile();
FreeLibrary(DLLHandle);
}
return result;
} //--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Edit1->Text = CallAddFile();
}
//-------------------------------- |
Opracował: Cyfrowy Baron
|