В Delphi Linux64 действует модель данных LP64, поэтому Longint и LongWord занимают там 8 байт, тогда как Delphi на Windows (Win32 и Win64) и Free Pascal на всех платформах держат их на 4. Перенос PDFlibPas, PDF-библиотеки для Delphi, на dcclinux64 спотыкается об эту разницу тремя разными способами, плюс ловушка с разрядностью указателя: проверка {$IFDEF WIN64}, которой выбирают целый тип размером с указатель, молча выбирает неверную ширину на каждой 64-битной не-Windows платформе
Ни одна из этих ловушек не экзотика. Каждая — привычка, которую Windows делает безопасной по стечению обстоятельств: писать Integer там, где RTL объявляет Longint, зеркалить C-структуру полями Longint или спрашивать «это Win64?», когда настоящий вопрос звучит так: «указатель 8 байт?». Код может таскать такие привычки годами без последствий, потому что на Win32, Win64 и FPC оба написания означают одно и то же. На Delphi Linux64 они перестают означать одно и то же, и первым это замечает компилятор
Дальше речь только о том, что ловит компилятор. PDFlibrary.pas собирается dcc32, dcc64 и dcclinux64 из одного дерева исходников, и именно эти проблемы стоят между привычками «только для Windows» и третьей целевой платформой
Что меняется, когда Delphi собирается под Linux64?
Из целочисленных типов ширину на Delphi Linux64 меняет ровно пара Longint и LongWord; Integer и Cardinal остаются 4-байтовыми на всех платформах Delphi, а NativeInt следует за указателем. Windows — это LLP64, где на 64 битах растут только указатели. 64-битные POSIX-цели Delphi (Linux64 и компиляторы macOS и iOS 64-bit) — это LP64, где растёт и C-тип long, и Embarcadero сделала Pascal-тип Longint следующим за ним
| Тип | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 на каждой платформе |
NativeInt / Pointer | 4 | 8 | 8 | размер указателя платформы |
C-тип long (ABI платформы) | 4 | 4 | 8 | следует за ABI ОС |
Столбец Free Pascal — ловушка для межкомпиляторной библиотеки. PDFlibPas уже поставляет Linux-сборку через FPC, а FPC держит Longint 32-битным на Linux x86-64. То есть «работает под Linux» было правдой и одновременно не говорило о Delphi Linux64 ничего. Не-Windows бэкенд FPC оказался правильным фундаментом для порта под Delphi, но размеры типов под ним другие
Почему проверка WIN64 обрезает указатели на Linux?
Короткий ответ: WIN64 означает «64-битная Windows», а не «64 бита», и считать их синонимами — значит выбрать неверную ширину целого. PDFlibDither, дизеринг с диффузией ошибок Флойда—Стейнберга, сжимающий 24-битный битмап до нескольких уровней на канал, шагает по трём строкам погрешностей сырой адресной арифметикой, и целый тип размером с указатель должен следовать за разрядностью CPU, а не операционной системы. Наивная условная компиляция ошибается:
// Наивно: ширину выбирает ОС, поэтому каждая 64-битная не-Windows платформа получает Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// Серпантинный обход шагает указателями строк погрешностей по одному слагаемому
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm)); // FPC x86-64: перезаписывает только младшие 32 бита
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm)); // dcclinux64: E2064, левой части нельзя присвоить значение
// Правильно: один тип размером с указатель везде
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt вне Windows, IntPtr / Integer на Win64 / Win32
С такой условной компиляцией OSPtr превращается в 4-байтовый Integer на FPC для Linux x86-64, macOS x64 и ARM64, и Inc(OSPtr(ErrorR), ...) переинтерпретирует первые 4 байта 8-байтового указателя как целое и увеличивает их. На CPU с little-endian это младшая половина адреса. Старшая половина не трогается вовсе — вот почему ошибка так хорошо прячется: результат корректен ровно до переноса младших 32 бит, после чего указатель молча отпрыгивает назад на 4 ГБ. FPC компилирует приведение переменной к типу без возражений. Delphi же отказывает в приведении переменной к типу другого размера слева от присваивания, поэтому dcclinux64 спотыкается об E2064 на первом же Inc
В этом полезная асимметрия: компилятор, отвергающий такое приведение, находит ошибку в разрядности указателя, которую другой компилятор молча пропускает, ещё до того, как код выполнит хоть одну строку на новой платформе. PDFlibPas берёт для этого TPLIntPtr из PDFlibTypes: тот сначала проверяет OS_WINDOWS и лишь потом различает Win64 и Win32, так что не-Windows цели всегда получают NativeInt
Правило, которое мы вынесли: никогда не выводить разрядность указателя из дефайна ОС. Нужен целый тип размером с указатель — берите NativeInt / NativeUInt (есть и в Delphi, и в FPC) или проектный алиас на их основе и выкидывайте условную компиляцию целиком. Если ветвление по разрядности в Delphi-коде действительно нужно, дефайн CPU64BITS отвечает именно на тот вопрос, который вы задаёте
Переопределения TStream: копируйте сигнатуру RTL посимвольно
Потомок TStream на Delphi Linux64 обязан объявлять и реализовывать Read, Write и SetSize с параметрами Longint, ровно как их объявляет RTL, потому что Integer больше не тот же тип. У PDFlibPas есть собственный TPDFlibWideFileStream для Unicode-имён файлов, и в объявлении класса стоял Longint. В секции реализации стоял Integer
Type
TPDFlibWideFileStream= Class(TStream)
Protected
Procedure SetSize(NewSize: Longint); Override;
Public
Function Read(Var Buffer; Count: Longint): Longint; Override;
Function Write(Const Buffer; Count: Longint): Longint; Override;
End;
// Раньше: принималось годами на Win32, Win64 и FPC, где Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// Теперь: заголовок реализации в точности повторяет объявление
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
Result:= FileRead(FHandle, Buffer, Count);
If Result= - 1 Then
Result:= 0;
End;
На всех компиляторах, которые знала библиотека, Integer и Longint были одним и тем же 4-байтовым типом, поэтому расходящийся заголовок было не отличить от совпадающего. На Delphi Linux64 реализация перестаёт соответствовать собственному объявлению, и юнит перестаёт собираться. Это ранит сильнее, чем кажется по масштабам: PDFlibExtra — базовый юнит, от которого зависит большая часть библиотеки, так что один несовпавший заголовок реализации оставляет без сборки большинство юнитов
Для нового кода та же логика работает в обратную сторону. Переопределяя метод RTL, копируйте типы его параметров буквально, даже когда «знаете», что два типа совпадают. Delphi Linux64 просто первая платформа, где это знание перестаёт быть верным
Каким записям нужен Integer вместо Longint, а каким — Longint?
Запись, зеркалящая формат файла или структуру Windows, должна использовать типы фиксированной ширины (Integer, Cardinal, Int32), а запись, зеркалящая C-структуру с long, должна оставить Longint / LongWord. В исходнике оба случая выглядят одинаково, а ответы противоположны: Windows-тип LONG всегда 32 бита, а C-тип long — как скажет ABI платформы
Type
// Uniscribe GOFFSET: два Windows LONG, всегда 8 байт в сумме
TGoffset= Packed Record
du: Integer; // было Longint: запись разрослась до 16 байт на Delphi Linux64
dv: Integer;
End;
// EMF LOGCOLORSPACE: заголовок заполняется фиксированным Move на 68 байт из данных записи
TPLEMFLogColorSpace= Packed Record
Signature, Version, Size, ColorType, Intent: Cardinal;
Endpoints: Array[0..8] Of Integer; // было Longint: 36 байт превратились в 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen — это C unsigned long, поэтому LongWord верен
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 байта на Windows, 8 на LP64, как на C-стороне
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
Случай с EMF показывает, насколько тихо это ломается. Импортёр копирует в TPLEMFLogColorSpace 68 байт: 20 байт заголовка, 36 байт конечных точек CIE, 12 байт гаммы. С Longint-endpoints на LP64 те же 68 байт легли бы в массив конечных точек размером 72 байта, значения гаммы остались бы незаписанными, и ничто не подняло бы ошибку. Обе записи сидят на Windows-only путях кода и всё равно используют типы фиксированной ширины: запись, зеркалящая байты на диске, не должна зависеть от того, каким компилятором её собрали
Биндинг PDFium — контрпример. Его m_FileLen зеркалит C-тип unsigned long: 32 бита на Windows и 64 на LP64, и LongWord отслеживает это точно на обеих. Переписать его в Cardinal при чистке — сломать в тот момент, когда у PDFium-хоста появится POSIX-загрузчик. Хост PDFium пока Windows-only, так что это заметка на будущее. У стороны соглашений о вызовах в C-биндингах свои ловушки — о них статья о DLL, ActiveX- и dylib-биндингах PDF Library
Дешёвая страховка для любой записи, зеркалящей байты: один раз при старте проверить её размер, например Assert(SizeOf(TGoffset) = 8) в секции initialization. Ничего не стоит и превращает молчаливое изменение раскладки в немедленный провал на первом же запуске новой платформы
Правильные условные дефайны: DELPHI_LINUX, Android и Types
Прежде чем всплыли проблемы типов, Delphi под Linux пришлось перестать прикидываться Windows. PDFlibDefines.inc не определял для Delphi Linux ничего, компилятор проваливался прямо в Windows/VCL-умолчания, и первой ошибкой был ненайденный Vcl.Graphics. Дефайны ведут Delphi Linux на тот же бэкенд, что FPC использует вне Windows: рендеринг через Cairo, fontconfig, OpenSSL и libcurl, загружаемые в рантайме
{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}
Две детали в этом блоке легко упустить. Исключение ANDROID стоит потому, что Delphi определяет LINUX и для Android-целей, а этот бэкенд Android не покрывает. Новый дефайн намеренно отделён от существующей ветки Delphi macOS (OS_MAC без FPC), которая тянет юниты Macapi; Linux-дефайн строится на не-Windows пути FPC
Другая повторяющаяся ошибка — необъявленные TPoint и TRect. На Windows эти типы доезжают до большинства юнитов косвенно, через юнит Windows. На Delphi Linux юнита Windows в uses нет, поэтому юниты вроде PDFlibFont и PDFlibPageTree теперь перечисляют Types явно под DELPHI_LINUX. Если ваш код собирается на Windows лишь потому, что Windows тащит за собой System.Types, ждите ту же стену. Общий паттерн кода, работающего на одном компиляторе по совпадению, — тема статьи о пяти Delphi-привычках, которые работают случайно, пока их не вскроет FPC
Где компилятор перестаёт помогать
Описанные выше LP64-проблемы компилятор способен найти. Те, что найти не может, — скажем, запись неверного размера, которая ни разу не проходит через приведение типа, — вылезают лишь когда тесты реально гоняются на платформе; именно для этого служат описанные выше проверки размера
Если вы пришли к коду дизеринга ради 1-битного вывода, поддерживаемый путь для отрендеренных страниц другой — даунконверсия полутонов GDI вне рендерера; он описан в статье о рендеринге страниц PDF в 1-битные монохромные битмапы
Описанная здесь межкомпиляторная база живёт в PDF Library for Delphi, которая сегодня целится в Delphi и C++Builder на Windows плюс headless-сборки Free Pascal на Linux и macOS; полный список поддерживаемых компиляторов и платформ — на странице продукта