Artikel Teknis

Matriks Build Lintas Compiler Delphi: HotXLS Sejak XE5

HotXLS mengirim satu codebase Object Pascal ke setiap release Delphi dan C++Builder mulai XE5, dan build-All-Lib-TRIAL.cmd adalah script yang membuktikannya: 43 build leg, mencakup 12 versi Delphi pada Win32 dan Win64 serta 10 package build C++Builder Win32 dan 9 Win64. Dari v2.363 sampai v2.374 script tersebut tidak pernah dijalankan sampai selesai, dan leg XE5 rusak selama seluruh periode itu

Tidak ada yang halus dari kegagalan tersebut setelah terlihat. Lima construct berbeda yang diterima compiler sekarang tanpa komentar merupakan hard error pada RAD Studio XE5, yang oleh build matrix diberi label 12.0. Release v2.375.0 memperbaiki kelimanya dan matrix kembali hijau dengan hasil 43 dari 43. Berikut setiap penolakan, alasan compiler lama bisa dibilang benar tentang dua penolakan yang berkaitan dengan type, dan bagian yang lebih memalukan: probe script yang ditulis untuk mendiagnosis kekacauan itu melaporkan false pass pada run pertamanya

Mengapa leg XE5 membusuk tanpa diketahui siapa pun?

Leg XE5 membusuk karena development harian hanya menjalankan set empat script 37.0, dan local build hijau tidak mengatakan apa pun tentang compiler yang tidak Anda panggil. Full matrix adalah script terpisah yang lambat, dipanggil trial installer sebelum Inno Setup mengumpulkan file, sehingga script tersebut dijalankan saat packaging, bukan saat commit. Dua belas release muat di celah itu

Aritmetika leg layak dijabarkan karena di situlah ilusi coverage tinggal. DELPHI_TRIAL_VERSIONS mencantumkan 12.0 sampai 37.0 dan masing-masing dari 12 versi itu dibuild dua kali, Win32 dan Win64. CB_TRIAL_WIN32_VERSIONS mencantumkan 10 versi, sedangkan CB_TRIAL_WIN64_VERSIONS hanya 9 karena XE5 memiliki C++Builder package project tetapi tidak mengirim package startup object Win64 c0pkg64.o. Dua belas ditambah dua belas ditambah sepuluh ditambah sembilan adalah 43. Menjalankan empat di antaranya lalu menyebut codebase portable adalah category error, dan itulah error khusus yang memungkinkan hal ini terjadi

HotXLS juga pernah terkena masalah dengan bentuk yang sama dari arah berlawanan. Unit baru yang terjangkau melalui clause uses tetapi tidak ada dalam file list .cbproj dikompilasi sempurna di bawah Delphi, karena dcc secara implisit menarik unit yang tidak terdaftar ke package dan paling buruk mengeluarkan hint W1033. C++Builder hanya mengeluarkan .obj untuk unit yang disebut dalam <DelphiCompile>, sehingga code yang sama mati pada tahap ilink dengan unresolved external. Satu toolchain menyembunyikan hal yang ditangkap toolchain lain. Itulah seluruh alasan untuk menjalankan matrix, bukan mempercayai compiler yang dianggap mewakili

Hard type cast yang ditolak compiler Win32 lama

Dua dari lima penolakan adalah bug yang sama dengan pakaian berbeda: hard type cast diterapkan pada floating-point expression, bukan pada variable. Di Win32, compiler lama mengevaluasi aritmetika melalui x87 stack, sehingga penambahan yang melibatkan Double dibawa dengan excess precision 80-bit dan static type-nya menjadi Extended 10-byte. Casting 10 byte turun ke TDateTime 8 byte bukan typecast legal, dan compiler menyatakannya sebagai E2089 Invalid typecast

Detail yang menjengkelkan adalah bentuk variable baik-baik saja. TDateTime(Serial) dikompilasi pada setiap versi dalam matrix karena Serial sudah berukuran 8 byte dan cast tersebut mempertahankan ukuran. Tambahkan apa pun ke dalamnya dan expression melebar di bawah Anda. Perbaikannya bukan cast yang lebih lebar atau conditional define, melainkan berhenti melakukan cast: implicit real-to-real assignment mengonversi dengan benar pada setiap compiler yang didukung HotXLS, dan menyatakan apa yang sebenarnya dimaksud code

// Ditolak pada XE5 (Win32): setiap penambahan dievaluasi sebagai
// Extended 10-byte, dan narrowing 10-ke-8 mengangkat E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // yang ini diterima: tidak ada penambahan

// Aman lintas versi: biarkan assignment real-to-real melakukan konversi
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Kelas penolakan yang sama pada cell value packer: hard Double cast
// terhadap integer. Lakukan pembagian; operator tersebut sudah menghasilkan real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portable
  ;

Branch Serial < 60 adalah fiksi leap-year 1900, bukan off-by-one: serial 60 adalah 1900-02-29 yang tidak pernah ada menurut Excel, sehingga serial di bawahnya membutuhkan satu hari tambahan sebelum DecodeDate melihatnya. Pekerjaan portabilitas tidak boleh diam-diam mengubah logic semacam itu, dan itulah alasan edit aman di sini menghapus cast tetapi membiarkan aritmetikanya tidak tersentuh

Apa yang rusak ketika nil menjadi procedural argument?

Literal nil tanpa type yang diberikan pada tempat yang mengharapkan procedural type gagal di-bind saat overload resolution pada compiler lama. Call site di HotXLS adalah ResolveIndexedColor, yang overloaded dan menerima callback TXLSTryResolveSystemColor yang tidak dibutuhkan sebagian besar caller. Compiler yang lebih baru me-resolve nil terhadap procedural parameter dan memilih overload yang tepat. XE5 tidak melakukannya, dan diagnostic menunjuk overload set, bukan argument-nya, begitulah dua puluh menit dapat hilang

Jawaban portabelnya adalah memberi type pada null callback. Variable tingkat unit dengan procedural type akan di-zero-initialize oleh language, sehingga sudah bernilai nil tanpa initializer, sekaligus membawa informasi type yang dibutuhkan resolver lama. Ketika variable tingkat unit berlebihan, typed local yang diisi nil memberikan hasil yang sama

var
  // Literal procedural nil tidak dapat di-bind pada overload resolution
  // compiler lama; variable bertype yang zero-initialized dapat melakukannya
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
  NilSystemColorResolver, Resolution);

// Perbaikan yang sama dengan typed local, di XLSX workbook
function TXLSXWorkbook.ResolveIndexedColor(AIndex: Int64;
  ASpace: TXLSIndexedColorSpace;
  out AResolution: TXLSIndexedColorResolution): Boolean;
var
  NoResolver: TXLSTryResolveSystemColor;
begin
  NoResolver := nil;
  Result := ResolveIndexedColor(AIndex, ASpace, xicrGeneral, NoResolver,
    AResolution);
end;

Perhatikan bahwa ini adalah perbedaan tingkat bahasa yang nyata, bukan compiler bug yang layak diatasi dengan define. Variable yang zero-initialized benar pada setiap versi dalam matrix dan hanya memerlukan satu baris, sehingga tidak ada conditional compilation di sini. Gunakan {$IF CompilerVersion} hanya ketika platform memang berbeda antar-release, yang dalam batch ini hanya terjadi sekali

Protected VCL method berpindah di antara release

TPicture.LoadFromStream bersifat public pada VCL saat ini dan protected pada versi lama yang didukung HotXLS, sehingga pemanggilan langsung dikompilasi sekarang tetapi gagal nanti. HotXLS menggunakannya untuk memvalidasi bahwa worksheet background image payload benar-benar ter-decode, sebuah signature check yang berjalan sebelum HTML exporter berkomitmen menanamkan byte. Jawaban Pascal klasik berlaku: deklarasikan descendant dalam unit yang sama murni untuk memperlebar visibility, lalu lakukan cast melaluinya pada call site

type
  // TPicture.LoadFromStream bersifat protected pada versi VCL lama yang
  // didukung library; descendant satu unit mengeksposnya
  TXlsxPictureAccess = class(TPicture);

// ...

Stream.WriteBuffer(AData[1], Length(AData));
Stream.Position := 0;
TXlsxPictureAccess(Picture).LoadFromStream(Stream);
Result := (Picture.Graphic <> nil) and not Picture.Graphic.Empty and
  (Picture.Graphic.Width > 0) and (Picture.Graphic.Height > 0);

Accessor-class trick ini aman karena descendant tidak menambah field dan tidak pernah diinstansiasi; cast hanya mengubah apa yang boleh dinamai oleh compiler. Namun comment pada deklarasi tetap layak dipertahankan, karena pembaca yang hanya pernah build dengan IDE saat ini akan melihat type yang tampak tidak berguna. Background image handling muncul lagi pada custom VCL grid rendering path, tempat payload ter-decode yang sama memberi makan sheet di layar

Type token GdiplusStartup berubah dua kali

Satu-satunya penolakan dalam batch yang benar-benar membutuhkan conditional compilation adalah type parameter var dari GdiplusStartup, yang berubah di berbagai generasi VCL dengan cara yang membuat tidak ada satu spelling yang valid di semua versi. Probing per versi menetapkan perilaku sebenarnya: leg 12.0 sampai 20.0 hanya menerima Cardinal, leg 21.0 dan 22.0 hanya menerima THandle atau ULONG_PTR, dan 23.0 serta 37.0 menerima keduanya. Dalam nama release, itu berarti Cardinal dari XE5 sampai 10.3 Rio dan THandle mulai 10.4 Sydney. Karena dua rentang yang menerima tidak beririsan pada 12.0 sampai 22.0, tidak ada declaration unconditional yang bekerja: guard bergantung pada CompilerVersion >= 34, yaitu Sydney, dan call ditulis fully qualified sebagai Winapi.GDIPAPI.GdiplusStartup agar unit resolution order tidak menggantikan declaration berbeda pada versi di tengah rentang

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // Type var-parameter GdiplusStartup di GDIPAPI mengikuti generasi VCL
  // Cardinal sampai Rio, THandle mulai Sydney
  {$IF CompilerVersion >= 34}
  StartupToken: THandle;
  {$ELSE}
  StartupToken: Cardinal;
  {$IFEND}
  TiffEncoder: TGUID;
begin
  FillChar(StartupInput, SizeOf(StartupInput), 0);
  StartupInput.GdiplusVersion := 1;
  CheckStatus(Winapi.GDIPAPI.GdiplusStartup(StartupToken, @StartupInput,
    nil), 'startup');
  if GetEncoderClsid('image/tiff', TiffEncoder) < 0 then
    raise EInvalidGraphic.Create('GDI+ TIFF encoder is unavailable');
  // ... encode ...
end;

Ini adalah branch TIFF dari page image exporter, sehingga blast radius jika salah adalah seluruh raster export surface, termasuk path yang dijelaskan dalam mengekspor cell range sebagai satu image. Perhatikan juga hal yang tidak diklaim guard: ULONG_PTR dan THandle memiliki lebar sama pada kedua platform, sehingga pilihan ini berkaitan dengan identifier yang disebut declaration, bukan dengan kebenaran 32-bit versus 64-bit

Mengapa probe run pertama tidak melaporkan apa pun?

Version probe tidak melaporkan apa pun pada run pertama karena assignment res=$(...) dilakukan di dalam subshell, tempat assignment tersebut tidak diteruskan ke parent. dcc32 keluar dengan code 0 saat sukses, sehingga exit code merupakan signal yang tepat untuk ditangkap, tetapi script menangkapnya ke variable yang lenyap satu baris kemudian. Setiap leg kembali kosong dan output tampak seperti probe yang tidak mengompilasi apa pun, dan memang itulah yang terjadi

Kegagalan kedua lebih buruk karena menghasilkan jawaban salah, bukan tanpa jawaban. Probe mengklasifikasikan leg dengan menghitung line yang cocok dengan Error, sedangkan Delphi tidak mengawali setiap fatal dengan kata itu. F1026 File not found bersifat fatal dan tidak cocok, sehingga probe yang sama sekali tidak dapat me-resolve unit diberi skor clean pass. XE5 tidak mengirim Winapi.GDIPOPS.dcu, probe pertama tepat mengenai masalah itu, dan hasilnya falsely green. Aturan yang muncul dari pengalaman ini sempit dan layak dinyatakan terang-terangan: nilai compiler probe dari artifact yang dihasilkan atau dari summary line milik compiler sendiri, jangan dari grep output untuk sebuah keyword. Grep stderr untuk Error adalah heuristic yang gagal pada arah yang paling tidak boleh gagal, yaitu melaporkan sukses secara diam-diam

Berapa biaya sebenarnya mendukung compiler selama satu dekade?

Perhitungan jujurnya adalah perubahan code di sini sepele, sedangkan perubahan proses tidak. Empat dari lima penolakan diperbaiki dengan menulis Pascal biasa, bukan menambah version machinery: hapus cast, lakukan division alih-alih cast, beri type pada nil, dan deklarasikan accessor class. Hanya GdiplusStartup yang mendapatkan {$IF}. Codebase yang membentang dari XE5 sampai release saat ini tidak berubah menjadi semak conditional define kecuali Anda membiarkan hard cast dan idiom compiler terbaru menumpuk sejak awal

Biaya sebenarnya adalah build time dan disiplin. Empat puluh tiga leg adalah script yang lambat, tepat karena itu ia bergeser ke waktu packaging lalu tidak pernah dijalankan. Jalan tengah yang dapat dipertanggungjawabkan adalah mempertahankan loop empat script yang cepat untuk iteration dan menjalankan full matrix menurut schedule yang tidak dapat dilewati, karena failure mode-nya bukan build yang rusak dan langsung terlihat, melainkan IDE yang didukung diam-diam berhenti didukung sejak dua belas release lalu

Kewajiban itu adalah sisi lain dari mengirim native component. HotXLS membaca dan menulis XLS, XLSX, dan ODS hanya melalui Object Pascal, tanpa instalasi Excel dan tanpa COM dependency, sehingga Office-free workbook automation dapat dilakukan pada server yang dikunci. Property yang sama berarti compiler adalah seluruh platform contract, sehingga setiap versi dalam matrix merupakan janji yang harus diverifikasi ulang, bukan diasumsikan

Cross-compiler build matrix dan version-safe code yang dibahas di sini tersedia sebagai bagian dari HotXLS Delphi Spreadsheet Component, yang mendukung Delphi dan C++Builder mulai XE5 hingga release terbaru dengan prebuilt library binary untuk setiap IDE yang didukung