Teknik Makale

Delphi Linux64 LP64 Tuzakları: Longint, İşaretçiler ve WIN64

Delphi Linux64, LP64 veri modelini izler; yani Longint ve LongWord orada 8 bayt, Windows tarafındaki Delphi'de (Win32 ve Win64) ve Free Pascal'da ise her hedefte 4 bayt kalıyor. PDFlibPas'ı, Delphi için PDF kütüphanesini, dcclinux64'a taşımak bu farkla üç ayrı biçimde çarpışıyor; üstüne bir de işaretçi genişliği tuzağı var: işaretçi boyutlu bir tam sayı seçmek için kullanılan {$IFDEF WIN64} testi, Windows olmayan her 64 bit hedefte sessizce yanlış genişliği seçiyor

Bu tuzakların hiçbiri egzotik değil. Her biri, Windows'un kazara güvenli kıldığı bir alışkanlık: RTL Longint dediği yerde Integer yazmak, bir C yapısını Longint alanlarıyla aynalamak ya da gerçek soru "işaretçi 8 bayt mı?" iken "bu Win64 mü?" diye sormak. Kod bu alışkanlıkları yıllarca cezasız taşıyabilir, çünkü Win32, Win64 ve FPC'de iki yazım aynı anlama gelir. Delphi Linux64'te bu anlam eşitliği bozulur ve durumu ilk fark eden derleyici olur

Aşağıdakilerin hepsi derleyicinin yakaladıklarıyla ilgili. PDFlibrary.pas, tek bir kaynak ağacından dcc32, dcc64 ve dcclinux64 ile derleniyor; burada anlatılanlar, yalnızca Windows alışkanlıklarıyla yazılmış kod ile o üçüncü hedef arasına giren sorunlar

Delphi Linux64'ü hedeflediğinde ne değişir?

Delphi Linux64'te genişliği değişen tam sayı türleri tam olarak Longint ve LongWord; Integer ve Cardinal her Delphi platformunda 4 bayt kalır, NativeInt ise işaretçiyi izler. Windows LLP64'tür: 64 bitte büyüyen tek şey işaretçidir. Delphi'nin 64 bit POSIX hedeflerinde (Linux64 ile 64 bit macOS ve iOS derleyicileri) model LP64'tür, orada C'nin long türü de büyür ve Embarcadero Pascal'daki Longint'i ona uydurmuştur

TürDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord448her hedefte 4
NativeInt / Pointer488hedefin işaretçi genişliği
C long (platformun ABI'sı)448işletim sisteminin ABI'sını izler

Çapraz derleyicili bir kütüphane için tuzak, Free Pascal sütunudur. PDFlibPas zaten FPC üzerinden bir Linux derlemesi dağıtıyor ve FPC, Linux x86-64'te Longint'i 32 bit tutuyor. Yani "Linux'ta çalışıyor" ifadesi doğrudu ama bize Delphi Linux64 hakkında hiçbir şey söylemiyordu. Delphi portu için doğru temel, FPC'nin Windows dışı arka ucu çıktı; ama altındaki tür boyutları farklı

PDFlibPas PDF kütüphanesinde Integer, Longint, NativeInt ve C long genişliklerinin Delphi Win32, Win64, Linux64 ve Free Pascal hedefleri arasındaki matrisi
Delphi Linux64'te yalnızca Longint, LongWord ve C long genişler; FPC bunları her hedefte 4 bayt tutuyor

WIN64 kontrolü Linux'ta işaretçileri neden kırpıyor?

Kısa cevap: WIN64 "64 bit Windows" demektir, "64 bit" değil; ikisini eş anlamlı saymak yanlış tam sayı genişliği seçtirir. PDFlibDither, 24 bitlik bir bitmap'i kanal başına birkaç seviyeye indiren Floyd-Steinberg hata yayma dither'ıdır ve hata satırlarını ham işaretçi aritmetiğiyle gezdirir; bu aritmetiğin arkasındaki işaretçi boyutlu tam sayı, işletim sistemini değil CPU genişliğini izlemek zorundadır. Naif koşullu ifade tam da bunu yanlış yapar:

// Naif: genişliği işletim sistemi seçiyor; Windows dışı her 64 bit hedef Integer alıyor
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// Serpantin tarama, hata satırı işaretçilerini birer terim ilerletir
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: yalnızca düşük 32 biti yeniden yazar
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, sol taraf atanamaz

// Doğrusu: her yerde tek işaretçi boyutlu tür
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: Windows dışında NativeInt, Win64 / Win32'de IntPtr / Integer

Bu koşullu ifadeyle OSPtr, Linux x86-64, macOS x64 ve ARM64 üzerindeki FPC'de 4 baytlık Integer'e çözülür ve Inc(OSPtr(ErrorR), ...) çağrısı, 8 baytlık işaretçinin ilk 4 baytını tam sayı gibi yorumlayıp artırır. Little-endian bir CPU'da bu, adresin düşük yarısıdır. Yüksek yarısına hiç dokunulmaz; hata tam da bu yüzden iyi gizlenir: düşük 32 bit taşana dek sonuç doğrudur, taşındığı anda işaretçi sessizce 4 GiB geriye atlar. FPC, değişken typecast'ını şikayetsiz derler. Delphi ise bir atamanın sol tarafında farklı boyutlu bir türe değişken typecast'ını reddeder; bu yüzden dcclinux64 ilk Inc'de E2064'te durur

Buradaki işe yarayan asimetri tam da bu: typecast'ı reddeden derleyici, öteki derleyicinin sessizce kabul ettiği bir işaretçi genişliği hatasını, kod yeni hedefte tek satır bile çalışmadan önce yakalar. PDFlibPas bu iş için PDFlibTypes'taki TPLIntPtr'ı kullanır; önce OS_WINDOWS'u test eder, ancak ondan sonra Win64 ile Win32'yi ayırır, böylece Windows dışı hedefler her zaman NativeInt alır

Çıkardığımız kural: işaretçi genişliğini asla bir işletim sistemi define'ından tahmin etme. İşaretçi boyutlu bir tam sayıya ihtiyacın varsa NativeInt / NativeUInt kullan (hem Delphi hem FPC'de var) ya da bunlara dayanan bir proje alias'ı kur ve koşullu ifadeyi tamamen at. Delphi kodunda gerçekten genişliğe göre dallanman gerekiyorsa, asıl sorduğun soruya cevap veren define CPU64BITS'tir

PDFlibPas Delphi kodunda işaretçi genişlikli türler için karar akışı: WIN64 define'ı, NativeInt devreye girene kadar Windows dışı 64 bit hedeflerde 4 baytlık Integer seçiyor
WIN64 define'ı işletim sistemini sorar; işaretçi genişlikli türler bu yüzden NativeInt'ten ya da bir CPU64BITS testinden gelmeli

TStream override'ları: RTL imzasını harf harf kopyalayın

Delphi Linux64'te bir TStream alt sınıfı, Read, Write ve SetSize metotlarını RTL'nin bildirdiği gibi Longint parametreleriyle bildirmek ve uygulamak zorunda, çünkü Integer artık aynı tür değil. PDFlibPas'ın Unicode dosya adları için kendi TPDFlibWideFileStream'i var; sınıf bildirimi Longint kullanıyordu. Implementation bölümü ise Integer kullanmış

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;

// Önce: Win32, Win64 ve FPC'de yıllarca kabul edildi; orada Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Sonra: implementation başlığı bildirimi harf harfine tekrarlar
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

Kütüphanenin o güne dek tanıştığı her derleyicide Integer ile Longint aynı 4 baytlık tür olduğu için, uyuşmayan bir başlık uyuşan bir başlıktan ayırt edilemiyordu. Delphi Linux64'te implementation kendi bildirimiyle artık eşleşmiyor ve unit derlenmeyi bırakıyor. Bu, boyunun ima ettiğinden daha çok acıtır, çünkü PDFlibExtra kütüphanenin çoğunun bağımlı olduğu temel bir unit'tir; tek bir uyuşmayan implementation başlığı, unit'lerin çoğunu derlemeden alıkoyar

Aynı akıl yeni kod için ters yönde de işler. Bir RTL metodunu override ettiğinizde parametre türlerini harf harf kopyalayın; iki türün aynı olduğunu "biliyor" olsanız bile. Delphi Linux64, o bilginin geçerliliğini yitirdiği ilk hedef olmaktan başka bir şey değil

Hangi record'lar Longint yerine Integer ister, hangileri Longint tutmalı?

Bir dosya biçimini ya da Windows yapısını aynalayan record, sabit genişlikli türler kullanmalı (Integer, Cardinal, Int32); long ile bildirilmiş bir C yapısını aynalayan record ise Longint / LongWord tutmalı. İki durum kaynakta birbirine benzer ve zıt cevaplar ister, çünkü Windows'un LONG'u her zaman 32 bittir, C'nin long'u ise platformun ABI'sı ne diyorsa odur

LP64'te Delphi record alanları için karar matrisi: diskteki baytlar için sabit genişlikli Integer, PDFlibPas'ta C long bağlamaları için Longint korunuyor
baytları aynalayan record'lar sabit genişlikler alır; yalnızca C long bağlamaları Longint tutar ve tek satırlık bir boyut iddiasıyla desteklenir
Type
  // Uniscribe GOFFSET: iki Windows LONG'u, toplamda her zaman 8 bayt
  TGoffset= Packed Record
    du: Integer;  // Longint'ti: record, Delphi Linux64'te 16 bayta büyüdü
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE: başlık, record verisinden sabit 68 baytlık Move ile dolduruluyor
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // Longint'ti: 36 bayt 72 oldu
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // PDFium FPDF_FILEACCESS: m_FileLen bir C unsigned long; LongWord doğru seçim
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // Windows'ta 4 bayt, LP64'te 8; C tarafıyla aynı
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

EMF vakası, bunun ne kadar sessizce sızdığını gösteriyor. Importer, TPLEMFLogColorSpace içine 68 bayt kopyalar: 20 bayt başlık, 36 bayt CIE endpoint, 12 bayt gamma. LP64'te Longint endpoint'lerle aynı 68 bayt, 72 baytlık bir endpoint dizisine düşer, gamma değerleri hiç yazılmaz ve hiçbir şey hata vermez. Her iki record da yalnızca Windows kod yollarında bulunur ve zaten sabit genişlikli türleri kullanır; çünkü diskteki baytları aynalayan bir record, onu derleyen derleyiciye bağlı olmamalı

PDFium bağlaması karşı örnek. Onun m_FileLen alanı bir C unsigned long'u aynalar; bu, Windows'ta 32 bit, LP64'te 64 bittir ve LongWord ikisinde de bunu tam olarak takip eder. Bir temizlik turunda bunu Cardinal olarak yeniden yazmak, PDFium host'u bir POSIX loader kazandığı anda işi bozar. PDFium host'u yalnızca Windows'tur, yani bu gelecek için bir not. C bağlamalarının çağrı kuralı tarafının da kendi tuzakları var; bunlar PDF Library DLL, ActiveX ve dylib bağlamaları yazısında ele alınıyor

Baytları aynalayan her record için ucuz bir koruma: boyutunu başlangıçta bir kez iddia edin, örneğin bir initialization bölümünde Assert(SizeOf(TGoffset) = 8). Bedeli sıfırdır ve sessiz bir yerleşim değişikliğini, yeni hedefteki ilk çalıştırmada anında hataya çevirir

Koşullu define'ları doğru kurmak: DELPHI_LINUX, Android ve Types

Tür sorunları gün yüzüne çıkmadan önce, Linux'taki Delphi'nin Windows taklidi yapmayı bırakması gerekiyordu. PDFlibDefines.inc, Delphi Linux için hiçbir şey tanımlamıyordu; derleyici doğrudan Windows/VCL varsayılanlarına düşüyordu ve ilk hata, Vcl.Graphics birimini bulamamaktı. Define'lar, Delphi Linux'u FPC'nin Windows dışı hedeflerde kullandığı arka uca yönlendiriyor: çalışma zamanında yüklenen Cairo rendering, fontconfig, OpenSSL ve libcurl

{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}

Bu bloktaki iki ayrıntı gözden kaçırılabilir. ANDROID dışlaması orada duruyor, çünkü Delphi Android hedefleri için de LINUX tanımlar ve bu arka uç Android'i kapsamaz. Ve yeni define, mevcut Delphi macOS dalından (FPC olmayan OS_MAC) bilinçli olarak ayrı tutulmuştur; o dal Macapi unit'lerine bağımlıdır, Linux define'ı ise onun yerine FPC'nin Windows dışı yoluna oturur

Tekrar eden öteki hata, TPoint ve TRect'in bildirimsiz kalmasıydı. Windows'ta bu türler, çoğu unit'e Windows unit'i üzerinden dolaylı yoldan ulaşır. Delphi Linux'ta uses listesinde Windows unit'i yoktur; bu yüzden PDFlibFont ve PDFlibPageTree gibi unit'ler artık DELPHI_LINUX altında Types'ı açıkça listeliyor. Kendi kodunuz, yalnızca Windows System.Types'ı yanında sürüklediği için derleniyorsa, aynı duvara çarpmayı bekleyin. Kazara tek derleyicide çalışan kodun genel deseni, FPC açığa çıkarana kadar kazara çalışan beş Delphi alışkanlığı yazısının konusu

Derleyicinin artık yardım etmediği yer

Yukarıdaki LP64 sorunları, bir derleyicinin bulabileceği türdendir. Bulamayacağı sorunlar — örneğin boyutu yanlış ama hiçbir typecast'tan geçmeyen bir record — ise ancak testler gerçekten hedefte koştuğunda ortaya çıkar; yukarıda anlatılan boyut iddiaları tam da bunun içindir

Sizi dithering koduna getiren şey 1 bitlik çıktıysa, render edilen sayfalar için desteklenen yol başka biridir: renderer dışında GDI halftone down-conversion; bu, PDF sayfalarını 1 bit monokrom bitmap'lere render etme yazısında anlatılıyor

Burada anlatılan çapraz derleyici hazırlığı, Delphi için PDF Library içinde yaşıyor; bugün Windows'ta Delphi ve C++Builder'ı, Linux ve macOS'ta ise headless Free Pascal derlemelerini hedefliyor. Ürün sayfası, desteklenen derleyicileri ve platformları tam listesiyle veriyor