DLL Programmierung - Anfängerfragen

Rund um die LCL und andere Komponenten
Antworten
hubblec4
Beiträge: 341
Registriert: Sa 25. Jan 2014, 17:50

DLL Programmierung - Anfängerfragen

Beitrag von hubblec4 »

Hallo

Ich habe leider keinerlei praktische Erfahrung auf diesem Gebiet, dennoch habe ich viel im Netz gesucht und gelesen.
Bevor ich mich aber in die Arbeit stürtze wollte ich ein paar Sachen im Vorfeld erfragen um zu sehen ob sich das alles für mich lohnt.

Ich möchte mit einer Online Datenbank kommunizieren.
Es werden später mal 3 separate programme mit der Datenbank interagieren und dafür wollte ich den Code nicht dreimal "schreiben/kopieren".

Könnte ich in so eine DLL, welche auch unter Linux laufen soll (und dort eine so-Datei ist) alles reinpacken was ich für die SQL Datenbank brauche?
Also angefangen davon das ich dort Klassen ablege auf die ich auch zugreifen kann?

Mcih verwirren auch ein bisschen diese "Aufrufkonventionen": Was bedeutet das für die Kommunikation meines Programms mit der DLL in bezug auf die übergabe von Parametern. Ich denke ich irre mich wenn ich da nur Zahlen(Int) und Strings(PChar) übermitteln könnte?

So mal ins blaue: Ich würde gern die DLL mit SQL-Befehlen füttern und zurück bekomme ich die Query-Daten um sie in der GUI anzuzeigen.

Mich interessiert das Thema auch so an sich, da mein Programm auch immer größer wird und ich über das Auslagern von Quell-Code nachdenken sollte.

hubble

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: DLL Programmierung - Anfängerfragen

Beitrag von Warf »

Werden alle 3 Programme in Lazarus geschrieben? Falls ja dann verwende Units statt DLL's. Jede Möglichkeit DLL's aus dem Weg zu gehen ist die bessere Möglichkeit.

Ansonsten hier ist ein Auszug aus meinem Pascal Tutorial über Programmbibliotheken:

Programmbibliotheken

Mit Units können wir Pascal Quelltext Bibliotheken erstellen. Eine weitere Form der Bibliotheken sind die Programmbibliotheken (engl. shared library). Programmbibliotheken sind Sammlungen von Funktionen in kompilierter Form. Programmbibliotheken ermöglichen das bereitstellen von Programmcode Programmiersprachenunabhängig. Das heißt einfach, da diese Bibliotheken kompiliert sind, sind sie wie Programme von Betriebsystem verwaltet, und andere Programme könne diese einfach Aufrufen, dann wird der Programmcode der Bibliothek ausgeführt. Im gegensatz zu Programmen sind Programmbibliotheken keine eigenständigen Programme und benötigen ein aufrufendes Programm welche sie aufruft.

Außerdem da Programmbibliotheken Kompiliert sind bieten sie die Möglichkeit Funktionen bereitzustellen, ohne dass der nutzer einblick in diesen erhält, oder darin was verändern kann.

Typische Programmbibliotheken sind System API's, wie die Windows API. Diese API's (Application Programming Interface) bieten über Bibliotheken die möglichkeit Systemfunktionen aufzurufen.

Programmbibliotheken sind je nach Betriebsystem unterschiedlich. Außerdem gibt es Statische Bibliothek und Dynamische Bibliotheken.
Statische und Dynamische Bibliotheken werden durch die Dateiendung unterschieden:
Windows: Statisch .lib Dynamisch .dll
Linux: Statisch .a Dynamisch .so
Mac OS X: Statisch .a Dynamisch .dylib

Dynamische Bibliotheken werden nicht in die Executeable kompiliert, und müssen separat mitgeliefert werden als so genannte Abhängigkeiten (engl. dependency). Bei Statischen Bibliotheken fällt dies weg. Solche Dateien sind euch daher vielleicht schon mal bei anderen Programmen aufgefallen.

Funktionen aus statische Bibliotheken werden von dem Linker während des Kompilierens im Programm verwiesen, sodass das Programm diese beim Ausführen über diese verweise aufgerufen.
Dynamische Bibliotheken können das selbe aber bieten auch die Möglichkeit während der Laufzeit Funktionen aus diesem Bibliotheken mit dem Funktionsnamen über Funktionszeiger aufzurufen. Damit lässt sich zum Beispiel ein Plugin System schreiben, welches alle Bibliotheken in einem Ordner läd und eine Funktion LoadPlugin aufruft.

Bei der Nutzung von Bibliotheken kümmert sich das System um das laden der Bibliotheken. Dabei suchen die Systeme in verschiedenen Ordnern nach den gesuchten Bibliotheken. Zunächst wird von den Systemen immer im Aktuellen Programmverzeichnis nach der Bibliothek gesucht. Wird sie dort nicht gefunden wird in den System Bibliotheksverzeichnis gesucht (Windows: Windows\System32, Linux/Unix: /lib/ und /usr/lib/). Wenn dort die Bibliothek nicht gefunden wird kennt das System noch Pfadvariablen (Windows PATH, Unix: /etc/ld.so.conf) welche weitere Suchpfade angeben können.

Außerdem kann auch der absolute Pfad zu der Bibliotheksdatei angegeben werden.

Kommen wir zur Nutzung in Pascal.

Um Bibliotheken zu verwenden benötigen wir Schnittstellen für diese Bibliothek in einer Unit. Bei größeren Bibliothek mit vielen Funktionen empfiehlt es sich eine Unit pro Bibliothek zu schreiben, welche dann eine Schnittstelle zu allen Funktionen dieser Bibliothek darstellt. Diese Units nennt man Wrapper.

Befassen wir uns zunächst mal mit dem Statischen Linken.
Um dem Linker zu sagen er soll eine bestimmte Bibliothek gegenlinken kann man die Compileranweisungen {$LinkLib Lib} global in einer Datei verwenden, wobei der Linker dann nach der Bibliothek Lib sucht und wenn Funktionen aus dieser angefordert werden diese gegenlinkt. Als Beispiel nehmen wir die Funktion strlen aus der Programmiersprache C:

Code: Alles auswählen

unit libtest;
 
{$MODE ObjFpc}{$H+}
 
interface
{$LINKLIB c}
 
function strlen(P: pchar): longint; cdecl; external;
 
implementation
 
end.


Der Linklib Compilerschalter sagt dem Linker er soll die C Bibliothek gegenlinken. Um Pascal nun zu sagen das die Funktion strlen exsistiert müssen wir dafür einen Funktionskopf angeben, welcher dem Funktionskopf der Bibliothek entspricht. Durch die cdecl Direktive sagen bestimmen wir für die Funktion die C Aufrufkonvention, da diese Funktion aus der C Lib stammt, und daher diese Aufrufkonvention verwendet. Das Schlüsselwort external sagt dem Linker er soll in den Bibliotheksdateien nach der Funktion strlen suchen und diese mit diesem Kopf verknüpfen. Da dieser Funktion im Interface steht ist sie damit auch für alle anderen Dateien zugänglich.

Damit lassen sich sowohl Dynamische als auch Statische Bibliotheken nutzen.

Funktionen aus dynamische Bibliotheken können mit einer Angabe bei der Funktionsdefinition verknüpft werden.

Code: Alles auswählen

unit libtest;
{$MODE ObjFpc}{$H+}
 
interface
 
const LibName = 'c';
 
function strlen(P: pchar): longint; cdecl; external 'c';
function GetStrLen(P: pchar): longint; cdecl; external LibName name 'strlen';
 
implementation
 
end.


Nach dem external über eine String Konstante, in diesem Fall 'c', kann angegeben werden in welcher Bibliothek die Funktion gesucht werden soll. Wird noch ein "name Stringkonstante" angegeben, so wird festgelegt wie die Funktion in der Bibliothek heißt, das ist zum Beispiel möglich um die Funktion in Pascal anders zu benennen als in der Bibliothek. Würde man diese Unit so in einem Programm einbinden, so würde sowohl strlen als auch GetStrLen die selbe Funktion aus der C Bibliothek aufrufen.

Bemerkung: Name kann man auch mit der Linklib Methode verwenden.


Zuletzt gibt es noch die Möglichkeit Bibliotheken dynamisch in der Laufzeit zu laden, dafür stellt die RTL die Unit dynlibs zur Verfügung.

Code: Alles auswählen

uses
  dynlibs;
 
type
  TMyProc = procedure(Argument: Integer); // Funktionszeiger
 
var
  Lib: TLibHandle;
  func: TMyProc;
 
begin
  Lib := LoadLibrary('MyLib.' + SharedSuffix); // Bibliothek laden
  if Lib = NilHandle then Exit; // Fehler beim laden -> Ende
  try
    func := TMyProc(GetProcedureAddress(Lib, 'Funktionsname'));
    if Assigned(func) then // Funkiton wurde gefunden
      func(12); // Funktion aufrufen
  finally
    UnloadLibrary(Lib); // Freigeben
  end;
end.


Mit der Funktion LoadLibrary können wir eine Bibliothek laden, diese Funktion gibt uns dann ein Handle zurück, eine art Zeiger für das Betriebsystem, damit das System weis um welche Bibliothek es sich handelt. SharedSuffix ist eine Konstante die je nach System die Dateiendung von Programmbibliotheken zurückgibt (Windows: 'dll'). Wird NilHandle (0) zurückgegeben so ist ein Fehler aufgetreten und die Bibliothek konnte nicht geladen werden. Danach öffnen wir einen Try-Finally Block, um sicher zu gehen, dass die Bibliothek auch Freigegeben wird.
Über GetProcedureAddress bekommen wir dann einen Funktionszeiger als untypisierten Zeiger übergeben. Um diesen ausführen zu können müssen wir ihn in einen Funktionszeiger der Bibliotheksfunktion Casten. In diesem Fall wäre das eine Funktion ohne Rückgabewert die einen Integer als Parameter an nimmt.
Das Freigeben der Bibliothek erfolgt am Ende über UnloadLibrary.

Unter Unix/Linux: Benutzt man Programmbibliotheken die nicht in Pascal geschrieben wurden welche Dynamischen Speicher (Zeiger, Strings, Dyn Arrays) übergeben der in der Bibliothek als auch im Programm verwaltet (Freigeben oder Alloziieren) wird, so muss als erste Unit in der Uses Klausel der Program Datei die Unit cmem eingebunden werden, damit diese im Initialization und Finalization Teil den Memory Manager umstellen kann.


Bibliotheken schreiben

Nun nachdem wir gelernt haben wie Programmbibliotheken verwendet werden befassen wir uns mit dem schreiben solcher. FreePascal unterstützt nur das schreiben Dynamischer Bibliotheken, bei Statischen kann man die Object Files welche beim Kompilieren erzeugt werden selbst gegenlinken und als Statische Bibliothek verwenden.

Dafür gibt es einen weiteren Typ von Pascal Quelltextdateien, die Library Datei.
Vom Aufbau ist dieser Dateityp sehr ähnlich zu dem Program Datentyp:

Code: Alles auswählen

library Name;
 
{ Compilerschalter }
 
{ Uses }
 
{ Typen }
{ Variablen }
{ Konstanten }
{ Funktionen }
 
exports // optional
{ Funktionsnamen }
 
begin // Optional
{ Quelltext }
end.


Das Schlüsselwort für diese Dateien ist library, sonst unterscheiden sich Program und Library Dateien bei der exports Sektion. In dieser Sektion listet man alle Funktionsnamen mit Komma getrennt auf die durch die Programmbibliothek bereitgestellt werden sollen.
Wie man sieht haben Bibliotheken auch eine ausführbaren Teil wie Programme, einen begin Block. Dieser wird beim Laden der Bibliothek ausgeführt. Sowohl die exports als auch begin Sektion sind optional, das heißt es ist möglich Bibliotheken zu schreiben die keine Funktionen bereitstellen, sondern nur Programmcode ausführen, aber auch möglich Bibliotheken zu schreiben welche keinen Programmcode ausführen. Außerdem ist es möglich Bibliotheken zu schreiben welche weder Programmcode ausführen noch Funktionen bereitstellen, wie sinnreich das ist dürft ihr selbst beurteilen.

Bemerkung: Der Initialization Teil einer der Units die von Bibliotheken verwendet werden wird beim Laden der Bibliothek ausgeführt und der Finalization Teil bei dem Freigeben.


Um nocheinmal unser Beispiel mit der Verketteten Liste zu verwenden schreiben wir nun für diese Liste eine eigene Programmbibliothek in der Datei linkedlist.pas:

Code: Alles auswählen

library LinkedList;
 
{$MODE OBJFPC}{$H+}
 
 
type
  PListItem = ^TListItem;
 
  TListItem = record
    Value: integer;
    NextItem: PListItem;
  end;
 
function GetListLength(List: PListItem): integer;
begin
  if Assigned(List) then
    Result := 1 + GetListLength(List^.NextItem)
  else
    Result := 0;
end;
 
procedure AddElementAtStart(var List: PListItem; x: integer);
var
  tmp: PListItem;
begin
  new(tmp);
  tmp^.Value := x;
  tmp^.NextItem := List;
  List := tmp;
end;
 
procedure AddElementAtEnd(var List: PListItem; x: integer);
begin
  if Assigned(List) then
    AddElementAtEnd(List^.NextItem, x)
  else
  begin
    new(List);
    List^.Value := x;
    List^.NextItem := nil;
  end;
end;
 
procedure DeleteListItem(List: PListItem; i: integer);
var
  tmp: PListItem;
begin
  if not Assigned(List) then
    exit; // List Item nicht vorhanden
  if i > 1 then
    DeleteListItem(List^.NextItem, i - 1)
  else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert
  begin
    tmp := List^.NextItem^.NextItem; //Übernächstes Item
    dispose(List^.NextItem);
    List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item
  end;
end;
 
function GetItemValue(List: PListItem; i: integer): integer;
begin
  Result := 0;
  if Assigned(List) then
    if i > 0 then
      Result := GetItemValue(List^.NextItem, i - 1)
    else
      Result := List^.Value;
end;
 
procedure SetItemValue(List: PListItem; i, x: integer);
begin
  if Assigned(List) then
    if i > 0 then
      SetItemValue(List^.NextItem, i - 1, x)
    else
      List^.Value := x;
end;
 
procedure ClearList(var List: PListItem);
begin
  if Assigned(List) then
  begin
    ClearList(List^.NextItem);
    dispose(List);
    List := nil;
  end;
end;
 
exports
GetListLength,
AddElementAtStart,
AddElementAtEnd,
DeleteListItem,
GetItemValue,
SetItemValue,
ClearList;
 
end.


Diese kompilieren wir dann ganz normal über

Code: Alles auswählen

fpc linkedlist.pas

Damit sollte der FPC eine Dynamisch Programmbibliothek erzeugen, nach den Nameskonventionen des betreffenden Betriebsystems (Unter Unix ist wird vor den Dateinamen ein lib gehängt).

Dann benötigen wir eine Wrapper Unit llist.pas für diese Bibliothek:

Code: Alles auswählen

unit llist;
 
{$MODE OBJFPC}{$H+}
 
interface
type
  PListItem = ^TListItem;
 
  TListItem = record
    Value: integer;
    NextItem: PListItem;
  end;
 
const
{$ifdef win32}
  lllib = 'linkedlist.dll';
{$else}
  {$ifdef darwin}
    lllib = 'liblinkedlist.dylib';
    {$linklib liblinkedlist.dylib}
  {$else}
    lllib = 'liblinkedlist.so';
  {$endif}
{$endif}
 
function GetListLength(List: PListItem): integer; external lllib;
procedure AddElementAtStart(var List: PListItem; x: integer); external lllib;
procedure AddElementAtEnd(var List: PListItem; x: integer); external lllib;
procedure DeleteListItem(List: PListItem; i: integer); external lllib;
function GetItemValue(List: PListItem; i: integer): integer; external lllib;
procedure SetItemValue(List: PListItem; i, x: integer); external lllib;
procedure ClearList(var List: PListItem); external lllib;
 
implementation
 
end.


Über die IFDEF Compileranweisungen befüllen wir je nach Betriebsystem die Konstante lllib mit dem Namen der Bibliothek. Bei Mac OSX (Darwin) muss außerdem noch eine $Linklib Compileranweisung gesetzt werden.

Dann können wir diese Wrapperunit ganz normal verwenden:

Code: Alles auswählen

program LinkedListTest;
{$MODE OBJFPC}{$H+}
 
uses
  llist;
 
var root: PListItem;
    i: Integer;
begin
  root:=nil; // Initialisierung leere liste
  AddElementAtStart(root, 10);
  AddElementAtEnd(root, 5);
  AddElementAtStart(root, 3);
  AddElementAtStart(root, 8);
  AddElementAtEnd(root, 15);
  for i:=0 to GetListLength(root) -1 do
    SetItemValue(root, i, GetItemValue(root, i)+2);
  for i:=GetListLength(root)-1 downto 0 do
    WriteLn(GetItemValue(root, i));
  ClearList(root);
end.



Konventionen für Programmbibliotheken

Programmbibliotheken bieten die Möglichkeit Funktionen bereitzustellen. Da sich jede Programmiersprache und auch einige Compiler bei Align von Records oder den Standardaufrufkonventionen von Funktionen unterscheiden muss man sich natürlich beim bereitstellen einer Programmbibliothek entsprechend richten.
FreePascal benutzt z.B. standardmäßig die Pascal Aufrufkonvention und ein eigenes Align für Recordelemente. Um nun die Bibliothek allgemein zugänglich zu machen ist dies nicht die beste Wahl. Den allgemeinen Standard bietet dabei die Programmiersprache C, da diese sehr weit Verbreitet ist, und der Standard durch die ISO wohldefiniert und einfach zugänglich ist.

Konkret bedeutet das:
Verwendung von Packed Records, oder C Record Align
Funktionsaufrufe mit cdecl Aufrufkonvention
Unter Unix/Linux: C Memory Management für Zeiger und Dynamischen Speicher (Strings und Arrays)


Um die Records anzupassen ist die einfachste Möglichkeit einfach Packed Records zu verwenden, diese sind zwar langsamer und umständlicher für das System als Records mit Alignment, aber werden von jeder Sprache gleich unterstützt und genutzt.
Wenn man nun aber nicht mit Packed Records arbeiten möchte tut es auch eine Zeile:

Code: Alles auswählen

{$PackRecords C}

am Anfang jeder Datei in der Records Definiert werden, um das C Alignment zu verwenden.


Die Aufrufkonvention für Funkitonen kann man im Funktionskopf definieren:

Code: Alles auswählen

procedure Foo(Arg: Typ); cdecl;

würde eine Funktion mit der C Aufrufkonvention definieren.
Unter Windows wird auch sehr oft, z.B. in der Windows System API, die stdcall Aufrufkonvention verwendet:

Code: Alles auswählen

procedure Foo(Arg: Typ); stdcall;


Unter Unix/Linux:
Um das Memory Management umzustellen gibt es die Unit cmem, welche in ihrem Initialization und Finalization Teil darum kümmert. Diese muss als erste Unit der Bibliothek über Uses eingebunden werden:

Code: Alles auswählen

library Name;
 
{ Compilerschalter }
 
uses
  cmem, //Weitere Units
 
...

Den Memory Manager umzustellen ist unerlässlig wenn man Dynamische Datentypen wie Zeiger, Strings oder Dynamische Arrays sowohl in der Bibliothek und einer Anwendung verwaltet wird. Aber es kommt sehr selten vor das Dynamischer Speicher sowohl von Bibliothek als auch Programm verwaltet werden muss.

Bemerkung: Während das Record Align und das Memory Management wichtig sind, um die Funktionalität bei der Nutzung durch andere Sprachen und Compiler zu gewährleisten, ist die Wahl der Aufrufkonvention (Pascal, C, StdCall) jedem Entwickler selbst überlassen, solange sie sauber dokumentiert ist.

Außerdem gibt es bei Mac OSX (Darwin) noch ein paar kleinere eigenheiten was das Dynamische Laden über LoadLibrary angeht. Über GetProcedureAddress(Lib, Name) können nur Funktionen geladen werden welche mit einem '_' vor dem Namen beginnen, das heißt beim Publizieren einer .dylib für OSX welche Dynamisch geladen werden soll muss in der Bibliothek jede Funktion noch ein _ vor den Namen bekommen:

Code: Alles auswählen

library Example;
 
{$ifdef darwin}
  procedure _Foo;
{$else}
  procedure Foo;
{$endif}
begin
...
end;
 
exports
  {$ifdef darwin}
  _Foo;
{$else}
  Foo;
{$endif}
 
end.

Bei Statischer Nutzung ist dies nicht notwendig.

Unsere Bibliothek würde mit diesen Konventionen dann so aussehen:

Code: Alles auswählen

library LinkedList;
 
{$MODE OBJFPC}{$H+}
{$PackRecords C}
 
type
  PListItem = ^TListItem;
 
  TListItem = record
    Value: integer;
    NextItem: PListItem;
  end;
 
function GetListLength(List: PListItem): integer; cdecl;
begin
  if Assigned(List) then
    Result := 1 + GetListLength(List^.NextItem)
  else
    Result := 0;
end;
 
procedure AddElementAtStart(var List: PListItem; x: integer); cdecl;
var
  tmp: PListItem;
begin
  new(tmp);
  tmp^.Value := x;
  tmp^.NextItem := List;
  List := tmp;
end;
 
procedure AddElementAtEnd(var List: PListItem; x: integer); cdecl;
begin
  if Assigned(List) then
    AddElementAtEnd(List^.NextItem, x)
  else
  begin
    new(List);
    List^.Value := x;
    List^.NextItem := nil;
  end;
end;
 
procedure DeleteListItem(List: PListItem; i: integer); cdecl;
var
  tmp: PListItem;
begin
  if not Assigned(List) then
    exit; // List Item nicht vorhanden
  if i > 1 then
    DeleteListItem(List^.NextItem, i - 1)
  else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert
  begin
    tmp := List^.NextItem^.NextItem; //Übernächstes Item
    dispose(List^.NextItem);
    List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item
  end;
end;
 
function GetItemValue(List: PListItem; i: integer): integer; cdecl;
begin
  Result := 0;
  if Assigned(List) then
    if i > 0 then
      Result := GetItemValue(List^.NextItem, i - 1)
    else
      Result := List^.Value;
end;
 
procedure SetItemValue(List: PListItem; i, x: integer); cdecl;
begin
  if Assigned(List) then
    if i > 0 then
      SetItemValue(List^.NextItem, i - 1, x)
    else
      List^.Value := x;
end;
 
procedure ClearList(var List: PListItem); cdecl;
begin
  if Assigned(List) then
  begin
    ClearList(List^.NextItem);
    dispose(List);
    List := nil;
  end;
end;
 
exports
GetListLength,
AddElementAtStart,
AddElementAtEnd,
DeleteListItem,
GetItemValue,
SetItemValue,
ClearList;
 
end.


Und der Wrapper:

Code: Alles auswählen

unit llist;
 
{$MODE OBJFPC}{$H+}
{$PackRecords C}
 
interface
type
  PListItem = ^TListItem;
 
  TListItem = record
    Value: integer;
    NextItem: PListItem;
  end;
 
const
{$ifdef win32}
  lllib = 'linkedlist.dll';
{$else}
  {$ifdef darwin}
    lllib = 'liblinkedlist.dylib';
    {$linklib liblinkedlist.dylib}
  {$else}
    lllib = 'liblinkedlist.so';
  {$endif}
{$endif}
 
function GetListLength(List: PListItem): integer; cdecl; external lllib;
procedure AddElementAtStart(var List: PListItem; x: integer); cdecl; external lllib;
procedure AddElementAtEnd(var List: PListItem; x: integer); cdecl; external lllib;
procedure DeleteListItem(List: PListItem; i: integer); cdecl; external lllib;
function GetItemValue(List: PListItem; i: integer): integer; cdecl; external lllib;
procedure SetItemValue(List: PListItem; i, x: integer); cdecl; external lllib;
procedure ClearList(var List: PListItem); cdecl; external lllib;
 
implementation
 
end.


Unsere Program Datei muss nicht bearbeitet werden.

Bemerkung: Da wir diese Bibliothek nur Statisch verwenden tritt das Problem unter OSX mit _ nicht auf.


Wrapper erstellen

Für viele Programmbibliotheken gibt es Wrapper für viele verschiedene Sprachen, meißt auch Pascal (oder Delphi). Ist dies aber nicht der Fall, so lässt sich zwar aus einer sauberen Dokumentation der Bibliothek ein solcher Konstruieren, allerdings ist dies sehr mühsam und in der Realität wird man schnell merken dass die meißten Bibliotheken recht unsauber dokumentiert sind.
Abhilfe verschafft uns dabei die Wrapper in anderen Programmiersprachen. Für die meißten Bibliotheken gibt es einen Wrapper in C, als .h Datei, welcher alle Funktionen sauber implementiert. Wenn man nun nicht so gut C kann, oder es einem zu aufwändig ist den Wrapper zu übersetzen gibt es ein kleines Programm als Teil der FPC Installation, h2pas, welches C Headerfiles weitestgehend in Pascal Units übersetzt. Die Dokumentation zu diesem Programm findet ihr hier.


Und immer dran denken, niemals Klassen exportieren. Die sind nicht mal unter verschiedenen FPC Versionen kompatibel, geschweige denn zwischen fpc und anderen Compilern

hubblec4
Beiträge: 341
Registriert: Sa 25. Jan 2014, 17:50

Re: DLL Programmierung - Anfängerfragen

Beitrag von hubblec4 »

Danke Warf, riesen Text.

Alle 3 Programme stammen von Lazarus.
Eine Unit schreiben und in allen 3 proggis verwenden, ist dies das beste? Warum ist dies der DLL vorzuziehen?

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: DLL Programmierung - Anfängerfragen

Beitrag von m.fuchs »

hubblec4 hat geschrieben:Eine Unit schreiben und in allen 3 proggis verwenden, ist dies das beste? Warum ist dies der DLL vorzuziehen?

  1. Du kannst direkt auf deine Klassen zugreifen und nicht nur exportierte Funktionen nutzen.
  2. Du kannst alle Datentypen die Freepascal und dein Programm kennt nutzen.
  3. Du kannst beim Debuggen direkt in den Quellcode springen ohne ein riesiges Debupgsetup aufzubauen.
  4. Du musst beim Ausliefern nicht noch eine DLL dazupacken.
  5. Du landest nicht in der DLL Hölle.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

hubblec4
Beiträge: 341
Registriert: Sa 25. Jan 2014, 17:50

Re: DLL Programmierung - Anfängerfragen

Beitrag von hubblec4 »

m.fuchs hat geschrieben:
hubblec4 hat geschrieben:Eine Unit schreiben und in allen 3 proggis verwenden, ist dies das beste? Warum ist dies der DLL vorzuziehen?

  1. Du kannst direkt auf deine Klassen zugreifen und nicht nur exportierte Funktionen nutzen.
  2. Du kannst alle Datentypen die Freepascal und dein Programm kennt nutzen.
  3. Du kannst beim Debuggen direkt in den Quellcode springen ohne ein riesiges Debupgsetup aufzubauen.
  4. Du musst beim Ausliefern nicht noch eine DLL dazupacken.
  5. Du landest nicht in der DLL Hölle.


OK, überzeugt.

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: DLL Programmierung - Anfängerfragen

Beitrag von af0815 »

7. Forms und Datamodule gehen in dll nur mit Tricks.

Also nix mit schnell eine Form in eine dll packen.

Andreas
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

hubblec4
Beiträge: 341
Registriert: Sa 25. Jan 2014, 17:50

Re: DLL Programmierung - Anfängerfragen

Beitrag von hubblec4 »

Eine From ist nicht geplant, dazu hatte ich auch ein Thema gefunden und dort gab es damals noch sorgen. Kann sein das es nun geht, aber ich brauche ja nicht.

Ich komme immer besser mit der Lazarus Struktur zurecht und finde die units schon recht simple aber mächtig. Sollte ein leichtes sein die in andere Projekte einzubinden.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: DLL Programmierung - Anfängerfragen

Beitrag von Warf »

Ich lese auch etwas heraus, das du nicht so erfahren im Umgang mit Units bist, daher hier mal der Abschnitt über Units aus meinem Tutorial (das ist der Anfang des Kapitels zu dem ich dir bereits schon einen auszug gepostet habe):

Units
Essentiell für Bibliotheken sind die Unit Dateien. Unit Dateien bieten uns die Möglichkeit Pascal Code, egal ob Typen, Konstanten, Variablen oder Funktionen, in eine extra Datei auszulagern, und diese dann in Programmen oder anderen Units zu verwenden. Eine Unit die wir bereits schon recht häufig verwendet haben ist die Unit SysUtils aus der RTL. Wir haben vor allem Funktionen wie StrToInt verwendet, aber auch schon Typen wie die Exception EConvertError (siehe Einschub Kontrollstrukturen 2).

Die Verwendung von Units geschieht über die Uses Klausel, wie wir es bereits mit SysUtils gemacht haben. Zu den Besonderheiten der Uses Klausel komme ich später noch zurück, doch jetzt erst einmal zum aufbau einer Unit Datei.

Ein Unit Datei ist eine Pascal Datei, normalerweise mit Dateiendung .pas oder .pp. Der Aufbau ist wie folgt:

Code: Alles auswählen

unit Name;
 
{$MODE ObjFpc}{$H+}
// Weitere Compileranweisungen z.B. Packrecords
 
interface // Öffentlicher Bereich, Zugriff aus allen anderen Dateien möglich
 
{ Uses Klausel  }
{ Type Klausel  }
{ Var Klausel   }
{ Const Klausel }
 
{ Öffentliche Funktionsköpfe }
 
implementation // Privater Bereich, nur lokale Definitionen und Programmcode
 
{ Uses Klausel  }
{ Type Klausel  }
{ Var Klausel   }
{ Const Klausel }
 
{ Funktionen }
 
initialization // optional
  // Dieser Bereich wird bei Programmstart ausgeführt
 
finalization // optional
  // Dieser Bereich wird bei Programmende ausgeführt
 
end.


Eine Unit Programmdatei beginnt mit dem Schlüsselwort unit gefolgt von dem Unit Name. Dieser Name ist der Bezeichner der in der Uses Klausel steht. Der Unit Name sollte mit dem Dateinamen übereinstimmen. Die Unit MyUnit sollte also entsprechend in einer Datei MyUnit.pas oder MyUnit.pp stehen. Diese Kopfzeile wird, wie bei Programm Dateien mit einem Semikolon abgeschlossen.
Abgeschlossen wird die Datei mit einem end. welches, wie bei Programmen auch, das Dateiende Signalisiert.
Direkt unter dem Kopf kommen dann globale Compileranweisungen wie unser altbekanntes $Mode.
Darunter wird die Unit in 2 Bereiche eingeteilt, dem Interface und der Implementation.

Der Interface Teil enthält die allgemeine Uses Klausel der Datei sowie alle Informationen die nach außen Sichtbar sein sollen, also die von anderen Units und Programmen verwendet werden können. Darunter fallen Typen, Funktionen, globale Variablen und Konstanten. Im Interface kann aber kein ausführbarer Block stehen. Um Funktionen öffentlich zu machen werden diese im Implementation Teil geschrieben, und im Interface steht ein Verweis darauf, der Funktionskopf.

Der Implementation Teil kann auch eine Uses Klausel enthalten, diese ist allerdings nur im Implementation Teil nutzbar. Dies hat den Grund, da man in Pascal keine Zirkulären Unit Referenzen schreiben kann, das heißt wenn Unit1 im Interface über Uses Unit2 einbindet darf Unit2 im Interface Teil nicht Unit1 einbinden. Werden allerdings dennoch Elemente, z.B. eine Globale Variable aus Unit1 benötigt so kann man Unit1 im Implementation Teil über uses Einbinden. Allerdings sollte man auch möglichst versuchen das zu vermeiden, und für die geteilten Informationen einfach eine extra Unit erstellen welche von Unit1 und Unit2 verwendet wird.
Darauf folgen im Implementation Teil Typen, Variablen und Konstanten die nur innerhalb der Unit verwendet werden. Hierbei gilt die Regel, es ist alles benutzbar was im Quelltext drüber steht.

Außerdem lassen sich im Implementation Teil Funktionen schreiben. Dies ist eigentlich genau so wie das Funktionsschreiben in einem Programm. Möchte man nun diese Funktionen für anderen Dateien sichtbar machen, so muss man den Funktionskopf in den Implementation Teil kopieren.

Unter dem Implementation Teil kann noch ein initialization und ein finalization Block stehen. Der Initialization Block wird beim start der Anwendung ausgeführt und kann genutzt werden um z.B. Globale Variablen für die Unit zu setzen. Der Finalization Block wird bei Programmende Ausgeführt und kann z.B. zum Aufräumen wie dem Freigeben von Zeigern verwendet werden.

Eine Beispiel Unit könnte wie folgt aussehen:

Code: Alles auswählen

unit TestUnit;
 
{$Mode ObjFpc}{$H+}
 
interface
 
uses SysUtils;
 
type
  PTestType = ^TTestType;
  TTestType = record
    i, x: Integer;
  end;
 
var
  GlobaleVariable: PTestType;
const
  GlobaleKonstante = 5;
 
function GlobaleFunktion(i: Integer): Integer;
 
implementation
 
var
  Aufrufe: Integer;
 
procedure LocalCounter;
begin
  inc(Aufrufe);
end;
 
function GlobaleFunktion(i: Integer): Integer;
begin
  Result := i * GlobaleKonstante;
  LocalCounter;
end;
 
initialization
  new(GlobaleVariable);
  Aufrufe := 0;
 
finalization
  dispose(GlobaleVariable);
 
end.


Diese Unit würde nun einem Programm oder anderen Units eine Globale Variable, eine Funktion, zwei Typen und eine Konstante bereitstellen. Gleichzeitig übernimmt diese Unit noch das Erstellen und bereinigen eines Zeigers und verwendet intern noch eine Variable und eine Funktion welche von anderen Dateien nicht verwendet werden können.

Nun aber zu einem etwas praktischeren Beispiel, nehmen wir die Verkettete Liste aus dem Letzen Kapitel, diese eignet sich sehr gut zum Auslagern.
Also erstellen wir im ordner unseres Programms Liste aus dem letzen Kapitel eine neue Datei, linkedlist.pas und schreiben dort schonmal die Allgemeine Unit struktur hin:

Code: Alles auswählen

unit LinkedList;
 
{$MODE ObjFpc}{$H+}
 
interface
 
implementation
 
end.


Bemerkung: In Lazarus gibt es die Schaltfläche "Neue Unit" oder den Menüeintrag Datei->Neue Unit um eine solche Unit zu erstellen.

Nun gehen wir in unsere Programmdatei, alle Funktionen die wir für die Liste erstellt haben können wir nun in den Implementation Teil der neuen Unit kopieren, und dann aus der Programmdatei löschen

Code: Alles auswählen

function GetListLength(List: PListItem): integer;
begin
  if Assigned(List) then
    Result := 1 + GetListLength(List^.NextItem)
  else
    Result := 0;
end;
 
procedure AddElementAtStart(var List: PListItem; x: integer);
var
  tmp: PListItem;
begin
  new(tmp);
  tmp^.Value := x;
  tmp^.NextItem := List;
  List := tmp;
end;
 
procedure AddElementAtEnd(var List: PListItem; x: integer);
begin
  if Assigned(List) then
    AddElementAtEnd(List^.NextItem, x)
  else
  begin
    new(List);
    List^.Value := x;
    List^.NextItem := nil;
  end;
end;
 
procedure DeleteListItem(List: PListItem; i: integer);
var
  tmp: PListItem;
begin
  if not Assigned(List) then
    exit; // List Item nicht vorhanden
  if i > 1 then
    DeleteListItem(List^.NextItem, i - 1)
  else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert
  begin
    tmp := List^.NextItem^.NextItem; //Übernächstes Item
    dispose(List^.NextItem);
    List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item
  end;
end;
 
function GetItemValue(List: PListItem; i: integer): integer;
begin
  Result := 0;
  if Assigned(List) then
    if i > 0 then
      Result := GetItemValue(List^.NextItem, i - 1)
    else
      Result := List^.Value;
end;
 
procedure SetItemValue(List: PListItem; i, x: integer);
begin
  if Assigned(List) then
    if i > 0 then
      SetItemValue(List^.NextItem, i - 1, x)
    else
      List^.Value := x;
end;
 
procedure ClearList(var List: PListItem);
begin
  if Assigned(List) then
  begin
    ClearList(List^.NextItem);
    dispose(List);
    List := nil;
  end;
end;


Um diese nun öffentlich nutzbar zu machen müssen wir noch die Funktionsköpfe jeder Funktion in den Interface Teil der Unit schreiben:

Code: Alles auswählen

function GetListLength(List: PListItem): integer;
procedure AddElementAtStart(var List: PListItem; x: integer);
procedure AddElementAtEnd(var List: PListItem; x: integer);
procedure DeleteListItem(List: PListItem; i: integer);
function GetItemValue(List: PListItem; i: integer): integer;
procedure SetItemValue(List: PListItem; i, x: integer);
procedure ClearList(var List: PListItem);


Nun müssen wir nur noch den Typ vom Programm in die Unit kopieren, und wir haben unsere Verkettete Liste komplett in eine Unit ausgelagert:

Code: Alles auswählen

unit LinkedList;
 
{$MODE ObjFpc}{$H+}
 
interface
 
type
  PListItem = ^TListItem;
 
  TListItem = record
    Value: integer;
    NextItem: PListItem;
  end;
 
function GetListLength(List: PListItem): integer;
procedure AddElementAtStart(var List: PListItem; x: integer);
procedure AddElementAtEnd(var List: PListItem; x: integer);
procedure DeleteListItem(List: PListItem; i: integer);
function GetItemValue(List: PListItem; i: integer): integer;
procedure SetItemValue(List: PListItem; i, x: integer);
procedure ClearList(var List: PListItem);
 
implementation
 
function GetListLength(List: PListItem): integer;
begin
  if Assigned(List) then
    Result := 1 + GetListLength(List^.NextItem)
  else
    Result := 0;
end;
 
procedure AddElementAtStart(var List: PListItem; x: integer);
var
  tmp: PListItem;
begin
  new(tmp);
  tmp^.Value := x;
  tmp^.NextItem := List;
  List := tmp;
end;
 
procedure AddElementAtEnd(var List: PListItem; x: integer);
begin
  if Assigned(List) then
    AddElementAtEnd(List^.NextItem, x)
  else
  begin
    new(List);
    List^.Value := x;
    List^.NextItem := nil;
  end;
end;
 
procedure DeleteListItem(List: PListItem; i: integer);
var
  tmp: PListItem;
begin
  if not Assigned(List) then
    exit; // List Item nicht vorhanden
  if i > 1 then
    DeleteListItem(List^.NextItem, i - 1)
  else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert
  begin
    tmp := List^.NextItem^.NextItem; //Übernächstes Item
    dispose(List^.NextItem);
    List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item
  end;
end;
 
function GetItemValue(List: PListItem; i: integer): integer;
begin
  Result := 0;
  if Assigned(List) then
    if i > 0 then
      Result := GetItemValue(List^.NextItem, i - 1)
    else
      Result := List^.Value;
end;
 
procedure SetItemValue(List: PListItem; i, x: integer);
begin
  if Assigned(List) then
    if i > 0 then
      SetItemValue(List^.NextItem, i - 1, x)
    else
      List^.Value := x;
end;
 
procedure ClearList(var List: PListItem);
begin
  if Assigned(List) then
  begin
    ClearList(List^.NextItem);
    dispose(List);
    List := nil;
  end;
end;
 
end.


In unserem Programm fügen wir dann noch ein uses LinkedList hinzu:

Code: Alles auswählen

program Liste;
 
{$MODE ObjFpc}{$H+}
 
uses
  LinkedList;
 
var root: PListItem;
    i: Integer;
begin
  root:=nil; // Initialisierung leere liste
  AddElementAtStart(root, 10);
  AddElementAtEnd(root, 5);
  AddElementAtStart(root, 3);
  AddElementAtStart(root, 8);
  AddElementAtEnd(root, 15);
  for i:=0 to GetListLength(root) -1 do
    SetItemValue(root, i, GetItemValue(root, i)+2);
  for i:=GetListLength(root)-1 downto 0 do
    WriteLn(GetItemValue(root, i));
  ClearList(root);
end.


Und schon haben wir eine eigene kleine Bibliotheken für Verkettete Listen erstellt, die wir jetzt in anderen Units oder Programmen verwenden können.


Units geben uns die Möglichkeit unser Programm in kleine Programmdateien zu unterteilen, was zum einen eine bessere Struktur und Lesbarkeit gibt, wodurch es einfacher ist bestimmte dinge zu finden, und zu bearbeiten, zum anderen erlauben sie uns aber auch Programmcode ohne viel aufwand jederzeit wieder zu verwenden.

Nun möchte ich nochmal auf Uses eingehen. Im Uses wird, über Komma getrennt, der name der Units angegeben die man in dieser Datei verwenden möchte, dabei sucht der FPC zunächst einmal in den Suchpfaden nach der Unit Datei, heißt die Unit z.B. Unit1 so sucht der Compiler nach Unit1.pas oder Unit1.pp.
Die Suchpfade des FPC sind:
1. Das Programmverzeichnis
2. Das Compilerverzeichnis
3. Die Suchpfade aus der fpc.cfg
4. Die Suchpfade die über -Fu beim Compilieren angegeben werden.

Bei der Verwendung von Lazarus kümmert sich die IDE um das verwalten der Suchpfade.

Liegt eine Datei nicht in den Suchpfaden, oder ist anders als der Unitname benannt, so kann man mit dem Schlüsselwort in auch den Pfad angeben:

Code: Alles auswählen

uses
  Unit1 in 'Unit1.pas', // Im selben Ordner
  Unit2 in '../Unit2.pas', // Relative Pfadangaben mit ../ und ./ möglich
  Unit3 in '/Pfad/Zur/Unit.pas'; // absoluze Pfadangaben


Bemerkung: Unter Windows sollte man natürlich \ statt / verwenden.

Die Reihenfolge in der man die Units einbindet spielt auch eine Rolle, hat man mehrere Dateien die gleichnamige Funktionen, Konstanten, etc. hat, so wird der FPC immer entweder aus der selben Datei nehemen oder aus der, die aus der Unit nehmen die als erste in der Uses Klausel steht. Möchte man dennoch z.B. eine Funktion aus einer anderen Unit verwenden kann man diese speziell angeben:

Code: Alles auswählen

uses
  Unit1, Unit2;
 
...
 
procedure Foo;
begin
 
end;
 
begin
  Test; // Prozedur Test aus Unit1
  Unit2.Test; // Aus Unit2
  Foo; // Aus eigener datei;
  Unit1.Foo; // Aus Unit1
end;


Außerdem werden die Initialization und Finalization Teile in der Reihenfolge ausgeführt wie sie in der Uses Klausel stehen. Hat man also eine Unit, welche eine z.B. eine Globale Variable im Initialization Teil setzt, welche von anderen Units im Initialization Teil verwendet wird, so muss diese vor den anderen Units im Uses stehen. Ein Beispiel dazu folgt später in diesem Kapitel.

hubblec4
Beiträge: 341
Registriert: Sa 25. Jan 2014, 17:50

Re: DLL Programmierung - Anfängerfragen

Beitrag von hubblec4 »

In der Tat fehlt es mir an Erfahrung was Lazarus alles bietet. Ich habe mir Lazarus selber beigebracht, und natürlich mit der Hilfe dieses tollen Forums und der netten Leute.

Ich kenne mich schon sehr gut mit den units aus, aber auch da gibt es noch Lücken.
initialization und finalization sind Dinge die ich noch nie benutzt habe, könnte aber sehr hilfreich sein.
Ebenso habe ich vor ca. einem Jahr die OOP erst erlernt, und da mit sicherheit nur einen Bruchteil.

Aufjedenfall Danke für die Infos.

Antworten