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