Multiplatform Programming Guide/ru

From Free Pascal wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) русский (ru) 中文(中国大陆)‎ (zh_CN)

Большинство LCL приложений работают в кроссплатформенном режиме без каких-либо дополнительных усилий.

Это руководство по написанию кроссплатформенных приложений с использованием Lazarus и Free Pascal. Он будет охватывать необходимые меры предосторожности, чтобы помочь в создании кроссплатформенной программы, готовой к Развертывание вашего приложения.

Введение в мультиплатформенное (кроссплатформенное) программирование

Сколько платформ вам нужно?

Чтобы ответить на этот вопрос, вы должны сначала определить, кто ваши потенциальные пользователи и как ваша программа будет использоваться. Этот вопрос зависит от того, где вы развертываете свое приложение.

Если вы разрабатываете стандартное настольное программное обеспечение в 2014 году, Microsoft Windows может быть самой важной платформой. Обратите внимание, что Mac OS X и/или Linux набирают популярность и могут стать важной целью для вашего приложения.

Популярность различных операционных систем для настольных компьютеров отличается в зависимости от страны, типа используемого программного обеспечения и целевой аудитории; тут нет общего правила. Например, Mac OS X довольно популярна в Северной Америке и Западной Европе, в то время как в Южной Америке компьютеры Mac в основном ограничены работой с видео и звуком.

Для многих контрактных проектов важна только одна платформа. Free Pascal и Lazarus вполне способны писать программы, ориентированные на конкретную платформу. Вы можете, например, получить доступ ко всему API Windows, чтобы написать хорошо интегрированную программу Windows.

Если вы разрабатываете программное обеспечение, которое будет работать на веб-сервере, обычно используется платформа Unix в одном из ее различных вариантов. В этом случае, возможно, только Linux, Solaris, * BSD и другие Unix-системы имеют смысл в качестве целевых платформ, хотя вы можете добавить поддержку Windows для полноты [картины].

Решив любые кросс-платформенные проблемы в своем дизайне, вы можете в значительной степени игнорировать другие платформы, так же, как и при разработке для одной платформы. Однако в какой-то момент вам нужно будет протестировать развертывание и запуск вашей программы на других платформах. Для этого будет полезно иметь неограниченный доступ к машинам с целевыми операционными системами. Если вам не нужны несколько физических компьютеров, попробуйте решения с двойной загрузкой или виртуальные машины, такие как VMware или Parallels.

Кроссплатформенное программирование

Работа с файлами и папками

При работе с файлами и папками важно использовать платформо-независимые разделители путей и [маркеры] конца строки. Вот список объявленных констант в Lazarus, которые будут использоваться при работе с файлами и папками.

  • PathSep, PathSeparator: разделитель пути при добавлении нескольких путей вместе (';', ...)
  • PathDelim, DirectorySeparator: разделитель каталогов для каждой платформы ('/', '\', ...)
  • LineEnding: правильная последовательность символов окончания строки (#13#10 - CRLF, #10 - LF, ...)

Еще одна важная вещь, которую следует отметить, - это чувствительность к регистру [имен файлов и каталогов] файловой системы. В Windows имена файлов обычно не чувствительны к регистру, в то время как они обычно [регистрозависимы] на платформах Linux и BSD. Но если файловая система EXT2, EXT3 и т.д. смонтирована в Windows, она будет чувствительна к регистру. Соответственно, файловая система FAT, смонтированная в Linux, не должна учитывать регистр символов.

Следует обратить особое внимание, что NTFS не чувствительна к регистру при использовании в Windows, но она чувствительна к регистру при монтировании ОС POSIX. Это может вызвать различные проблемы, в том числе потерю файлов, если файлы с одинаковыми именами файлов в разных случаях существуют в разделе NTFS, смонтированном в Windows. Разработчики должны рассмотреть возможность использования пользовательских функций для проверки и предотвращения создания нескольких файлов с одинаковыми именами в NTFS.

Mac OS X по умолчанию использует имена файлов без учета регистра. Это может быть причиной досадных ошибок, поэтому любое переносимое приложение должно постоянно использовать имена файлов.

RTL-функции файлов используют системную кодировку для имен файлов. Под Windows это одна из кодовых страниц Windows, в то время как Linux, BSD и Mac OS X обычно используют UTF-8. Модуль FileUtil [библиотеки] LCL предоставляет файловые функции, которые принимают строки UTF-8, как и остальная часть LCL.

// [функциям] AnsiToUTF8 и UTF8ToAnsi нужен менеджер широких строк (widestringmanager) под Linux, BSD, MacOSX,
// но обычно эти ОС используют UTF-8 в качестве системной кодировки, поэтому [там] менеджер широких строк
// не нужен.
function NeedRTLAnsi: boolean;// true, если системная кодировка не UTF-8
procedure SetNeedRTLAnsi(NewValue: boolean);
function UTF8ToSys(const s: string): string;// как UTF8ToAnsi, но более не зависим от widestringmanager
function SysToUTF8(const s: string): string;// как AnsiToUTF8, но более не зависим от widestringmanager
function UTF8ToConsole(const s: string): string;// преобразовывает строку UTF8 в консольную кодировку (используется Write, WriteLn)

// файловые операции
function FileExistsUTF8(const Filename: string): boolean;
function FileAgeUTF8(const FileName: string): Longint;
function DirectoryExistsUTF8(const Directory: string): Boolean;
function ExpandFileNameUTF8(const FileName: string): string;
function ExpandUNCFileNameUTF8(const FileName: string): string;
function ExtractShortPathNameUTF8(Const FileName : String) : String;
function FindFirstUTF8(const Path: string; Attr: Longint; out Rslt: TSearchRec): Longint;
function FindNextUTF8(var Rslt: TSearchRec): Longint;
procedure FindCloseUTF8(var F: TSearchrec);
function FileSetDateUTF8(const FileName: String; Age: Longint): Longint;
function FileGetAttrUTF8(const FileName: String): Longint;
function FileSetAttrUTF8(const Filename: String; Attr: longint): Longint;
function DeleteFileUTF8(const FileName: String): Boolean;
function RenameFileUTF8(const OldName, NewName: String): Boolean;
function FileSearchUTF8(const Name, DirList : String): String;
function FileIsReadOnlyUTF8(const FileName: String): Boolean;
function GetCurrentDirUTF8: String;
function SetCurrentDirUTF8(const NewDir: String): Boolean;
function CreateDirUTF8(const NewDir: String): Boolean;
function RemoveDirUTF8(const Dir: String): Boolean;
function ForceDirectoriesUTF8(const Dir: string): Boolean;

// окружение
function ParamStrUTF8(Param: Integer): string;
function GetEnvironmentStringUTF8(Index: Integer): string;
function GetEnvironmentVariableUTF8(const EnvVar: string): String;
function GetAppConfigDirUTF8(Global: Boolean): string;

// другое
function SysErrorMessageUTF8(ErrorCode: Integer): String;



Прим.перев.: после появления поддержки юникода на уровне компилятора (FPC 2.7.1) вместо AnsiToUTF8, UTF8ToAnsi, SysToUTF8, UTF8ToAnsi для работы с WinAPI рекомендуется использовать функции UTF8ToWinCP и WinCPToUTF8. Подробнее здесь: RTL с кодовой страницей UTF-8 по умолчанию


Пустые имена файлов и двойные разделители путей

Существуют различия в обработке имен файлов/каталогов в Windows по сравнению с Linux/Unix/Unix-подобными системами.

  • Windows позволяет пустые имена файлов. Вот почему FileExistsUTF8 ('..\') проверяет в Windows в родительском каталоге наличие файла без имени.
  • В Linux/Unix/Unix-подобных системах пустой файл сопоставляется с каталогом, а каталоги рассматриваются как файлы. Это означает, что FileExistsUTF8 ('../') в Unix проверяет наличие родительского каталога, что обычно приводит к значению true.

Двойные разделители пути в именах файлов также обрабатываются по-разному:

  • Windows: 'C:\' не то же самое, что 'C:\\'
  • Unix-подобные OS: путь '/usr//' совпадает с '/usr/'. Если '/usr' является каталогом, то даже все три равнозначны.

Это важно при объединении имен файлов. Например:

FullFilename:=FilePath+PathDelim+ShortFilename; // может привести к двум PathDelims, которые дают разные результаты под Windows и Linux
FullFilename:=AppendPathDelim(FilePath)+ShortFilename); // создает только один PathDelim
FullFilename:=TrimFilename(FilePath+PathDelim+ShortFilename); // создает только один PathDelim и делает еще несколько чисток

Функция TrimFilename заменяет разделители двойных путей одиночными и сокращает пути '..'. Например /usr//lib/../src обрезается до /usr/src.

Если вы хотите узнать, существует ли каталог, используйте DirectoryExistsUTF8.

Другая распространенная задача - проверить, существует ли часть пути имени файла. Вы можете получить путь с помощью ExtractFilePath, но он будет содержать разделитель пути.

  • Под Unix-подобной системой вы можете просто использовать FileExistsUTF8 в пути. Например, FileExistsUTF8('/home/user/') вернет true, если каталог /home/user существует.
  • В Windows вы должны использовать функцию DirectoryExistsUTF8, но перед этим вы должны удалить разделитель пути, например, с помощью функции ChompPathDelim.

В Unix-подобных системах корневым каталогом является '/', а использование функции ChompPathDelim создаст пустую строку. Функция DirPathExists работает как функция DirectoryExistsUTF8, но обрезает заданный путь.

Обратите внимание, что Unix/Linux использует символ '~' (тильда) для обозначения домашнего каталога, обычно '/home/jim/' для пользователя с именем jim. Так что '~/myapp/myfile' и '/home/jim/myapp/myfile' идентичны в командной строке и в скриптах. Тем не менее, тильда не будет автоматически расширяться Lazarus'ом. Необходимо использовать ExpandFileNameUTF8('~/myapp/myfile'), чтобы получить полный путь.

Кодировка текста

Текстовые файлы часто кодируются в текущей кодировке системы. В Windows это обычно одна из кодовых страниц Windows, в то время как Linux, BSD и Mac OS X обычно используют UTF-8. Не существует 100%-го правила, чтобы узнать, какую кодировку использует текстовый файл. Модуль LCL lconvencoding имеет функцию для угадывания кодировки:

function GuessEncoding(const s: string): string;
function GetDefaultTextEncoding: string;

И он содержит функции для преобразования из одной кодировки в другую:

function ConvertEncoding(const s, FromEncoding, ToEncoding: string): string;

function UTF8BOMToUTF8(const s: string): string; // UTF8 с BOM
function ISO_8859_1ToUTF8(const s: string): string; // Центральная Европа
function CP1250ToUTF8(const s: string): string; // Центральная Европа
function CP1251ToUTF8(const s: string): string; // кириллица
function CP1252ToUTF8(const s: string): string; // latin 1
...
function UTF8ToUTF8BOM(const s: string): string; // UTF8 с BOM
function UTF8ToISO_8859_1(const s: string): string; // Центральная Европа
function UTF8ToCP1250(const s: string): string; // Центральная Европа
function UTF8ToCP1251(const s: string): string; // кириллица
function UTF8ToCP1252(const s: string): string; // latin 1
...

Например, чтобы загрузить текстовый файл и преобразовать его в UTF-8, вы можете использовать:

var
  sl: TStringList;
  OriginalText: String;
  TextAsUTF8: String;
begin
  sl:=TStringList.Create;
  try
    sl.LoadFromFile('sometext.txt'); // осторожно: это изменяет конец строки на конец строки OCи
    OriginalText:=sl.Text;
    TextAsUTF8:=ConvertEncoding(OriginalText,GuessEncoding(OriginalText),EncodingUTF8);
    ...
  finally
    sl.Free;
  end;
end;

А для сохранения текстового файла в системной кодировке вы можете использовать:

sl.Text:=ConvertEncoding(TextAsUTF8,EncodingUTF8,GetDefaultTextEncoding);
sl.SaveToFile('sometext.txt');

Конфигурационные файлы

Вы можете использовать функцию GetAppConfigDir из модуля SysUtils, чтобы найти подходящее место для хранения файлов конфигурации в другой системе. Функция имеет один параметр, называемый Global. Если [его значение] - True, то возвращаемый каталог является глобальным каталогом, то есть действительным для всех пользователей в системе. Если параметр Global имеет значение false, тогда каталог является специфическим для пользователя, выполняющего программу. В системах, которые не поддерживают многопользовательские среды, эти два каталога могут быть одинаковыми.

Существует также [функция] GetAppConfigFile, который возвращает соответствующее имя для файла конфигурации приложения. Вы можете использовать это так:

ConfigFilePath := GetAppConfigFile(False);

Ниже приведены примеры вывода функций пути по умолчанию в различных системах:

program project1;

{$mode objfpc}{$H+}

uses
  SysUtils;

begin
  WriteLn(GetAppConfigDir(True));
  WriteLn(GetAppConfigDir(False));
  WriteLn(GetAppConfigFile(True));
  WriteLn(GetAppConfigFile(False));
end.

Вывод в системе GNU/Linux с FPC 2.2.2. Обратите внимание, что использование True является ошибкой, что уже исправлено в 2.2.3:

/etc/project1/
/home/user/.config/project1/
/etc/project1.cfg
/home/user/.config/project1.cfg

Вы можете заметить, что глобальные файлы конфигурации хранятся в каталоге /etc, а локальные конфигурации хранятся в скрытой папке в домашнем каталоге пользователя. Каталоги, имя которых начинается с точки (.), скрыты в Linux. Вы можете создать каталог в месте, возвращаемом GetAppConfigDir, а затем сохранить там файлы конфигурации.

Note-icon.png

Примечание: Обычные пользователи не могут писать в каталог /etc. Это могут делать только пользователи с правами администратора.

Вывод на последние версии Windows с FPC 3.0.0 +:

C:\ProgramData\project1\
C:\Users\user\AppData\Local\project1\
C:\ProgramData\project1\project1.cfg
C:\Users\user\AppData\Local\project1\project1.cfg

Обратите внимание, что до FPC 2.2.4 функция использовала каталог, в котором приложение должно было хранить глобальные конфигурации в Windows.

Вывод на Windows 98 с FPC 2.2.0:

C:\Program Files\PROJECT1
C:\Windows\Local Settings\Application Data\PROJECT1
C:\Program Files\PROJECT1\PROJECT1.cfg
C:\Windows\Local Settings\Application Data\PROJECT1\PROJECT1.cfg

Вывод на Mac OS X с FPC 2.2.0:

/etc
/Users/user/.config/project1
/etc/project1.cfg
/Users/user/.config/project1/project1.cfg
Note-icon.png

Примечание: Использование UPX мешает использованию функций GetAppConfigDir и GetAppConfigFile.



Прим.перев.: Очевидно, имеется ввиду библиотека упаковщика исполняемых файлов - UPX (the Ultimate Packer for eXecutables)



Note-icon.png

Примечание: В Mac OS X файлы конфигурации в большинстве случаев являются файлами предпочтений, которые должны быть файлами XML с окончанием ".plist" и храниться в /Library/Preferences или ~/Library/Preferences с именами, взятыми из поля "Bundle identifier"(Идентификатор пакета) в Info.plist пакета приложения. Использование вызовов Carbon CFPreference ... вероятно, самый простой способ добиться этого. Файлы .config в каталоге User являются нарушением указаний по программированию.

Файлы данных и ресурсов

Очень распространенный вопрос - где хранить файлы данных, которые могут понадобиться приложению, такие как изображения, музыка, файлы XML, файлы базы данных, файлы справки и т.д. К сожалению, нет кроссплатформенной функции, которая бы обеспечивала лучшее место для поиска файлов данных. Решение состоит в том, чтобы реализовать по-разному на каждой платформе, используя [директивы условной компиляции] IFDEF.

Windows

В Windows данные приложения, которые изменяет программа, должны быть помещены не в каталог приложения (например, C:\Program Files\), а в определенном месте (см., например, Как написать приложение для Windows XP, в разделе "Classify Application Data"). Windows Vista и новее активно применяют это (пользователи имеют доступ на запись в эти каталоги только при использовании повышения [прав] или отключения UAC), но используют механизм перенаправления папок для поддержки старых, неправильно запрограммированных приложений. Просто чтение, а не запись данных из каталогов приложений все равно будет работать.

Короче говоря: используйте такую папку:

    OpDirLocal:= GetEnvironmentVariableUTF8('appdata')+'\MyAppName';


См. Советы по программированию Windows: получение специальных папок (Мои документы, Рабочий стол, локальные данные приложения и т.д.).

Unix/Linux

В большинстве Unix-систем (таких как Linux, FreeBSD, OpenBSD, Solaris) файлы данных приложения расположены в фиксированном месте, которое может выглядеть примерно так: /usr/share/app_name или /opt/app_name.

Данные приложения, которые должны быть записаны приложением, часто хранятся в таких местах, как /var/<programname>, с установленными соответствующими разрешениями.

Специфичные для пользователя конфигурации/данные для чтения/записи обычно хранятся где-то в домашнем каталоге пользователя (например, в ~/.myfancyprogram).

Как получить путь к этому домашнему каталогу:

    OpDirLocal:= GetEnvironmentVariableUTF8('HOME')+'/.myappname';

OS X

macOS (Mac OS X) является исключением среди UNIX. Приложение публикуется в связке - каталоге с расширением "app", которое обрабатывается файловым менеджером как файл (вы также можете сделать "cd path/myapp.app"). Ваши файлы ресурсов должны находиться внутри пакета. Если пакет - "path/MyApp.app", то:

  • исполняемый файл - "path/MyApp.app/Contents/MacOS/myapp"
  • каталог ресурсов - "path/MyApp.app/Contents/Resources"

Сохраните файлы конфигурации в домашний каталог:

    OpDirLocal:= GetEnvironmentVariableUTF8('HOME')+'/.myappname';

Читайте ресурсы с:

    OpDirRes:= ExtractFileDir(ExtractFileDir(Application.ExeName))+'/Resources';
Warning-icon.png

Предупреждение: никогда не используйте paramstr(0) на любой платформе Unix для определения расположения исполняемого файла, так как это соглашение Dos-Windows-OS/2 и имеет несколько концептуальных проблем, которые не могут быть решены с помощью эмуляции на других платформ. Единственное, что paramstr(0) гарантированно возвращает на платформах Unix, это имя, под которым была запущена программа. Каталог, в котором он находится, и имя фактического бинарного файла (в случае, если он был запущен с использованием символической ссылки) не обязательно будут доступны через paramstr(0).

Пример кода

Код: кроссплатформенный путь к ресурсам

32/64 bit

Обнаружение разрядности ОСи во время выполнения

Хотя вы можете контролировать, компилируете ли вы 32- или 64-битную версию с помощью определений компилятора, иногда вы захотите знать, какова разрядность операционной системы. Например, если вы запускаете 32-битную программу Lazarus в 64-битной Windows, вы можете запустить внешнюю программу из каталога Program Files (x86) для 32-битных программ или вы можете захотеть получить различную информацию пользователя: мне это нужно в моей программе установки Лазаруса LazUpdater, чтобы предложить пользователю выбор 32- и 64-битных компиляторов. Код: пример определения Windows x32-x64.

Обнаружение разрядности внешней библиотеки перед ее загрузкой

Если вы хотите загрузить функции из динамической библиотеки в вашу программу, она должна иметь ту же разрядность, что и ваше приложение. В 64-битной Windows ваше приложение может быть 32-битным или 64-битным, и в вашей системе могут быть 32-битные и 64-битные библиотеки. Поэтому вы можете проверить, является ли разрядность dll такой же, как разрядность вашего приложения, прежде чем загружать dll динамически. Вот функция, которая проверяет разрядность dll, (предоставленная на форуме пользователем GetMem):

uses {..., } JwaWindows;

function GetPEType(const APath: WideString): Byte;
const
  PE_UNKNOWN = 0; // если файл не является корректной dll, возвращается 0
 // PE_16BIT   = 1; // не поддерживается этой функцией
  PE_32BIT   = 2;
  PE_64BIT   = 3;
var
  hFile, hFileMap: THandle;
  PMapView: Pointer;
  PIDH: PImageDosHeader;
  PINTH: PImageNtHeaders;
  Base: Pointer;
begin
  Result := PE_UNKNOWN;
 
  hFile := CreateFileW(PWideChar(APath), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if hFile = INVALID_HANDLE_VALUE then
  begin
    CloseHandle(hFile);
    Exit;
  end;
 
  hFileMap  := CreateFileMapping(hFile, nil, PAGE_READONLY, 0, 0, nil);
  if hFileMap = 0 then
  begin
    CloseHandle(hFile);
    CloseHandle(hFileMap);
    Exit;
  end;
 
  PMapView := MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 0);
  if PMapView = nil then
  begin
    CloseHandle(hFile);
    CloseHandle(hFileMap);
    Exit;
  end;
 
  PIDH := PImageDosHeader(PMapView);
  if PIDH^.e_magic <> IMAGE_DOS_SIGNATURE then
  begin
    CloseHandle(hFile);
    CloseHandle(hFileMap);
    UnmapViewOfFile(PMapView);
    Exit;
  end;
 
  Base := PIDH;
  PINTH := PIMAGENTHEADERS(Base + LongWord(PIDH^.e_lfanew));
  if PINTH^.Signature = IMAGE_NT_SIGNATURE then
  begin
    case PINTH^.OptionalHeader.Magic of
      $10b: Result := PE_32BIT;
      $20b: Result := PE_64BIT
    end;
  end;
 
  CloseHandle(hFile);
  CloseHandle(hFileMap);
  UnmapViewOfFile(PMapView);
end;

//Теперь, если вы компилируете свое приложение для 32-битной и 64-битной windows, вы можете проверить, является ли разрядность dll такой же, как у вашего приложения:
function IsCorrectBitness(const APath: WideString): Boolean;
begin  
  {$ifdef CPU32}
    Result := GetPEType(APath) = 2; //приложение скомпилировано как 32-битное, мы спрашиваем, возвращает ли GetPeType 2
  {$endif}
  {$ifdef CPU64}
    Result := GetPEType(APath) = 3; //приложение скомпилировано как 64-битное, мы спрашиваем, возвращает ли GetPeType 3
  {$endif}
end;

Pointer / Integer Typecasts

Pointers under 64bit need 8 bytes instead of 4 on 32bit. The 'Integer' type remains 32bit on all platforms for compatibility. This means you can not typecast pointers into integers and back.

FPC defines two types for this: PtrInt and PtrUInt. PtrInt is a 32bit signed integer on 32 bit platforms and a 64bit signed integer on 64bit platforms. The same for PtrUInt, but unsigned integer instead.

Use for code that should work with Delphi and FPC:

{$IFNDEF FPC}
type
  PtrInt = integer;
  PtrUInt = cardinal;
{$ENDIF}

Replace all integer(SomePointerOrObject) with PtrInt(SomePointerOrObject).

Endianess

Intel platforms are little endian, that means the least significant byte comes first. For example the two bytes of a word $1234 is stored as $34 $12 on little endian systems. On big endian systems like the powerpc the two bytes of a word $1234 are stored as $12 $34. The difference is important when reading files created on other systems.

Use for code that should work on both:

{$IFDEF ENDIAN_BIG}
...
{$ELSE}
...
{$ENDIF}

The opposite is ENDIAN_LITTLE.

The system unit provides plenty of endian converting functions, like SwapEndian, BEtoN (big endian to current endian), LEtoN (little endian to current endian), NtoBE (current endian to big endian) and NtoLE (current endian to little endian).


Libc and other special units

Avoid legacy units like "oldlinux" and "libc" that are not supported outside of linux/i386.

Assembler

Avoid assembler.

Compiler defines

{$ifdef CPU32}
...write here code for 32 bit processors
{$ENDIF}
{$ifdef CPU64}
...write here code for 64 bit processors
{$ENDIF}

Projects, packages and search paths

Lazarus projects and packages are designed for multi platforms. Normally you can simply copy the project and the required packages to another machine and compile them there. You don't need to create one project per platform.

Some advice to achieve this

The compiler creates for every unit a ppu with the same name. This ppu can be used by other projects and packages. The unit source files (e.g. unit1.pas) should not be shared. Simply give the compiler a unit output directory where to create the ppu files. The IDE does that by default, so nothing to do for you here.

Every unit file must be part of one project or package. If a unit file is only used by a single project, add it to this project. Otherwise add it to a package. If you have not yet created a package for your shared units, see here: Creating a package for your common units

Every project and every package should have disjunct directories - they should not share directories. Otherwise you must be an expert in the art of compiler search paths. If you are not an expert or if others who may use your project/package are not experts: do not share directories between projects/packages.

Platform specific units

For example the unit wintricks.pas should only be used under Windows. In the uses section use:

uses
  Classes, SysUtils
  {$IFDEF Windows}
  ,WinTricks
  {$ENDIF}
  ;

If the unit is part of a package, you must also select the unit in the package editor of the package and disable the Use unit checkbox.

See also Platform specific units

Platform specific search paths

When you target several platforms and access the operating system directly, then you will quickly get tired of endless IFDEF constructions. One solution that is used often in the FPC and Lazarus sources is to use include files. Create one sub directory per target. For example win32, linux, bsd, darwin. Put into each directory an include file with the same name. Then use a macro in the include path. The unit can use a normal include directive. An example for one include file for each LCL widget set:

Create one file for each widget set you want to support:

win32/example.inc
gtk/example.inc
gtk2/example.inc
carbon/example.inc

You do not need to add the files to the package or project. Add the include search path $(LCLWidgetType) to the compiler options of your package or project.

In your unit use the directive: {$I example.inc}

Here are some useful macros and common values:

  • LCLWidgetType: win32, gtk, gtk2, qt, carbon, fpgui, nogui
  • TargetOS: linux, win32, win64, wince, freebsd, netbsd, openbsd, darwin (many more)
  • TargetCPU: i386, x86_64, arm, powerpc, sparc
  • SrcOS: win, unix

You can use the $Env() macro to use environment variables.

And of course you can use combinations. For example the LCL uses:

$(LazarusDir)/lcl/units/$(TargetCPU)-$(TargetOS);$(LazarusDir)/lcl/units/$(TargetCPU)-$(TargetOS)/$(LCLWidgetType)

See here the complete list of macros: IDE Macros in paths and filenames

Machine / User specific search paths

For example you have two windows machines stan and oliver. On stan your units are in C:\units and on oliver your units are in D:\path. The units belong to the package SharedStuff which is C:\units\sharedstuff.lpk on stan and D:\path\sharedstuff.lpk on oliver. Once you opened the lpk in the IDE or by lazbuild, the path is automatically stored in its configuration files (packagefiles.xml). When compiling a project that requires the package SharedStuff, the IDE and lazbuild knows where it is. So no configuration is needed.

If you have want to deploy a package over many machine or for all users of a machine (e.g. a pool for students), then you can add a lpl file in the lazarus source directory. See packager/globallinks for examples.

Locale differences

Some functions from Free Pascal, like StrToFloat behave differently depending on the current [locale]]. For example, in the USA the decimal separator is usually ".", but in many European and South American countries it is ",". This can be a problem as sometimes it is desired to have these functions behave in a fixed way, independently from the locale. An example is a file format with decimal points that always needs to be interpreted the same way.

The next sections explain how to do that.


StrToFloat

A new set of format settings which set a fixed decimal separator can be created with the following code:

// in your .lpr project file
uses
...
{$IFDEF UNIX}
clocale 
{ required on Linux/Unix for formatsettings support. Should be one of the first (probably after cthreads?}
{$ENDIF}

and:

// in your code:
var
  FPointSeparator, FCommaSeparator: TFormatSettings;
begin
  // Format settings to convert a string to a float
  FPointSeparator := DefaultFormatSettings;
  FPointSeparator.DecimalSeparator := '.';
  FPointSeparator.ThousandSeparator := '#';// disable the thousand separator
  FCommaSeparator := DefaultFormatSettings;
  FCommaSeparator.DecimalSeparator := ',';
  FCommaSeparator.ThousandSeparator := '#';// disable the thousand separator

Later on you can use this format settings when calling StrToFloat, like this:

// This function works like StrToFloat, but simply tries two possible decimal separator
// This will avoid an exception when the string format doesn't match the locale
function AnSemantico.StringToFloat(AStr: string): Double;
begin
  if Pos('.', AStr) > 0 then Result := StrToFloat(AStr, FPointSeparator)
  else Result := StrToFloat(AStr, FCommaSeparator);
end;

Gtk2 and masking FPU exceptions

Gtk2 library changes the default value of FPU (floating point unit) exception mask. The consequence of this is that some floating point exceptions do not get raised if Gtk2 library is used by the application. That means that, if for example you develop a LCL application on Windows with win32/64 widgetset (which is Windows default) and plan to compile for Linux (where Gtk2 is default widgetset), you should keep this incompatibilities in mind.

After this forum topic and answers on this bug report it became clear that nothing can be done about this, so we must know what actually these differences are.

Therefore, let's do a test:

uses
  ..., math,...

{...}

var
  FPUException: TFPUException;
  FPUExceptionMask: TFPUExceptionMask;
begin
  FPUExceptionMask := GetExceptionMask;
  for FPUException := Low(TFPUException) to High(TFPUException) do begin
    write(FPUException, ' - ');
    if not (FPUException in FPUExceptionMask) then
      write('not ');

    writeln('masked!');
  end;
  readln;
end.

Our simple program will get what FPC default is:


exInvalidOp - not masked!
exDenormalized - masked!
exZeroDivide - not masked!
exOverflow - not masked!
exUnderflow - masked!
exPrecision - masked!

However, with Gtk2, only exOverflow is not masked.

The consequence is that EInvalidOp and EZeroDivide exceptions do not get raised if the application links to Gtk2 library! Normally, dividing non-zero value by zero raises EZeroDivide exception and dividing zero by zero raises EInvalidOp. For example the code like this:

var
  X, A, B: Double;
// ...

try
  X := A / B;
  // code block 1
except   
  // code block 2
end;
// ...

will take different direction when compiled in application with Gtk2 widgetset. On win widgetset, when B equals zero, an exception will get raised (EZeroDivide or EInvalidOp, depending on whether A is zero) and "code block 2" will be executed. On Gtk2 X becomes Infinity, NegInfinity, or NaN and "code block 1" will be executed.

We can think of different ways to overcome this inconsistency. Most of the time you can simply test if B equals zero and don't try the dividing in that case. However, sometimes you will need some different approach. So, take a look at the following examples:

uses
  ..., math,...

//...
var
  X, A, B: Double;
  Ind: Boolean;
// ...
try
  X := A / B;
  Ind := IsInfinite(X) or IsNan(X); // with gtk2, we fall here
except   
  Ind := True; // in windows, we fall here when B equals zero
end;
if Ind then begin
  // code block 2
end else begin
  // code block 1
end;
// ...

Or:

uses
  ..., math,...

//...
var
  X, A, B: Double;
  FPUExceptionMask: TFPUExceptionMask;
// ...

try
  FPUExceptionMask := GetExceptionMask;
  SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]); // unmask
  try
    X := A / B;
  finally
    SetExceptionMask(FPUExceptionMask); // return previous masking immediately, we must not let Gtk2 internals to be called without the mask
  end;
  // code block 1
except   
  // code block 2
end;
// ...

Be cautious, do not do something like this (call LCL with still removed mask):

try
  FPUExceptionMask := GetExceptionMask;
  SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]);
  try
    Edit1.Text := FloatToStr(A / B); // NO! Setting Edit's text goes down to widgetset internals and Gtk2 API must not be called without the mask!
  finally
    SetExceptionMask(FPUExceptionMask);
  end;
  // code block 1
except   
  // code block 2
end;
// ...

But use an auxiliary variable:

try
  FPUExceptionMask := GetExceptionMask;
  SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]);
  try
    X := A / B; // First, we set auxiliary variable X
  finally
    SetExceptionMask(FPUExceptionMask);
  end;
  Edit1.Text := FloatToStr(X); // Now we can set Edit's text.
  // code block 1
except   
  // code block 2
end;
// ...

In all situations, when developing LCL applications, it is most important to know about this and to keep in mind that some floating point operations can go different way with different widgetsets. Then you can think of an appropriate way to workaround this, but this should not go unnoticed.

Issues when moving from Windows to *nix etc

Issues specific to Linux, OSX, Android and other Unixes are described here. Not all subjects may apply to all platforms

On Unix there is no "application directory"

Many programmers are used to call ExtractFilePath(ParamStr(0)) or Application.ExeName to get the location of the executable, and then search for the necessary files for the program execution (Images, XML files, database files, etc) based on the location of the executable. This is wrong on unixes. The string on ParamStr(0) may contain a directory other than the one of the executable, and it also varies between different shell programs (sh, bash, etc).

Even if Application.ExeName could in fact know the directory where the executable is, that file could be a symbolic link, so you could get the directory of the link instead (depending on the Linux kernel version, you either get the directory of the link or of the program binary itself).

To avoid this read the sections about configuration files and data files.

Making do without Windows COM Automation

With Windows, COM Automation is a powerful way not only of manipulating other programs remotely but also for allowing other programs to manipulate your program. With Delphi you can make your program both an COM Automation client and a COM Automation server, meaning it can both manipulate other programs and in turn be manipulated by other programs. For examples, see Using COM Automation to interact with OpenOffice and Microsoft Office.

OSX alternative

Unfortunately, COM Automation isn't available on OS X and Linux. However, you can simulate some of the functionality of COM Automation on OS X using AppleScript.

AppleScript is similar to COM Automation in some ways. For example, you can write scripts that manipulate other programs. Here's a very simple example of AppleScript that starts NeoOffice (the Mac version of OpenOffice.org):

 tell application "NeoOffice"
   launch
 end tell

An app that is designed to be manipulated by AppleScript provides a "dictionary" of classes and commands that can be used with the app, similar to the classes of a Windows Automation server. However, even apps like NeoOffice that don't provide a dictionary will still respond to the commands "launch", "activate" and "quit". AppleScript can be run from the OS X Script Editor or Finder or even converted to an app that you can drop on the dock just like any app. You can also run AppleScript from your program, as in this example:

 fpsystem('myscript.applescript');

This assumes the script is in the indicated file. You can also run scripts on the fly from your app using the OS X OsaScript command:

 fpsystem('osascript -e '#39'tell application "NeoOffice"'#39 +
       ' -e '#39'launch'#39' -e '#39'end tell'#39);
       {Note use of #39 to single-quote the parameters}

However, these examples are just the equivalent of the following Open command:

 fpsystem('open -a NeoOffice');

Similarly, in OS X you can emulate the Windows shell commands to launch a web browser and launch an email client with:

 fpsystem('open -a safari "http://gigaset.com/shc/0,1935,hq_en_0_141387_rArNrNrNrN,00.html"');

and

 fpsystem('open -a mail "mailto:ss4200@invalid.org"');

which assumes, fairly safely, that an OS X system will have the Safari and Mail applications installed. Of course, you should never make assumptions like this, and for the two previous examples, you can in fact just rely on OS X to do the right thing and pick the user's default web browser and email client if you instead use these variations:

 fpsystem('open "http://gigaset.com/shc/0,1935,hq_en_0_141387_rArNrNrNrN,00.html"');

and

 fpsystem('open "mailto:ss4200@invalid.org"');

Do not forget to include the Unix unit in your uses clause if you use fpsystem or shell (interchangeable).

The real power of AppleScript is to manipulate programs remotely to create and open documents and automate other activities. How much you can do with a program depends on how extensive its AppleScript dictionary is (if it has one). For example, Microsoft's Office X programs are not very usable with AppleScript, whereas the newer Office 2004 programs have completely rewritten AppleScript dictionaries that compare in many ways with what's available via the Windows Office Automation servers.

Linux alternatives

While Linux shells support sophisticated command line scripting, the type of scripting is limited to what can be passed to a program on the command line. There is no single, unified way to access a program's internal classes and commands with Linux the way they are via Windows COM Automation and OS X AppleScript. However, individual desktop environments (GNOME/KDE) and application frameworks often provide such methods of interprocess communication. On GNOME see Bonobo Components. KDE has the KParts framework, DCOP. OpenOffice has a platform neutral API for controlling the office remotely (google OpenOffice SDK) - though you would probably have to write glue code in another language that has bindings (such as Python) to use it. In addition, some applications have "server modes" activated by special command-line options that allow them to be controlled from another process. It is also possible (Borland did it with Kylix document browser) to "embed" one top-level X application window into another using XReparentWindow (I think).

As with Windows, many OS X and Linux programs are made up of multiple library files (.dylib and .so extensions). Sometimes these libraries are designed so you can also use them in programs you write. While this can be a way of adding some of the functionality of an external program to your program, it's not really the same as running and manipulating the external program itself. Instead, your program is just linking to and using the external program's library similar to the way it would use any programming library.

Alternatives for Windows API functions

Many Windows programs use the Windows API extensively. In cross-platform applications Win API functions in the Windows unit should not be used, or should be enclosed by a conditional compile (e.g. {$IFDEF MSWINDOWS} ).

Fortunately many of the commonly used Windows API functions are implemented in a multiplatform way in the unit lclintf. This can be a solution for programs which rely heavily on the Windows API, although the best solution is to replace these calls with true cross-platform components from the LCL. You can replace calls to GDI painting functions with calls to a TCanvas object's methods, for example.

Key codes

Fortunately, detecting key codes (e.g. on KeyUp events) is portable: see LCL Key Handling.

Installing your application

See Deploying Your Application.

See Also