HotXLSはXE5以降のすべてのDelphiとC++Builder releaseへ1つのObject Pascal codebaseを出荷しています。それを証明するscriptがbuild-All-Lib-TRIAL.cmdです。Win32とWin64の12 Delphi versionに加え、10 C++Builder Win32と9 Win64 package buildをカバーする43 build legがあります。v2.363からv2.374まで、このscriptはcompletionまで一度も実行されず、XE5 legはその間ずっと壊れていました
failureを見た後なら何もsubtleではありませんでした。current compilerがcommentなしに受け入れる5つのconstructが、RAD Studio XE5、build matrixで12.0と名付けられたcompilerではhard errorになります。v2.375.0 releaseで5つすべてを修正し、matrixは43 of 43でgreenへ戻りました。以下では各rejection、old compilerがtype groundsで拒否する2つについてなぜある意味正しいか、そしてより恥ずかしい部分、つまり混乱を診断するために書いたprobe scriptが初回runでfalse passを報告した理由を説明します
XE5 legが誰にも気づかれず腐った理由
XE5 legが腐ったのは、日々のdevelopmentが37.0の4-script setだけを実行していたためです。green local buildは、invokeしていないcompilerについて何も教えません。full matrixは別の遅いscriptで、trial installerがInno Setupでfileを集める前に呼びます。そのためcommit時ではなくpackaging時にexerciseされます。この隙間に12 releaseが入りました
coverage illusionがある場所なので、leg arithmeticを明記する価値があります。DELPHI_TRIAL_VERSIONSは12.0から37.0までの12 versionをenumerateし、12 versionはそれぞれWin32とWin64の2回buildします。CB_TRIAL_WIN32_VERSIONSは10 versionをlistし、CB_TRIAL_WIN64_VERSIONSは9だけです。XE5にはC++Builder package projectはありますが、Win64 package startup object c0pkg64.oが付属しないためです。12 plus 12 plus 10 plus 9は43です。4つだけを実行してcodebaseをportableと呼ぶのはcategory errorであり、それが今回の原因です
HotXLSは反対方向から同じ形に襲われたこともあります。uses clauseを通じてreachできる新しいunitが.cbproj file listにない場合、Delphiでは完全にcompileできます。dccがunlisted unitをpackageへimplicitにpullし、せいぜいW1033 hintを出すだけだからです。C++Builderは<DelphiCompile>に書かれたunitだけについて.objを出力するため、同じcodeがilink stageでunresolved externalにより死にます。一方のtoolchainが隠すものを他方が捕捉します。代表的なcompilerを信じずmatrixを実行する理由はこれだけです
old Win32 compilerがrejectするhard type cast
5つのrejectionのうち2つは、服装が違うだけで同じbugです。floating-point expressionではなくvariableへ適用すべきhard type castです。Win32ではolder compilerがx87 stackを通してarithmeticをevaluateするため、Doubleを含むadditionは80-bit excess precisionで処理され、static typeは10-byteのExtendedになります。10 byteを8-byteのTDateTimeへcastするのはlegalなtypecastではなく、compilerはE2089 Invalid typecastと言います
腹立たしい細部はvariable formなら問題ないことです。TDateTime(Serial)はmatrix内のすべてのversionでcompileします。Serialはすでに8 byteで、castがsize-preservingだからです。そこへ何かを足すとexpressionが内部でwideになります。fixはwider castやconditional defineではなくcastをやめることです。implicitなreal-to-real assignmentがcompilerごとに正しくconvertし、codeが実際に意味するものを表します
// XE5(Win32)ではrejectされる。各additionが10-byteの
// Extendedとしてevaluateされ、10-to-8 narrowing castがE2089をraiseする
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // additionがなく、これは受け入れられる
// version-safe:real-to-real assignmentにconversionを任せる
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// cell value packerにも同じ種類のrejection:integerへのhard Double cast
// 代わりにdivideする。operatorがすでにrealを返す
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portable
;
Serial < 60 branchは1900 leap-year fictionであり、off-by-oneではありません。serial 60はExcelに存在しない1900-02-29なので、DecodeDateが見る前のserialがそれ未満ならextra dayが必要です。portability workでこの種のlogicを静かに変えてはいけません。そのためsafe editはcastを除くだけでarithmeticをそのまま残します
nilがprocedural argumentになると壊れるもの
procedural typeが期待される場所にbare nilを渡すと、older compilerではoverload resolution中にbindできません。HotXLSのcall siteはoverloadされたResolveIndexedColorで、多くのcallerが不要とするTXLSTryResolveSystemColor callbackを受け取ります。newer compilerはnilをprocedural parameterへresolveし、正しいoverloadを選びます。XE5はそうせず、diagnosticはargumentではなくoverload setを指すため、20分を失います
portableな答えはnull callbackにtypeを与えることです。unit-level variableはlanguageによってzero-initializeされるため、initializerなしですでにnilであり、old resolverが欲しがるtype informationを持ちます。unit-level variableが大げさな場所では、typed localへnilをassignしても同じです
var
// nil procedural literalはolder compilerのoverload resolutionでbindしない
// typedでzero-initializedなvariableならbindできる
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// XLSX workbookではtyped localを使った同じfix
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;
これはrealなlanguage-level differenceであり、defineで避けるべきcompiler bugではありません。zero-initialized variableはmatrix内のすべてのversionで正しく、1行のコストなので、ここにconditional compilationはありません。{$IF CompilerVersion}を使うのはrelease間でplatformが本当に異なる場合だけにしてください。このbatchで該当するのはちょうど1回です
protected VCL methodがrelease間で移動する
TPicture.LoadFromStreamはcurrent VCLではpublicですが、HotXLSがsupportするolder versionではprotectedです。そのためdirect callは現在はcompileしても、当時は失敗します。HotXLSはworksheet background image payloadが本当にdecodeできることをvalidateするためにこれを使います。HTML exporterがbyteのembeddingをcommitする前に行うsignature checkです。classic Pascalの答えを使います。同じunitでvisibilityを広げるだけのdescendantを宣言し、call siteではそれを通してcastします
type
// TPicture.LoadFromStreamはsupport対象のolder VCL versionでprotected
// same-unit descendantがそれを公開する
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はここではsafeです。descendantはfieldを追加せず、instantiateされないからです。castが変えるのはcompilerがnameを許す範囲だけです。それでもdeclarationにはcommentを残す価値があります。current IDEだけでbuildするreaderには、pointlessなtypeに見えるためです。background image handlingはcustom VCL grid rendering pathにも再登場し、同じdecode済みpayloadがon-screen sheetへ供給されます
GdiplusStartup token typeが2回変わった
このbatchで本当にconditional compilationが必要なのはGdiplusStartupのvar parameter typeだけです。VCL generation間で、どこでも有効な1つのspellingが残らない形で変わりました。versionごとのprobeで実際の動作を固定しました。12.0から20.0のlegはCardinalだけを受け入れ、21.0と22.0はTHandleまたはULONG_PTRだけを受け入れ、23.0と37.0は両方を受け入れます。release nameで言えば、XE5から10.3 RioまではCardinal、10.4 Sydney以降はTHandleです。12.0から22.0では受け入れ範囲が重ならないため、unconditional declarationは使えません。guardはSydneyであるCompilerVersion >= 34にし、callはWinapi.GDIPAPI.GdiplusStartupとしてfully qualifyします。そうすればunit resolution orderがrange途中の別declarationを選ぶことがありません
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// GDIPAPIのGdiplusStartup var-parameter typeはVCL generationに従う
// RioまではCardinal、Sydney以降はTHandle
{$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;
これはpage image exporterのTIFF branchなので、間違えたときのblast radiusは、cell rangeを1つのimageとしてexportする処理を含むraster export surface全体です。またguardが主張しないことにも注意してください。ULONG_PTRとTHandleは両platformで同じwidthなので、選択は32-bitと64-bitのcorrectnessではなく、declarationがどのidentifierをnameするかの問題です
初回probe runが何も報告しなかった理由
version probeの初回runが何も報告しなかったのは、res=$(...) assignmentがsubshell内で行われ、parentへpropagateしなかったためです。dcc32はsuccess時にexit 0なので、captureすべきsignalはexit codeでした。しかしscriptはそれを1行後には存在しないvariableへcaptureしていました。すべてのlegがemptyになり、compileを何もしていないprobeのように見えました。実際、そうでした
2つ目のfailureはさらに悪く、no answerではなくwrong answerを出しました。probeはErrorに一致するlineを数えてlegをclassifyしていましたが、Delphiはすべてのfatalの先頭にそのwordを付けません。F1026 File not foundはfatalですがmatchしないため、unitをまったくresolveできないprobeがclean passとしてscoreされました。XE5にはWinapi.GDIPOPS.dcuが付属せず、初回probeはそこへ命中しましたがfalse greenになりました。そこから出たruleは狭いですが明確です。compiler probeはproduced artifactまたはcompiler自身のsummary lineで判定し、outputのkeyword grepでは判定してはいけません。stderrのErrorをgrepするのは、もっとも避けたい方向、つまりsuccessを静かに報告する方向へ失敗するheuristicです
10年分のcompilerをsupportする実際のコスト
正直なaccountingでは、ここでのcode changeはtrivialですが、process changeはそうではありません。5つのrejectionのうち4つはversion machineryを追加せず、普通のPascalを書くことで修正しました。castを落とし、castの代わりにdivideし、nilへtypeを与え、accessor classを宣言するだけです。GdiplusStartupだけが{$IF}を獲得しました。XE5からcurrent releaseまでをspanするcodebaseは、最初からhard castとnewest-compiler idiomを蓄積させなければconditional defineの茂みにはなりません
本当にかかるのはbuild timeとdisciplineです。43 legは遅いscriptであり、それがpackaging timeへ流れ、最後にはneverへ流れた理由でもあります。守れる中間案は、iteration用にfast four-script loopを残し、skipできないscheduleでfull matrixを実行することです。failure modeは壊れたbuildとして気づけるものではなく、12 release前からsupport対象IDEが静かにsupport対象でなくなっていることだからです
native componentを出荷する義務もそこから生まれます。HotXLSはObject Pascalだけで、Excel installもCOM dependencyもなくXLS、XLSX、ODSをreadとwriteします。これがlocked-down serverでOffice-free workbook automationを可能にします。同じ性質がcompilerをplatform contract全体にします。matrix内の各versionは、仮定ではなく再検証しなければならないpromiseです
ここで説明したcross-compiler build matrixとversion-safe codeは、DelphiとC++Builder向けHotXLS Delphi Spreadsheet Componentの一部です。XE5からcurrent releaseまでをsupportし、対応する各IDE向けにprebuilt library binaryを提供します