🔝 Retour au Sommaire
Avant de plonger dans l'abstraction, comprenons d'abord ce qu'est une API système. Une API (Application Programming Interface) système est l'ensemble des fonctions et procédures que le système d'exploitation met à disposition des programmes pour interagir avec lui.
Imaginez que vous êtes dans un restaurant :
- Vous = Votre application
- La cuisine = Le système d'exploitation
- Le serveur = L'API système
Vous ne pouvez pas aller directement en cuisine (accéder au matériel), vous devez passer par le serveur (l'API) qui transmet vos demandes et vous ramène ce que vous avez commandé.
Sur Windows :
// Ouvrir une boîte de dialogue Windows native
MessageBox(0, 'Bonjour', 'Titre', MB_OK);
// Créer un fichier via l'API Windows
CreateFile('C:\test.txt', GENERIC_WRITE, 0, nil, CREATE_ALWAYS, 0, 0);Sur Linux :
// Ouvrir un fichier via l'API POSIX
open('/home/user/test.txt', O_CREAT or O_WRONLY, 0644);
// Afficher via GTK
gtk_message_dialog_new(...);Le problème est évident : le même besoin (créer un fichier, afficher un message) nécessite du code complètement différent selon l'OS !
Sans abstraction, votre code ressemblerait à ceci :
procedure SauvegarderFichier(const NomFichier: string; const Contenu: string);
begin
{$IFDEF WINDOWS}
// 50 lignes de code spécifique Windows
var
hFile: THandle;
BytesWritten: DWORD;
begin
hFile := CreateFileW(PWideChar(NomFichier), GENERIC_WRITE, 0, nil,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if hFile <> INVALID_HANDLE_VALUE then
begin
WriteFile(hFile, Contenu[1], Length(Contenu), BytesWritten, nil);
CloseHandle(hFile);
end;
end;
{$ENDIF}
{$IFDEF LINUX}
// 50 autres lignes de code spécifique Linux
var
fd: Integer;
begin
fd := FpOpen(PChar(NomFichier), O_WRONLY or O_CREAT or O_TRUNC, &644);
if fd >= 0 then
begin
FpWrite(fd, Contenu[1], Length(Contenu));
FpClose(fd);
end;
end;
{$ENDIF}
end;Problèmes de cette approche :
- Code dupliqué et difficile à maintenir
- Risque d'oublier une plateforme lors des modifications
- Impossible de tester facilement sur toutes les plateformes
- Code illisible avec toutes ces conditions
L'abstraction consiste à créer une couche intermédiaire qui "cache" les différences entre les systèmes :
procedure SauvegarderFichier(const NomFichier: string; const Contenu: string);
var
F: TextFile;
begin
AssignFile(F, NomFichier);
Rewrite(F);
Write(F, Contenu);
CloseFile(F);
end;Ce code fonctionne identiquement sur Windows et Linux ! FreePascal s'occupe de traduire ces commandes dans l'API appropriée selon l'OS.
FreePascal/Lazarus offre plusieurs niveaux d'abstraction, du plus bas au plus haut :
La RTL est la bibliothèque de base de FreePascal qui abstrait les opérations système fondamentales.
program ExempleRTL;
uses SysUtils;
var
MonFichier: File of Byte;
Buffer: array[0..1023] of Byte;
BytesLus: Integer;
begin
// Ces fonctions marchent sur TOUS les OS
// 1. Vérifier l'existence d'un fichier
if FileExists('config.ini') then
WriteLn('Le fichier existe');
// 2. Créer un répertoire
if not DirectoryExists('MonDossier') then
CreateDir('MonDossier');
// 3. Obtenir le répertoire temporaire (différent selon l'OS)
WriteLn('Dossier temp: ', GetTempDir);
// Windows: C:\Users\Username\AppData\Local\Temp\
// Linux: /tmp/
// 4. Obtenir le répertoire home de l'utilisateur
WriteLn('Dossier home: ', GetUserDir);
// Windows: C:\Users\Username\
// Linux: /home/username/
// 5. Construire un chemin portable
WriteLn('Chemin portable: ',
IncludeTrailingPathDelimiter(GetUserDir) + 'Documents');
// Windows: C:\Users\Username\Documents\
// Linux: /home/username/Documents/
end.program ProcessusPortable;
{$mode objfpc}{$H+}
{$modeswitch anonymousfunctions} // Nécessaire pour CreateAnonymousThread
uses
{$IFDEF UNIX}cthreads,{$ENDIF} // Nécessaire pour les threads sur Unix
Classes, SysUtils, Process;
var
MonProcessus: TProcess;
MonThread: TThread;
begin
// Lancer un processus externe (portable)
MonProcessus := TProcess.Create(nil);
try
{$IFDEF WINDOWS}
MonProcessus.Executable := 'notepad.exe';
{$ELSE}
MonProcessus.Executable := 'gedit';
{$ENDIF}
MonProcessus.Execute;
finally
MonProcessus.Free;
end;
// Créer un thread (fonctionne partout)
MonThread := TThread.CreateAnonymousThread(
procedure
begin
WriteLn('Thread en cours d''exécution');
Sleep(1000); // Attendre 1 seconde (portable)
end
);
MonThread.Start;
MonThread.WaitFor;
MonThread.Free;
end.La FCL construit sur la RTL en offrant des classes et composants plus sophistiqués.
program FCLExample;
uses Classes, SysUtils;
var
Flux: TFileStream;
Texte: string;
Octets: TBytes;
begin
// Créer/écrire dans un fichier (portable)
Flux := TFileStream.Create('donnees.bin', fmCreate or fmOpenWrite);
try
Texte := 'Bonjour Multi-plateforme!';
// Convertir string en bytes de manière portable
Octets := TEncoding.UTF8.GetBytes(Texte);
Flux.Write(Octets[0], Length(Octets));
finally
Flux.Free;
end;
// Lire le fichier (portable)
if FileExists('donnees.bin') then
begin
Flux := TFileStream.Create('donnees.bin', fmOpenRead);
try
SetLength(Octets, Flux.Size);
Flux.Read(Octets[0], Flux.Size);
Texte := TEncoding.UTF8.GetString(Octets);
WriteLn('Lu: ', Texte);
finally
Flux.Free;
end;
end;
end.program ConfigPortable;
uses Registry, SysUtils;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
try
// Sur Windows : utilise le registre Windows
// Sur Linux : utilise des fichiers de configuration
Reg.RootKey := HKEY_CURRENT_USER;
if Reg.OpenKey('/Software/MonApplication', True) then
begin
// Écrire une valeur
Reg.WriteString('DernierLancement', DateTimeToStr(Now));
Reg.WriteInteger('NombreLancements',
Reg.ReadInteger('NombreLancements') + 1);
// Lire une valeur
if Reg.ValueExists('Theme') then
WriteLn('Theme: ', Reg.ReadString('Theme'));
Reg.CloseKey;
end;
finally
Reg.Free;
end;
end.La LCL abstrait complètement les différences d'interface graphique entre les OS.
program LCLExample;
uses Forms, Dialogs, StdCtrls, Controls;
type
TMainForm = class(TForm)
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
end;
var
MainForm: TMainForm;
procedure TMainForm.Button1Click(Sender: TObject);
begin
// Ces dialogues s'adaptent à l'OS
// Dialogue de message (style natif de l'OS)
ShowMessage('Bonjour ' + Edit1.Text);
// Dialogue de confirmation (boutons selon l'OS)
if MessageDlg('Question', 'Voulez-vous continuer?',
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
// Dialogue de sélection de fichier (natif)
with TOpenDialog.Create(nil) do
try
Title := 'Choisir un fichier';
Filter := 'Tous les fichiers|*.*|Fichiers texte|*.txt';
if Execute then
ShowMessage('Fichier choisi: ' + FileName);
finally
Free;
end;
end;
end;
begin
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.Quand vous avez du code spécifique à chaque OS, créez une interface commune :
unit PlatformServices;
interface
type
{ Interface commune pour tous les OS }
IPlatformInfo = interface
function GetOSName: string;
function GetUserName: string;
function GetComputerName: string;
function IsAdmin: Boolean;
function GetMemoryInfo: Int64;
end;
{ Fonction pour obtenir l'implémentation correcte }
function GetPlatformInfo: IPlatformInfo;
implementation
uses
{$IFDEF WINDOWS}Windows, {$ENDIF}
{$IFDEF UNIX}BaseUnix, Unix, {$ENDIF}
SysUtils;
type
{$IFDEF WINDOWS}
{ Implémentation Windows }
TWindowsPlatform = class(TInterfacedObject, IPlatformInfo)
public
function GetOSName: string;
function GetUserName: string;
function GetComputerName: string;
function IsAdmin: Boolean;
function GetMemoryInfo: Int64;
end;
{$ENDIF}
{$IFDEF UNIX}
{ Implémentation Linux/Unix }
TUnixPlatform = class(TInterfacedObject, IPlatformInfo)
public
function GetOSName: string;
function GetUserName: string;
function GetComputerName: string;
function IsAdmin: Boolean;
function GetMemoryInfo: Int64;
end;
{$ENDIF}
{$IFDEF WINDOWS}
function TWindowsPlatform.GetOSName: string;
var
VersionInfo: TOSVersionInfo;
begin
VersionInfo.dwOSVersionInfoSize := SizeOf(VersionInfo);
if GetVersionEx(VersionInfo) then
begin
Result := Format('Windows %d.%d Build %d',
[VersionInfo.dwMajorVersion,
VersionInfo.dwMinorVersion,
VersionInfo.dwBuildNumber]);
end
else
Result := 'Windows';
end;
function TWindowsPlatform.GetUserName: string;
var
Buffer: array[0..255] of Char;
Size: DWORD;
begin
Size := SizeOf(Buffer);
if Windows.GetUserName(Buffer, Size) then
Result := Buffer
else
Result := 'Unknown';
end;
function TWindowsPlatform.GetComputerName: string;
var
Buffer: array[0..255] of Char;
Size: DWORD;
begin
Size := SizeOf(Buffer);
if Windows.GetComputerName(Buffer, Size) then
Result := Buffer
else
Result := 'Unknown';
end;
function TWindowsPlatform.IsAdmin: Boolean;
var
Token: THandle;
Elevation: TOKEN_ELEVATION;
Size: DWORD;
begin
Result := False;
if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then
try
if GetTokenInformation(Token, TokenElevation, @Elevation,
SizeOf(Elevation), Size) then
Result := Elevation.TokenIsElevated <> 0;
finally
CloseHandle(Token);
end;
end;
function TWindowsPlatform.GetMemoryInfo: Int64;
var
MemStatus: TMemoryStatusEx;
begin
MemStatus.dwLength := SizeOf(MemStatus);
if GlobalMemoryStatusEx(MemStatus) then
Result := MemStatus.ullTotalPhys
else
Result := 0;
end;
{$ENDIF}
{$IFDEF UNIX}
function TUnixPlatform.GetOSName: string;
begin
// Lire depuis /etc/os-release ou uname
Result := 'Linux/Unix';
// Simplification - normalement on lirait /etc/os-release
if FileExists('/etc/os-release') then
begin
// Parser le fichier pour obtenir PRETTY_NAME
Result := 'Ubuntu/Debian/Other Linux';
end;
end;
function TUnixPlatform.GetUserName: string;
begin
Result := GetEnvironmentVariable('USER');
if Result = '' then
Result := GetEnvironmentVariable('LOGNAME');
end;
function TUnixPlatform.GetComputerName: string;
begin
Result := GetEnvironmentVariable('HOSTNAME');
if Result = '' then
begin
// Alternativement, lire depuis /etc/hostname
if FileExists('/etc/hostname') then
begin
with TStringList.Create do
try
LoadFromFile('/etc/hostname');
if Count > 0 then
Result := Strings[0];
finally
Free;
end;
end;
end;
end;
function TUnixPlatform.IsAdmin: Boolean;
begin
// Sur Unix, vérifier si UID = 0 (root)
Result := FpGetuid = 0;
end;
function TUnixPlatform.GetMemoryInfo: Int64;
var
F: TextFile;
Line: string;
Value: Int64;
begin
Result := 0;
if FileExists('/proc/meminfo') then
begin
AssignFile(F, '/proc/meminfo');
Reset(F);
try
while not EOF(F) do
begin
ReadLn(F, Line);
if Pos('MemTotal:', Line) = 1 then
begin
// Extraire la valeur en kB et convertir en bytes
Delete(Line, 1, 9); // Enlever 'MemTotal:'
Line := Trim(Line);
if Pos(' kB', Line) > 0 then
Delete(Line, Pos(' kB', Line), 3);
if TryStrToInt64(Trim(Line), Value) then
Result := Value * 1024; // Convertir kB en bytes
Break;
end;
end;
finally
CloseFile(F);
end;
end;
end;
{$ENDIF}
function GetPlatformInfo: IPlatformInfo;
begin
{$IFDEF WINDOWS}
Result := TWindowsPlatform.Create;
{$ENDIF}
{$IFDEF UNIX}
Result := TUnixPlatform.Create;
{$ENDIF}
end;
end.program TestAbstraction;
uses PlatformServices, SysUtils;
var
Info: IPlatformInfo;
begin
// Obtenir l'implémentation appropriée automatiquement
Info := GetPlatformInfo;
// Utiliser de manière totalement portable !
WriteLn('Système d''exploitation: ', Info.GetOSName);
WriteLn('Utilisateur: ', Info.GetUserName);
WriteLn('Ordinateur: ', Info.GetComputerName);
WriteLn('Administrateur: ', Info.IsAdmin);
WriteLn('Mémoire totale: ', Info.GetMemoryInfo div (1024*1024*1024), ' GB');
end.type
{ Classe abstraite }
TNotification = class
public
procedure Show(const Message: string); virtual; abstract;
end;
{ Implémentations concrètes }
TWindowsNotification = class(TNotification)
public
procedure Show(const Message: string); override;
end;
TLinuxNotification = class(TNotification)
public
procedure Show(const Message: string); override;
end;
{ Factory qui crée la bonne instance }
function CreateNotification: TNotification;
begin
{$IFDEF WINDOWS}
Result := TWindowsNotification.Create;
{$ELSE}
Result := TLinuxNotification.Create;
{$ENDIF}
end;
// Utilisation
var
Notif: TNotification;
begin
Notif := CreateNotification; // Création automatique du bon type
try
Notif.Show('Message portable!');
finally
Notif.Free;
end;
end;type
{ Interface commune pour les services système }
ISystemService = interface
function StartService(const ServiceName: string): Boolean;
function StopService(const ServiceName: string): Boolean;
function IsServiceRunning(const ServiceName: string): Boolean;
end;
{ Adaptateur Windows }
TWindowsServiceAdapter = class(TInterfacedObject, ISystemService)
private
// Utilise l'API Windows Service Manager
public
function StartService(const ServiceName: string): Boolean;
function StopService(const ServiceName: string): Boolean;
function IsServiceRunning(const ServiceName: string): Boolean;
end;
{ Adaptateur Linux }
TLinuxServiceAdapter = class(TInterfacedObject, ISystemService)
private
// Utilise systemctl ou service command
public
function StartService(const ServiceName: string): Boolean;
function StopService(const ServiceName: string): Boolean;
function IsServiceRunning(const ServiceName: string): Boolean;
end;type
{ Stratégie de sauvegarde de configuration }
IConfigStrategy = interface
procedure SaveConfig(const Key, Value: string);
function LoadConfig(const Key: string): string;
end;
{ Stratégie Windows : Registre }
TRegistryStrategy = class(TInterfacedObject, IConfigStrategy)
public
procedure SaveConfig(const Key, Value: string);
function LoadConfig(const Key: string): string;
end;
{ Stratégie Linux : Fichier INI }
TIniFileStrategy = class(TInterfacedObject, IConfigStrategy)
private
FFileName: string;
public
constructor Create;
procedure SaveConfig(const Key, Value: string);
function LoadConfig(const Key: string): string;
end;
{ Contexte qui utilise la stratégie }
TConfiguration = class
private
FStrategy: IConfigStrategy;
public
constructor Create;
procedure SetValue(const Key, Value: string);
function GetValue(const Key: string): string;
end;
constructor TConfiguration.Create;
begin
{$IFDEF WINDOWS}
FStrategy := TRegistryStrategy.Create;
{$ELSE}
FStrategy := TIniFileStrategy.Create;
{$ENDIF}
end;// MAUVAIS - Hardcodé
const
CONFIG_PATH = 'C:\Program Files\MyApp\';
// BON - Portable
function GetConfigPath: string;
begin
Result := IncludeTrailingPathDelimiter(GetAppConfigDir(False));
end;// MAUVAIS - API Windows directe
{$IFDEF WINDOWS}
var
StartupInfo: TStartupInfo;
ProcessInfo: TProcessInformation;
begin
CreateProcess(nil, 'notepad.exe', nil, nil, False, 0, nil, nil,
StartupInfo, ProcessInfo);
{$ENDIF}
// BON - Utilisation de TProcess (portable)
var
Process: TProcess;
begin
Process := TProcess.Create(nil);
try
Process.Executable := 'notepad.exe'; // ou 'gedit' sur Linux
Process.Execute;
finally
Process.Free;
end;
end;unit PlatformConsts;
interface
const
{$IFDEF WINDOWS}
PathSeparator = '\';
ExecutableExt = '.exe';
LibraryExt = '.dll';
{$ELSE}
PathSeparator = '/';
ExecutableExt = '';
LibraryExt = '.so';
{$ENDIF}
{$IFDEF WINDOWS}
DefaultEditor = 'notepad.exe';
{$ELSE}
DefaultEditor = 'gedit';
{$ENDIF}
implementation
end.{ Ouvre un fichier avec l'application par défaut
Windows: Utilise ShellExecute
Linux: Utilise xdg-open
macOS: Utilise open command
@param FileName Le fichier à ouvrir
@returns True si succès }
function OpenDocument(const FileName: string): Boolean;
begin
{$IFDEF WINDOWS}
Result := ShellExecute(0, 'open', PChar(FileName), nil, nil,
SW_SHOWNORMAL) > 32;
{$ENDIF}
{$IFDEF UNIX}
with TProcess.Create(nil) do
try
Executable := 'xdg-open';
Parameters.Add(FileName);
Options := [poNoConsole];
Execute;
Result := True;
finally
Free;
end;
{$ENDIF}
end;// ERREUR - Threads non initialisés sur Unix
program MonProgramme;
uses Classes;
// CORRECT
program MonProgramme;
uses
{$IFDEF UNIX}cthreads,{$ENDIF} // DOIT être en premier !
Classes;// ERREUR - Suppose que le slash est toujours '\'
FileName := ExtractFilePath(ParamStr(0)) + 'config\settings.ini';
// CORRECT - Utilise PathDelim
FileName := ExtractFilePath(ParamStr(0)) + 'config' + PathDelim + 'settings.ini';
// ENCORE MIEUX - Utilise les fonctions de chemin
FileName := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) +
'config' + PathDelim + 'settings.ini';// PROBLÈME - L'encodage par défaut varie
var
F: TextFile;
begin
AssignFile(F, 'data.txt');
Rewrite(F);
WriteLn(F, 'Données avec accents: é à ü');
CloseFile(F);
// SOLUTION - Spécifier l'encodage explicitement
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.Text := 'Données avec accents: é à ü';
SL.SaveToFile('data.txt', TEncoding.UTF8); // Force UTF-8
finally
SL.Free;
end;
end;// ERREUR - Chemin Windows uniquement
const
SQLITE_DLL = 'C:\Program Files\SQLite\sqlite3.dll';
// CORRECT - Recherche portable
function GetSQLiteLibrary: string;
begin
{$IFDEF WINDOWS}
Result := 'sqlite3.dll';
// Windows cherche dans le répertoire de l'exe et PATH
{$ELSE}
Result := 'libsqlite3.so.0';
// Linux cherche dans /usr/lib, /usr/local/lib, etc.
{$ENDIF}
// Vérification optionnelle
if not FileExists(Result) then
begin
// Chercher dans des emplacements spécifiques
{$IFDEF WINDOWS}
if FileExists(ExtractFilePath(ParamStr(0)) + Result) then
Result := ExtractFilePath(ParamStr(0)) + Result;
{$ELSE}
if FileExists('/usr/lib/x86_64-linux-gnu/' + Result) then
Result := '/usr/lib/x86_64-linux-gnu/' + Result
else if FileExists('/usr/local/lib/' + Result) then
Result := '/usr/local/lib/' + Result;
{$ENDIF}
end;
end;unit TestPlatform;
interface
uses
{$IFDEF FPC}
fpcunit, testregistry,
{$ENDIF}
Classes, SysUtils;
type
TTestAbstraction = class(TTestCase)
published
procedure TestPathSeparator;
procedure TestTempDirectory;
procedure TestFileOperations;
procedure TestProcessLaunch;
end;
implementation
procedure TTestAbstraction.TestPathSeparator;
var
Path: string;
begin
Path := 'folder' + PathDelim + 'file.txt';
{$IFDEF WINDOWS}
AssertEquals('folder\file.txt', Path);
{$ELSE}
AssertEquals('folder/file.txt', Path);
{$ENDIF}
end;
procedure TTestAbstraction.TestTempDirectory;
var
TempDir: string;
begin
TempDir := GetTempDir;
// Vérifier que le répertoire existe
AssertTrue('Temp directory exists', DirectoryExists(TempDir));
// Vérifier qu'on peut y écrire
AssertTrue('Can write to temp',
FileCreate(TempDir + 'test.tmp') > 0);
// Nettoyer
DeleteFile(TempDir + 'test.tmp');
end;
procedure TTestAbstraction.TestFileOperations;
var
TestFile: string;
Content: TStringList;
begin
TestFile := GetTempDir + 'test_file.txt';
Content := TStringList.Create;
try
Content.Add('Line 1');
Content.Add('Line 2');
// Test écriture
Content.SaveToFile(TestFile);
AssertTrue('File created', FileExists(TestFile));
// Test lecture
Content.Clear;
Content.LoadFromFile(TestFile);
AssertEquals('Line count', 2, Content.Count);
AssertEquals('First line', 'Line 1', Content[0]);
// Nettoyage
DeleteFile(TestFile);
AssertFalse('File deleted', FileExists(TestFile));
finally
Content.Free;
end;
end;
procedure TTestAbstraction.TestProcessLaunch;
var
Process: TProcess;
Output: TStringList;
begin
Process := TProcess.Create(nil);
Output := TStringList.Create;
try
{$IFDEF WINDOWS}
Process.Executable := 'cmd.exe';
Process.Parameters.Add('/c');
Process.Parameters.Add('echo test');
{$ELSE}
Process.Executable := 'echo';
Process.Parameters.Add('test');
{$ENDIF}
Process.Options := [poWaitOnExit, poUsePipes];
Process.Execute;
Output.LoadFromStream(Process.Output);
AssertTrue('Process executed', Output.Count > 0);
AssertTrue('Output contains test', Pos('test', Output.Text) > 0);
finally
Process.Free;
Output.Free;
end;
end;
initialization
RegisterTest(TTestAbstraction);
end.Créons un système de notifications qui s'adapte à chaque OS :
unit CrossPlatformNotifications;
interface
type
TNotificationType = (ntInfo, ntWarning, ntError, ntSuccess);
TNotificationManager = class
private
class var FInstance: TNotificationManager;
public
class function Instance: TNotificationManager;
procedure ShowNotification(const Title, Message: string;
NotifType: TNotificationType);
procedure ShowBalloonTip(const Title, Message: string;
TimeoutMS: Integer = 3000);
function SupportsNativeNotifications: Boolean;
end;
implementation
uses
{$IFDEF WINDOWS}
Windows, ShellAPI, CommCtrl,
{$ENDIF}
{$IFDEF UNIX}
Process,
{$ENDIF}
Forms, Controls, ExtCtrls, SysUtils;
class function TNotificationManager.Instance: TNotificationManager;
begin
if not Assigned(FInstance) then
FInstance := TNotificationManager.Create;
Result := FInstance;
end;
function TNotificationManager.SupportsNativeNotifications: Boolean;
begin
{$IFDEF WINDOWS}
// Windows 10+ supporte les notifications modernes
Result := (Win32MajorVersion >= 10);
{$ENDIF}
{$IFDEF UNIX}
// Vérifier si notify-send est disponible
Result := FileExists('/usr/bin/notify-send');
{$ENDIF}
end;
procedure TNotificationManager.ShowNotification(const Title, Message: string;
NotifType: TNotificationType);
var
IconName: string;
{$IFDEF UNIX}
Process: TProcess;
{$ENDIF}
begin
{$IFDEF WINDOWS}
// Sur Windows, utiliser MessageBox avec l'icône appropriée
case NotifType of
ntInfo: MessageBox(0, PChar(Message), PChar(Title), MB_ICONINFORMATION);
ntWarning: MessageBox(0, PChar(Message), PChar(Title), MB_ICONWARNING);
ntError: MessageBox(0, PChar(Message), PChar(Title), MB_ICONERROR);
ntSuccess: MessageBox(0, PChar(Message), PChar(Title), MB_ICONINFORMATION);
end;
{$ENDIF}
{$IFDEF UNIX}
// Sur Linux, utiliser notify-send si disponible
if FileExists('/usr/bin/notify-send') then
begin
case NotifType of
ntInfo: IconName := 'dialog-information';
ntWarning: IconName := 'dialog-warning';
ntError: IconName := 'dialog-error';
ntSuccess: IconName := 'emblem-default';
end;
Process := TProcess.Create(nil);
try
Process.Executable := 'notify-send';
Process.Parameters.Add('-i');
Process.Parameters.Add(IconName);
Process.Parameters.Add(Title);
Process.Parameters.Add(Message);
Process.Options := [poNoConsole];
Process.Execute;
finally
Process.Free;
end;
end
else
begin
// Fallback : utiliser une simple boîte de dialogue
Application.MessageBox(PChar(Message), PChar(Title), MB_OK);
end;
{$ENDIF}
end;
procedure TNotificationManager.ShowBalloonTip(const Title, Message: string;
TimeoutMS: Integer);
{$IFDEF WINDOWS}
var
NotifyIconData: TNotifyIconData;
TrayIcon: TTrayIcon;
{$ENDIF}
{$IFDEF UNIX}
var
Process: TProcess;
{$ENDIF}
begin
{$IFDEF WINDOWS}
// Créer une icône système temporaire pour la notification
TrayIcon := TTrayIcon.Create(nil);
try
TrayIcon.Visible := True;
FillChar(NotifyIconData, SizeOf(NotifyIconData), 0);
NotifyIconData.cbSize := SizeOf(NotifyIconData);
NotifyIconData.hWnd := Application.MainForm.Handle;
NotifyIconData.uID := 1;
NotifyIconData.uFlags := NIF_INFO;
NotifyIconData.dwInfoFlags := NIIF_INFO;
StrPCopy(NotifyIconData.szInfoTitle, Title);
StrPCopy(NotifyIconData.szInfo, Message);
NotifyIconData.uTimeout := TimeoutMS;
Shell_NotifyIcon(NIM_MODIFY, @NotifyIconData);
Sleep(TimeoutMS);
finally
TrayIcon.Free;
end;
{$ENDIF}
{$IFDEF UNIX}
// Sur Linux, notify-send avec timeout
if FileExists('/usr/bin/notify-send') then
begin
Process := TProcess.Create(nil);
try
Process.Executable := 'notify-send';
Process.Parameters.Add('-t');
Process.Parameters.Add(IntToStr(TimeoutMS));
Process.Parameters.Add(Title);
Process.Parameters.Add(Message);
Process.Options := [poNoConsole];
Process.Execute;
finally
Process.Free;
end;
end;
{$ENDIF}
end;
finalization
FreeAndNil(TNotificationManager.FInstance);
end.unit CrossPlatformPermissions;
interface
type
TFilePermission = (fpRead, fpWrite, fpExecute);
TFilePermissions = set of TFilePermission;
TPermissionManager = class
public
class function GetFilePermissions(const FileName: string): TFilePermissions;
class function SetFilePermissions(const FileName: string;
Permissions: TFilePermissions): Boolean;
class function IsWritable(const FileName: string): Boolean;
class function IsExecutable(const FileName: string): Boolean;
class function SetExecutable(const FileName: string;
Executable: Boolean = True): Boolean;
end;
implementation
uses
{$IFDEF WINDOWS}
Windows,
{$ENDIF}
{$IFDEF UNIX}
BaseUnix, Unix,
{$ENDIF}
SysUtils;
class function TPermissionManager.GetFilePermissions(
const FileName: string): TFilePermissions;
{$IFDEF WINDOWS}
var
Attrs: DWORD;
{$ENDIF}
{$IFDEF UNIX}
var
StatBuf: Stat;
Mode: mode_t;
{$ENDIF}
begin
Result := [];
if not FileExists(FileName) then
Exit;
{$IFDEF WINDOWS}
// Sur Windows, utiliser les attributs de fichier
Attrs := GetFileAttributes(PChar(FileName));
if Attrs <> INVALID_FILE_ATTRIBUTES then
begin
// Windows : tous les fichiers sont "lisibles" s'ils existent
Include(Result, fpRead);
// Vérifier si le fichier est en lecture seule
if (Attrs and FILE_ATTRIBUTE_READONLY) = 0 then
Include(Result, fpWrite);
// Sur Windows, vérifier l'extension pour "exécutable"
if (LowerCase(ExtractFileExt(FileName)) = '.exe') or
(LowerCase(ExtractFileExt(FileName)) = '.bat') or
(LowerCase(ExtractFileExt(FileName)) = '.cmd') then
Include(Result, fpExecute);
end;
{$ENDIF}
{$IFDEF UNIX}
// Sur Unix/Linux, utiliser les permissions POSIX
if FpStat(PChar(FileName), StatBuf) = 0 then
begin
Mode := StatBuf.st_mode;
// Vérifier les permissions pour l'utilisateur actuel
// (simplifié - devrait vérifier owner/group/other selon l'uid)
if (Mode and S_IRUSR) <> 0 then
Include(Result, fpRead);
if (Mode and S_IWUSR) <> 0 then
Include(Result, fpWrite);
if (Mode and S_IXUSR) <> 0 then
Include(Result, fpExecute);
end;
{$ENDIF}
end;
class function TPermissionManager.SetFilePermissions(const FileName: string;
Permissions: TFilePermissions): Boolean;
{$IFDEF WINDOWS}
var
Attrs: DWORD;
{$ENDIF}
{$IFDEF UNIX}
var
Mode: mode_t;
{$ENDIF}
begin
Result := False;
if not FileExists(FileName) then
Exit;
{$IFDEF WINDOWS}
Attrs := GetFileAttributes(PChar(FileName));
if Attrs <> INVALID_FILE_ATTRIBUTES then
begin
// Ajuster l'attribut lecture seule
if fpWrite in Permissions then
Attrs := Attrs and not FILE_ATTRIBUTE_READONLY
else
Attrs := Attrs or FILE_ATTRIBUTE_READONLY;
Result := SetFileAttributes(PChar(FileName), Attrs);
end;
{$ENDIF}
{$IFDEF UNIX}
Mode := 0;
// Construire le mode Unix à partir des permissions
if fpRead in Permissions then
Mode := Mode or S_IRUSR or S_IRGRP or S_IROTH;
if fpWrite in Permissions then
Mode := Mode or S_IWUSR;
if fpExecute in Permissions then
Mode := Mode or S_IXUSR;
Result := FpChmod(PChar(FileName), Mode) = 0;
{$ENDIF}
end;
class function TPermissionManager.IsWritable(const FileName: string): Boolean;
begin
Result := fpWrite in GetFilePermissions(FileName);
end;
class function TPermissionManager.IsExecutable(const FileName: string): Boolean;
begin
Result := fpExecute in GetFilePermissions(FileName);
end;
class function TPermissionManager.SetExecutable(const FileName: string;
Executable: Boolean): Boolean;
var
Perms: TFilePermissions;
begin
Perms := GetFilePermissions(FileName);
if Executable then
Include(Perms, fpExecute)
else
Exclude(Perms, fpExecute);
Result := SetFilePermissions(FileName, Perms);
end;
end.unit CrossPlatformClipboard;
interface
type
TClipboardManager = class
private
class var FInstance: TClipboardManager;
public
class function Instance: TClipboardManager;
procedure SetText(const Text: string);
function GetText: string;
function HasText: Boolean;
procedure SetHTML(const HTML: string);
function GetHTML: string;
function HasHTML: Boolean;
procedure Clear;
// Formats spéciaux
procedure CopyFile(const FileName: string);
function GetFiles: TStringList;
function HasFiles: Boolean;
end;
implementation
uses
Clipbrd, Classes, SysUtils
{$IFDEF WINDOWS}
, Windows
{$ENDIF}
{$IFDEF UNIX}
, LCLType, LCLIntf
{$ENDIF};
class function TClipboardManager.Instance: TClipboardManager;
begin
if not Assigned(FInstance) then
FInstance := TClipboardManager.Create;
Result := FInstance;
end;
procedure TClipboardManager.SetText(const Text: string);
begin
Clipboard.AsText := Text;
end;
function TClipboardManager.GetText: string;
begin
Result := Clipboard.AsText;
end;
function TClipboardManager.HasText: Boolean;
begin
Result := Clipboard.HasFormat(CF_TEXT);
end;
procedure TClipboardManager.SetHTML(const HTML: string);
{$IFDEF WINDOWS}
const
CF_HTML_FORMAT = 'HTML Format';
var
HTMLFormat: UINT;
HTMLData: string;
DataHandle: HGLOBAL;
DataPtr: Pointer;
{$ENDIF}
begin
{$IFDEF WINDOWS}
// Format HTML pour Windows
HTMLData := 'Version:0.9' + #13#10 +
'StartHTML:00000097' + #13#10 +
'EndHTML:' + Format('%.8d', [97 + Length(HTML)]) + #13#10 +
'StartFragment:00000097' + #13#10 +
'EndFragment:' + Format('%.8d', [97 + Length(HTML)]) + #13#10 +
HTML;
HTMLFormat := RegisterClipboardFormat(CF_HTML_FORMAT);
Clipboard.Open;
try
DataHandle := GlobalAlloc(GMEM_MOVEABLE or GMEM_ZEROINIT,
Length(HTMLData) + 1);
if DataHandle <> 0 then
begin
DataPtr := GlobalLock(DataHandle);
if DataPtr <> nil then
begin
Move(HTMLData[1], DataPtr^, Length(HTMLData));
GlobalUnlock(DataHandle);
SetClipboardData(HTMLFormat, DataHandle);
end;
end;
finally
Clipboard.Close;
end;
{$ENDIF}
{$IFDEF UNIX}
// Sur Linux, utiliser le format MIME HTML
// Note: Nécessite une implémentation plus complexe avec GTK/Qt
// Pour simplifier, on met juste le texte
Clipboard.AsText := HTML;
{$ENDIF}
end;
function TClipboardManager.GetHTML: string;
begin
// Implémentation simplifiée
Result := Clipboard.AsText;
end;
function TClipboardManager.HasHTML: Boolean;
begin
{$IFDEF WINDOWS}
Result := Clipboard.HasFormat(RegisterClipboardFormat('HTML Format'));
{$ELSE}
Result := HasText; // Simplification pour Linux
{$ENDIF}
end;
procedure TClipboardManager.Clear;
begin
Clipboard.Clear;
end;
procedure TClipboardManager.CopyFile(const FileName: string);
var
Files: TStringList;
begin
Files := TStringList.Create;
try
Files.Add(FileName);
{$IFDEF WINDOWS}
// Format CF_HDROP pour Windows
Clipboard.Open;
try
// Implémentation du format CF_HDROP
// (simplifié - nécessite plus de code pour être complet)
Clipboard.AsText := FileName;
finally
Clipboard.Close;
end;
{$ENDIF}
{$IFDEF UNIX}
// Format URI pour Linux
Clipboard.AsText := 'file://' + FileName;
{$ENDIF}
finally
Files.Free;
end;
end;
function TClipboardManager.GetFiles: TStringList;
begin
Result := TStringList.Create;
// Implémentation simplifiée
if HasText then
begin
if Pos('file://', Clipboard.AsText) = 1 then
Result.Add(Copy(Clipboard.AsText, 8, MaxInt))
else if FileExists(Clipboard.AsText) then
Result.Add(Clipboard.AsText);
end;
end;
function TClipboardManager.HasFiles: Boolean;
begin
{$IFDEF WINDOWS}
Result := Clipboard.HasFormat(CF_HDROP);
{$ELSE}
Result := HasText and (Pos('file://', Clipboard.AsText) = 1);
{$ENDIF}
end;
finalization
FreeAndNil(TClipboardManager.FInstance);
end.unit DebugHelpers;
interface
procedure DebugLog(const Message: string); overload;
procedure DebugLog(const Format: string; const Args: array of const); overload;
procedure DebugPlatformInfo;
procedure AssertPortable(Condition: Boolean; const Message: string);
implementation
uses
SysUtils
{$IFDEF WINDOWS}
, Windows
{$ENDIF}
{$IFDEF UNIX}
, BaseUnix
{$ENDIF};
procedure DebugLog(const Message: string);
{$IFDEF ENABLE_LOG_FILE}
var
LogFile: TextFile;
{$ENDIF}
begin
{$IFDEF DEBUG}
{$IFDEF WINDOWS}
OutputDebugString(PChar(FormatDateTime('hh:nn:ss.zzz', Now) + ': ' + Message));
{$ENDIF}
{$IFDEF UNIX}
WriteLn(ErrOutput, FormatDateTime('hh:nn:ss.zzz', Now) + ': ' + Message);
{$ENDIF}
// Écrire aussi dans un fichier de log
{$IFDEF ENABLE_LOG_FILE}
AssignFile(LogFile, ChangeFileExt(ParamStr(0), '.log'));
if FileExists(ChangeFileExt(ParamStr(0), '.log')) then
Append(LogFile)
else
Rewrite(LogFile);
try
WriteLn(LogFile, FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', Now) +
' [' + IntToStr(GetCurrentThreadId) + '] ' + Message);
finally
CloseFile(LogFile);
end;
{$ENDIF}
{$ENDIF}
end;
procedure DebugLog(const Format: string; const Args: array of const);
begin
DebugLog(SysUtils.Format(Format, Args));
end;
procedure DebugPlatformInfo;
begin
DebugLog('=== Platform Information ===');
{$IFDEF WINDOWS}
DebugLog('Platform: Windows');
DebugLog('Windows Version: %d.%d Build %d',
[Win32MajorVersion, Win32MinorVersion, Win32BuildNumber]);
DebugLog('Platform: %d', [Win32Platform]);
DebugLog('CSD Version: %s', [Win32CSDVersion]);
{$ENDIF}
{$IFDEF UNIX}
DebugLog('Platform: Unix/Linux');
{$IFDEF LINUX}
DebugLog('System: Linux');
{$ENDIF}
{$IFDEF FREEBSD}
DebugLog('System: FreeBSD');
{$ENDIF}
{$IFDEF DARWIN}
DebugLog('System: macOS/Darwin');
{$ENDIF}
{$ENDIF}
// Informations communes
DebugLog('Processor: %s', [{$I %FPCTARGETCPU%}]);
DebugLog('Pointer Size: %d bits', [SizeOf(Pointer) * 8]);
DebugLog('Compiler Version: %s', [{$I %FPCVERSION%}]);
DebugLog('Application: %s', [ParamStr(0)]);
DebugLog('Current Directory: %s', [GetCurrentDir]);
DebugLog('Temp Directory: %s', [GetTempDir]);
DebugLog('User Directory: %s', [GetUserDir]);
DebugLog('PathDelim: "%s"', [PathDelim]);
DebugLog('DirectorySeparator: "%s"', [DirectorySeparator]);
// Détection du type de fin de ligne
if LineEnding = #13#10 then
DebugLog('LineEnding: CRLF (Windows)')
else if LineEnding = #10 then
DebugLog('LineEnding: LF (Unix/Linux)')
else
DebugLog('LineEnding: Other');
DebugLog('=========================');
end;
procedure AssertPortable(Condition: Boolean; const Message: string);
begin
if not Condition then
begin
DebugLog('ASSERTION FAILED: ' + Message);
DebugLog('Platform: ' +
{$IFDEF WINDOWS}'Windows'{$ELSE}'Unix/Linux'{$ENDIF});
DebugLog('Stack trace:');
// Ici on pourrait ajouter une capture de la pile d'appels
raise Exception.CreateFmt('Portable assertion failed: %s on %s',
[Message, {$IFDEF WINDOWS}'Windows'{$ELSE}'Unix/Linux'{$ENDIF}]);
end;
end;
end.program TestDebug;
uses DebugHelpers, SysUtils;
begin
// Afficher les informations de la plateforme
DebugPlatformInfo;
// Logger des messages
DebugLog('Application démarrée');
DebugLog('Utilisateur: %s', [GetEnvironmentVariable('USER')]);
// Vérifier des conditions portables
AssertPortable(DirectoryExists(GetUserDir),
'Le répertoire utilisateur doit exister');
// Test avec condition qui échoue
try
AssertPortable(False, 'Test d''échec volontaire');
except
on E: Exception do
DebugLog('Exception capturée: ' + E.Message);
end;
DebugLog('Application terminée');
end.- L'abstraction est essentielle pour le développement multi-plateforme
- FreePascal/Lazarus offre plusieurs niveaux : RTL → FCL → LCL
- Utilisez toujours les API portables quand elles existent
- Créez vos propres abstractions pour les cas spécifiques
- Testez sur toutes les plateformes cibles régulièrement
- Documentez les différences de comportement entre OS
Avant de considérer votre code comme portable, vérifiez :
- ✓ Aucun chemin hardcodé (C:, /home/, etc.)
- ✓ Utilisation de PathDelim et DirectorySeparator
- ✓ Gestion des différences de casse des systèmes de fichiers
- ✓ Encodage de caractères explicite (UTF-8 recommandé)
- ✓ Tests sur Windows ET Linux
- ✓ Gestion des erreurs spécifiques à chaque OS
- ✓ Documentation des dépendances externes
- ✓ Compilation conditionnelle propre et organisée
- ✓ Pas d'utilisation directe d'API système (sauf si nécessaire)
- ✓ Utilisation des fonctions de la RTL/FCL/LCL
| Aspect | Windows | Linux |
|---|---|---|
| Séparateur de chemin | \ | / |
| Fin de ligne | CRLF (\r\n) | LF (\n) |
| Sensibilité à la casse | Non | Oui |
| Extensions exécutables | .exe, .bat, .cmd | Aucune (permission x) |
| Bibliothèques | .dll | .so |
| Répertoire temp | %TEMP% | /tmp |
| Configuration | Registre | Fichiers texte |
| Services | Windows Services | systemd/init.d |
| Permissions | Attributs simples | rwx pour user/group/other |
- Documentation RTL : https://www.freepascal.org/docs-html/rtl/
- Documentation FCL : https://www.freepascal.org/docs-html/fcl/
- Wiki Lazarus : https://wiki.lazarus.freepascal.org/
- Forum Lazarus : https://forum.lazarus.freepascal.org/
- Exemples de code : Répertoire examples/ de votre installation Lazarus
L'abstraction des API systèmes est la fondation du développement multi-plateforme réussi. En maîtrisant ces concepts et techniques, vous pouvez créer des applications véritablement portables qui exploitent le meilleur de chaque système d'exploitation tout en maintenant une base de code unique et maintenable.
Les clés du succès sont :
- Comprendre les différences fondamentales entre les OS
- Utiliser les abstractions fournies par FreePascal/Lazarus
- Créer ses propres abstractions quand nécessaire
- Tester régulièrement sur toutes les plateformes cibles
- Documenter les comportements spécifiques
Avec ces outils et techniques, vous êtes maintenant prêt à développer des applications professionnelles qui fonctionnent parfaitement sur Windows et Linux !