Skip to content

Latest commit

 

History

History
1743 lines (1458 loc) · 42.1 KB

File metadata and controls

1743 lines (1458 loc) · 42.1 KB

🔝 Retour au Sommaire

5.1 Abstraction des API systèmes

Introduction : Qu'est-ce qu'une API système ?

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.

Analogie Simple

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é.

Exemples Concrets d'API Système

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 !

Pourquoi l'Abstraction est Nécessaire

Le Problème Sans Abstraction

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

La Solution : L'Abstraction

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.

Les Niveaux d'Abstraction dans FreePascal/Lazarus

FreePascal/Lazarus offre plusieurs niveaux d'abstraction, du plus bas au plus haut :

Niveau 1 : RTL (Run-Time Library) - Le Plus Bas Niveau Portable

La RTL est la bibliothèque de base de FreePascal qui abstrait les opérations système fondamentales.

Gestion des Fichiers

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.

Gestion des Processus et Threads

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.

Niveau 2 : FCL (Free Component Library) - Abstraction Orientée Objet

La FCL construit sur la RTL en offrant des classes et composants plus sophistiqués.

Exemple avec TFileStream (Gestion de Fichiers Avancée)

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.

TRegistry : Abstraction du Registre Windows / Configuration Linux

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.

Niveau 3 : LCL (Lazarus Component Library) - Interface Graphique

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.

Créer ses Propres Abstractions

Principe de Base

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.

Utilisation de l'Abstraction

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.

Patterns d'Abstraction Courants

1. Factory Pattern pour la Création d'Objets

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;

2. Adapter Pattern pour API Existantes

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;

3. Strategy Pattern pour Comportements Variables

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;

Bonnes Pratiques pour l'Abstraction

1. Utilisez Toujours les Constantes du Système

// MAUVAIS - Hardcodé
const
  CONFIG_PATH = 'C:\Program Files\MyApp\';

// BON - Portable
function GetConfigPath: string;  
begin
  Result := IncludeTrailingPathDelimiter(GetAppConfigDir(False));
end;

2. Préférez les Units Système aux API Directes

// 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;

3. Centralisez les Différences Plateforme

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.

4. Documentez les Comportements Spécifiques

{ 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;

Erreurs Courantes à Éviter

1. Oublier l'Initialisation Spécifique

// ERREUR - Threads non initialisés sur Unix
program MonProgramme;  
uses Classes;

// CORRECT
program MonProgramme;  
uses
  {$IFDEF UNIX}cthreads,{$ENDIF} // DOIT être en premier !
  Classes;

2. Supposer des Valeurs par Défaut

// 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';

3. Ne Pas Gérer les Différences d'Encodage

// 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;

4. Chemins de Bibliothèques Non Portables

// 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;

Tester l'Abstraction

Stratégie de Test Multi-plateforme

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.

Exemples Pratiques d'Abstraction

Exemple 1 : Gestionnaire de Notifications Multi-plateforme

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.

Exemple 2 : Gestion des Droits et Permissions

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.

Exemple 3 : Gestion du Presse-papiers Portable

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.

Guide de Débogage des Abstractions

Techniques de Débogage Cross-Platform

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.

Exemple d'Utilisation du Débogage

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.

Résumé des Points Clés

Ce qu'il Faut Retenir

  1. L'abstraction est essentielle pour le développement multi-plateforme
  2. FreePascal/Lazarus offre plusieurs niveaux : RTL → FCL → LCL
  3. Utilisez toujours les API portables quand elles existent
  4. Créez vos propres abstractions pour les cas spécifiques
  5. Testez sur toutes les plateformes cibles régulièrement
  6. Documentez les différences de comportement entre OS

Checklist de Portabilité

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

Tableau Récapitulatif des Principales Différences

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

Ressources pour Approfondir

Conclusion

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 !

⏭️ Widgetsets détaillés