Техническая статья

Ловушки LP64 в Delphi Linux64: Longint, указатели и WIN64

В 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 Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 на каждой платформе
NativeInt / Pointer488размер указателя платформы
C-тип long (ABI платформы)448следует за ABI ОС

Столбец Free Pascal — ловушка для межкомпиляторной библиотеки. PDFlibPas уже поставляет Linux-сборку через FPC, а FPC держит Longint 32-битным на Linux x86-64. То есть «работает под Linux» было правдой и одновременно не говорило о Delphi Linux64 ничего. Не-Windows бэкенд FPC оказался правильным фундаментом для порта под Delphi, но размеры типов под ним другие

Матрица разрядностей Integer, Longint, NativeInt и C-типа long на платформах Delphi Win32, Win64, Linux64 и Free Pascal в PDF-библиотеке PDFlibPas
на Delphi Linux64 расширяются только Longint, LongWord и C-тип long, а FPC держит их 4-байтовыми на всех платформах

Почему проверка 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 отвечает именно на тот вопрос, который вы задаёте

Схема выбора типов размером с указатель в Delphi-коде PDFlibPas: дефайн WIN64 выбирает 4-байтовый Integer на 64-битных не-Windows платформах, пока не вступает NativeInt
дефайн WIN64 спрашивает об операционной системе, поэтому типы размером с указатель надо брать из NativeInt или проверки 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 платформы

Матрица решений для полей записей Delphi на LP64: Integer фиксированной ширины для байтов на диске, Longint сохраняется для биндингов C long в PDFlibPas
записи, зеркалящие байты, берут фиксированные ширины, и только биндинги C long оставляют Longint — с однострочной проверкой размера
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; полный список поддерживаемых компиляторов и платформ — на странице продукта