Building and using Free Pascal dynamic link libraries.
A shared library is a computer file that contains executable code designed to be used by multiple computer programs or other libraries. When running a program, that is configured to use a shared library, the operating system loads the shared library from a file (other than the program's executable file) into memory at runtime.
A dynamic-link library (DLL) is a shared library in the Microsoft Windows or OS/2 operating system. It can contain executable code (functions, procedures), data, and resources, in any combination. A DLL file mostly has the file extension .dll.
This tutorial is about creating a DLL using Lazarus/Free Pascal on Windows 10, and calling the DLL procedures from Pascal, C, and C++. It should apply to Windows 11, too, probably also to Windows 8, or even earlier. No idea, how far it applies to Linux or MacOS... The sample code has been tested using Lazarus 2.2.6 (with FPC 3.2.2 64-bit), and (for the C and C++ samples) MSYS2 2024-01-13, UCRT64 subsystem (with gcc 14.1.0 64-bit). The Pascal builds should work with other versions of FPC, too; no guarantee for other C/C++ compilers. For help with MSYS2, please, have a look at Using Sublime Text as IDE for GCC/GFortran development.
To create a shared library in Lazarus, choose File > New... from the menu bar, then, in the opening New... window, select Library
This creates a source skeleton, similar to the one of a command line program. Note, that for a library, the reserved word program has to be replaced by the reserved word library
library Project1;
{$mode objfpc} {$H+}
uses
Classes;
begin
end.
Here are some general remarks concerning the code of a Free Pascal library.
- As the library (normally) doesn't contain a program (but just functions and procedures, called by a program), the begin of the program-block is omitted. On the other hand, the terminal end is mandatory.
- To make a procedure or function accessible to a calling program, it must be exported, using an exports statement.
- To make the DLL procedures and functions accessible to C/C++ programs, they should be declared as cdecl.
- The cdecl declarations require the usage of C compatible data types. In particular: Use cint32 instead of Integer, use cfloat instead of Real, and use PChar instead of string (for characters, you may use Char).
- To use the C data types, you need to include the ctypes unit. To work with the data type PChar, you need to include the unit Strings.
Creating and building the library.
The sample library "display", used in this tutorial, contains some display procedures, based on the Crt unit. Most of them are not really useful for being called from a Pascal program (as the procedure itself, or a similar procedure already exists in the Crt unit), but they are a simple way to perform positional and colored output to the command line screen from a C/C++ program.
Here is the code of my "display" library (you are free to extend it with further procedures, or change it to fit your needs and wishes). The code includes a short description of what a given procedure does, and what are the arguments that you must specify when calling it. Click the following link to download the source code of the tutorial samples.
library display;
{$mode objfpc}{$H+}
uses
SysUtils, Crt, Strings, ctypes;
{ Clear the screen }
procedure ClearScreen; cdecl;
// Arguments: none
begin
ClrScr;
end;
{ Clear end of line }
procedure ClearEol; cdecl;
// Arguments: none
begin
ClrEol;
end;
{ Display end of line character = new line }
procedure WriteEol; cdecl;
// Arguments: none
begin
Write(LineEnding);
end;
{ Repeated character display }
procedure WriteChar(C: Char; Count: cint32); cdecl;
// Arguments:
// character to be displayed (Char)
// repeat factor = count (CInt32)
var
I: Integer;
begin
for I := 1 to Count do
Write(C);
end;
{ Formattded string display }
procedure WriteString(S: PChar; Align: Char = 'L'; Width: cint32 = 0); cdecl;
// Arguments:
// string to be displayed (PChar)
// alignment (Char): L (left; default), R (right), C (center)
// display area width (CInt32); left/right/left+right padding with spaces; default = 0 (no padding)
var
Len, Spaces, Spaces2: Integer;
begin
Align := Uppercase(Align)[1];
Len := StrLen(S);
if Width >= 0 then begin
Spaces := Width - Len;
if Align = 'L' then begin
// Left alignment
Write(S);
WriteChar(' ', Spaces);
end
else if Align = 'R' then begin
// Right alignment
WriteChar(' ', Spaces);
Write(S);
end
else if Align = 'C' then begin
// Centered
Spaces2 := Spaces div 2;
WriteChar(' ', Spaces2);
Write(S);
WriteChar(' ', Spaces2);
if Spaces mod 2 = 1 then
Write(' ');
end;
end;
end;
{ Formatted integer display }
procedure WriteInt(N: cint32; Width: cint32 = 0); cdecl;
// Arguments:
// integer to be displayed (CInt32)
// display area width (CInt32); left padding with spaces; default = 0 (no padding)
begin
if Width >= 0 then
Write(N:Width);
end;
{ Formatted float (real) display }
procedure WriteFloat(R: cfloat; Width: cint32 = 0; Digits: cint32 = 0); cdecl;
// Arguments:
// float to be displayed (CFloat)
// display area width (CInt32); left padding with spaces; default = 0 (no padding)
// number of decimal digits (CInt32); default = 0
begin
if (Width >= 0) and (Digits >= 0) then
Write(R:Width:Digits);
end;
{ Set foreground (text) and background colors }
procedure SetColor(FG: cint32; BG: cint32 = Black); cdecl;
// Arguments:
// foreground color (CInt32); Crt unit text color (0 - 15)
// background color (CInt32); Crt unit background color (0 - 7); default = 0 (black)
begin
if (FG >= 0) and (FG <= 15) and (BG >= 0) and (BG <= 7) then begin
TextColor(FG); TextBackground(BG);
end;
end;
{ Reset foreground (text) and background colors }
procedure ResetColor; cdecl;
// Arguments: none
// Text color is set to LightGray (7), background color to Black (0)
begin
TextColor(LightGray); TextBackground(Black);
end;
{ Colored string display }
procedure WriteColor(S: PChar; FG: cint32; BG: cint32 = Black); cdecl;
// Arguments:
// string to be displayed (PChar)
// foreground color (CInt32); Crt unit text color (0 - 15)
// background color (CInt32); Crt unit background color (0 - 7); default = 0 (black)
begin
SetColor(FG, BG);
Write(S);
ResetColor;
end;
{ Set cursor position }
procedure SetPos(Y, X: cint32); cdecl;
// Arguments:
// screen row = Y (CInt32)
// screen column = X (CInt32)
begin
if (X > 0) and (Y > 0) then
GotoXY(X, Y);
end;
{ Positional string display }
procedure WritePos(S: PChar; Y, X: cint32); cdecl;
// Arguments:
// string to be displayed (PChar)
// screen position: row = Y (CInt32)
// screen position: column = X (CInt32)
begin
SetPos(Y, X);
Write(S);
end;
{ Export the procedures (!) }
exports
ClearScreen, ClearEol, WriteEol, WriteChar, WriteString, WriteInt, WriteFloat,
SetColor, ResetColor, WriteColor, SetPos, WritePos;
end.
Build the DLL in Lazarus, just the same way that you would build a program (choose Run > Build from the menu bar). The screenshot shows a successful build with the creation of a DLL (display.dll).
Testing the DLL procedures (Pascal).
Some general remarks concerning the code of a Free Pascal program calling DLL subroutines.
- The name of the DLL, containing the procedures/functions, has to be specified using the directive {$LINKLIB <DLL-name>} (in our case: {$LINKLIB display}).
- All procedure and function used in the calling program must be declared as external.
- If the functions have been declared as cdecl in the DLL, they have to be declared as cdecl in the calling program, too.
- The remarks concerning cdecl declarations, made above for the DLL source (C compatible data types, units to include) also apply to the calling program.
Here is the code of a simple program (test_display) that can be used to test (most of) the procedures of the "display" DLL.
program test_display;
{$mode objfpc}{$H+}
{$LINKLIB display}
uses
ctypes;
const
Black = 0; Yellow = 14; DarkGray = 8;
Title: PChar = 'Test DISPLAY unit.';
EndOfProg: PChar = 'Enter to terminate ';
Name: PChar = 'Aly';
Arr: array[0..9] of Real = (
1, 1.25, 1.50, 2, 2.25, 2.50, 3, 3.25, 3.50, 4
);
var
I: Integer;
{ Declaration of the external (DLL) procedures used }
procedure ClearScreen; cdecl; external;
procedure WriteEol; cdecl; external;
procedure SetColor(FG: cint32; BG: cint32 = Black); cdecl; external;
procedure ResetColor; cdecl; external;
procedure SetPos(Y, X: cint32); cdecl; external;
procedure WriteString(S: PChar; Align: Char = 'l'; Width: cint32 = 0); cdecl; external;
procedure WriteChar(C: Char; Count: cint32); cdecl; external;
procedure WriteFloat(R: cfloat; Width: cint32 = 0; Digits: cint32 = 0); cdecl; external;
procedure WriteColor(S: PChar; FG: cint32; BG: cint32 = Black); cdecl; external;
procedure WritePos(S: PChar; Y, X: cint32); cdecl; external;
{ Main program }
begin
ClearScreen;
// Display colored text (underlined with '=' signs) at given screen position
SetColor(Yellow);
WritePos(Title, 1, 21);
SetPos(2, 21);
WriteChar('=', 18);
ResetColor; WriteEol; WriteEol;
// String alignment (left, right, centered)
WriteChar('-', 10); WriteEol;
WriteString(Name); WriteEol;
WriteString(Name, 'r', 10); WriteEol;
WriteString(Name, 'c', 10); WriteEol; WriteEol;
// Formatted display of real numbers
for I := 0 to 9 do begin
WriteFloat(Arr[I], 0, 2);
WriteChar(' ', 2);
end;
WriteEol; WriteEol;
// Colored string display
WriteColor(EndOfProg, DarkGray);
// Wait for user hitting ENTER key (and terminate the program)
Readln;
end.
To build the program in Lazarus, place the DLL into the project directory, and build the program just the same way that you always do. As the custom display functions are part of display.dll, thus, to execute the program, you must place the DLL where Windows can find it, i.e. either into the same directory as the executable, or into C:\Windows\System32. Trying to run the program without the DLL results in its abortion with the error message Entry point not found, similar to the one on the screenshot.
The screenshot below shows the successful execution of the program.
Testing the DLL procedures (C).
Now lets test our DLL with a C program. The executable test_display1.exe should do the same as does the Pascal program test_display.exe from before.
The best practice is to define all functions and procedures of the DLL in a header file, where they have to be declared as extern. Here is the code of display.h.
extern void ClearScreen(void);
extern void ClearEol(void);
extern void WriteEol(void);
extern void SetColor(int, int);
extern void ResetColor(void);
extern void SetPos(int, int);
extern void WriteString(char*, char, int);
extern void WriteChar(char, int);
extern void WriteFloat(float, int, int);
extern void WriteColor(char*, int, int);
extern void WritePos(char*, int, int);
With the procedures declared in display.h, all we have to do in the C source file is to include the header file. So no difference of any kind with the source of a "normal" C program. Here is the code of test_display.c.
#include <stdio.h>
#include "display.h"
int main() {
int Black = 0; int Yellow = 14; int DarkGray = 8;
char *Title = "Test DISPLAY unit.";
char *EndOfProg = "Enter to terminate ";
char *Name = "Aly";
float Arr[10] = {
1, 1.25, 1.50, 2, 2.25, 2.50, 3, 3.25, 3.50, 4
};
char Buffer[2];
ClearScreen();
SetColor(Yellow, Black);
WritePos(Title, 1, 21);
SetPos(2, 21);
WriteChar('=', 18);
ResetColor(); WriteEol(); WriteEol();
WriteChar('-', 10); WriteEol();
WriteString(Name, 'l', 0); WriteEol();
WriteString(Name, 'r', 10); WriteEol();
WriteString(Name, 'c', 10); WriteEol(); WriteEol();
for (int I = 0; I < 10; I++) {
WriteFloat(Arr[I], 0, 2);
WriteChar(' ', 2);
}
WriteEol(); WriteEol();
WriteColor(EndOfProg, DarkGray, Black); fgets(Buffer, 2, stdin);
}
To build the C program, the DLL has to be together with the source and the header file. I did this build in the MSYS2 UCRT64 shell, installed as terminal in Sublime Text (screenshot below). The build command is as follows:
gcc -o test_display1.exe test_display.c display.dll
And here is the program output in UCRT64 (if the colors look somewhat different as before, this is due to the terminal, not to C; running the program in Command Prompt will produce exactly the same output as does the Pascal program).
Testing the DLL procedures (C++).
We can use the C header file display.h from before. All we have to do in the C++ source file is to include the header file. However, as the procedures actually are C and not C++ code, the include has to be done using the extern "C" directive. For the rest, no difference of any kind with the source of a "normal" C++ program. Here is the code of test_display.cpp.
#include <iostream>
extern "C" {
#include "display.h"
}
using namespace std;
int main(void) {
int Black = 0; int Yellow = 14; int DarkGray = 8;
char *Title = "Test DISPLAY unit.";
char *EndOfProg = "Enter to terminate ";
char *Name = "Aly";
float Arr[10] = {
1, 1.25, 1.50, 2, 2.25, 2.50, 3, 3.25, 3.50, 4
};
char Buffer[2];
ClearScreen();
SetColor(Yellow, Black);
WritePos(Title, 1, 21);
SetPos(2, 21);
WriteChar('=', 18);
ResetColor(); WriteEol(); WriteEol();
WriteChar('-', 10); WriteEol();
WriteString(Name, 'l', 0); WriteEol();
WriteString(Name, 'r', 10); WriteEol();
WriteString(Name, 'c', 10); WriteEol(); WriteEol();
for (int I = 0; I < 10; I++) {
WriteFloat(Arr[I], 0, 2);
WriteChar(' ', 2);
}
WriteEol(); WriteEol();
WriteColor(EndOfProg, DarkGray, Black); fgets(Buffer, 2, stdin);
return EXIT_SUCCESS;
}
To build the C++ program, the DLL has to be together with the source and the C header file. I did this build in the MSYS2 UCRT64 shell, installed as terminal in Sublime Text (screenshot below). The build command is as follows:
g++ -o test_display2.exe test_display.cpp display.dll
I'm not a C/C++ programmer, so I don't know if using code forbidden by ISO C++ is an issue. It's surely not in this sample, and the important thing for me is that the build succeeded without errors and that the executable test_display2.exe was created.
The program output of test_display2.exe is (of course) exactly the same as for the C program test_display1.exe.
If you find this text helpful, please, support me and this website by signing my guestbook.