HotXLS는 Windows에서 Free Pascal과 Lazarus로 빌드되며, 이 포트는 Object Pascal 문법과는 아무 상관 없는 네 가지 결정에 달려 있었습니다. 코어를 DELPHIUNICODE 모드로 유지하기, OLE 구조화 스토리지 인터페이스를 손으로 참조 카운팅을 관리하는 CORBA 인터페이스로 선언하기, Win32 AES 오브젝트 파일을 Pascal 구현으로 교체하기, 잘린 ZIP을 완전물로 받아들일 수 있던 inflate 루프 고치기
성숙한 Delphi 라이브러리를 포트해 본 사람이라면 이 작업의 생김새를 압니다. 컴파일러는 첫 패스에서 거의 모든 것을 받아들입니다. 뒤따르는 것은 깨끗하게 컴파일되고 잘못된 결과를 내는 동작 차이의 긴 꼬리인데, 스프레드시트 엔진은 텍스트 인코딩, COM 구조화 스토리지, 압축, 암호학을 하나의 코드 경로에서 만지기 때문에 이에 비정상적으로 노출되어 있습니다
코어는 평범한 DELPHI가 아니라 왜 DELPHIUNICODE를 고집하는가?
수식 엔진이 UTF-16 의미론을 실은 String과 Char에 의존하고, ANSI 대안은 무엇이든 파일에 닿기 전에 문자를 잃기 때문입니다. 코어를 FPC DELPHI 모드로 빌드하는 게 유혹적입니다. 대부분의 포트가 손을 뻗는 호환성 스위치이고 코드는 컴파일되니까요. 그러다 중국어 시트 이름이나 키릴 라벨을 실은 워크북이 계산 경로를 왕복하면, 라이터가 보는 시점에는 문자가 사라져 있고 에러는 어디에도 없습니다
모드는 라이브러리 전체에서 균일하지 않은데, 지저분해서가 아니라 의도입니다. PNG 바이트 디코더와 LCL 오버라이드는 진짜로 ANSI 시그니처가 필요합니다. 이들이 다루는 것은 바이트와 위젯셋이 건네주는 것이기 때문입니다. 이 유닛들은 별도의 LX_FPC_ANSI 스위치를 켭니다. 한 라이브러리에 두 모드는 스멜처럼 들리지만, 대안이 입력을 텍스트로 취급하는 바이트 디코더라는 것을 알면 그렇지 않습니다
나중에 사람들을 잡는 짝 디테일이 하나 있습니다. FPC 런타임에서 DELPHIUNICODE는 TFormatSettings.DecimalSeparator를 WideChar로 만들지 않습니다. 유니코드 소수점 구분자를 실은 입력은 유니코드 문자열 안에서 먼저 ASCII 구분자로 정규화해야 하고, 기대하는 것과 맞지 않는 구분자를 실은 입력은 파서가 인식하지 못한 문자에서 조용히 잘리는 게 아니라 거부되어야 합니다
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // 반드시 맨 먼저: LCL 위젯셋 초기화
SysUtils, lxHandle; // 및 UTF-8 변환 계층
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('input.xls');
Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
Book.SaveToFile('output.xls');
finally
Book.Free;
end;
end.
Interfaces 유닛은 선택 사항이 아니고 맨 먼저 와야 합니다. LCL 위젯셋과 UTF-8 변환 계층을 초기화하는 것이 바로 이것이고, HotXLS는 폰트, 파일 경로, 텍스트가 RTL과 LCL 경계를 넘는 순간 둘 다에 의존합니다. 이것을 건너뛴 콘솔 프로그램은 컴파일되고 모든 비 ASCII 경로에서 오동작합니다. 여기서 성공적인 컴파일이 너무 적은 것을 증명하는 이유이기도 합니다. 실제 폰트 이름과 실제 경로를 실은 실제 문서가 완전한 왕복을 마친 때에만 포트가 동작함이 입증됐습니다
클래스 VMT는 COM vtable이 아닙니다
Free Pascal은 클래스 VMT를 Windows에 COM 인터페이스 vtable로 건네는 것을 허용하지 않습니다. 선언이 Delphi가 받아들이는 것과 똑같아 보일 때조차요. 레이아웃은 잘못된 슬롯으로의 호출을 만들어 내는 방식으로 다르고, 그것은 호출 지점과 무관한 어딘가에서의 크래시로 나타납니다. 구조화 스토리지가 여기서 중요한 이유는 클래식 바이너리 워크북 포맷이 OLE 복합 파일이고, 그것을 읽거나 쓴다는 것은 Windows 스토리지 API가 콜백할 ILockBytes를 구현한다는 뜻이기 때문입니다
동작하는 배치는 COM 슬롯을 명시적으로 선언하고 AddRef와 Release를 손으로 관리하는 CORBA 인터페이스입니다. 이 타입들에 대해 자동 참조 카운팅을 포기하고 수명에 대한 책임을 지는 것을 의미하는데, 하나의 유닛 안에 사는 몇 안 되는 인터페이스를 위한 공정한 트레이드입니다. 그 작업 안의 구체적 함정은 QueryInterface입니다. 인터페이스 포인터를 돌려줘야지 오브젝트 포인터를 돌려주면 안 됩니다. 둘 다 컴파일됩니다. 하나는 첫 머신 워드가 vtable이 아닌 주소를 Windows에게 건넵니다
FPC 전용 선언들은 FPC 소스 디렉터리에서 lxAESBackend.inc와 lxZlibBackend.inc 옆의 lxOleInterfaces.inc에 삽니다. 컴파일러별 선택이 엔진 곳곳에 흩어지는 대신 한곳에 모이도록 하기 위해서입니다. 포맷 자체와 라이브러리가 그것을 항해하는 방법은 Pascal에서 OLE2 복합 파일 읽기에 기술되어 있습니다
같은 패밀리에 속하는 타입 디테일이 하나 더 있습니다. FPC 분기에서 LargeInt는 Int64로 해석되어야 하고, Comp의 컴파일러 분류가 두 툴체인 사이에서 오버로드 해석이 다른 후보를 고를 만큼 다릅니다. 큰 오프셋 동작은 HGLOBAL 스트림이 아니라 파일 스트림으로 테스트하세요. Windows 글로벌 메모리 스트림은 4GiB를 넘는 seek에서 스스로 감싸지므로, 거기서 통과한 테스트는 여러분의 산술에 관해 아무것도 증명하지 않습니다
자기 일관적인 AES 구현이 숨기는 것
Delphi 빌드가 링크하는 Win32 AES 오브젝트 파일은 OMF이고 Free Pascal 링커는 그것을 소비할 수 없으므로, FPC 분기는 Pascal AES 구현을 씁니다. Delphi는 늘 그래 온 오브젝트 파일을 계속 링크하므로 기존 고객을 위한 릴리스 바이너리는 변하지 않습니다
검증 요건이 모든 프로젝트에 가질 만한 부분입니다. 데이터를 암호화하고 같은 구현으로 다시 복호화하는 것은 아무것도 증명하지 않습니다. 키 스케줄이 틀리거나 블록 순서가 틀리거나 체이닝이 틀린 대칭 알고리즘은 완벽하게 자기 일관적이고 자기 출력을 매번 왕복시킵니다. 잡아내는 것은 known-answer 벡터뿐입니다. 키 확장, 블록 순서, CBC 체이닝을 공개된 값과 대조하는 것이죠. 자기 일관적인 잘못된 구현을 배송하면 증상은 고객이 처음으로 Excel에서 그 파일을 여는 순간 나타납니다
압축에는 다른 성격의 결함이 있었습니다. Pascal inflate 백엔드는 압축 입력을 모두 소비한 뒤에도 출력이 남아 있을 수 있으므로, 호출자는 스트림이 끝을 보고할 때까지 계속 호출해야 합니다. 소진된 입력을 스트림 끝으로 취급하면 마지막 블록이 잘립니다. 더 나쁜 것은 손상된 아카이브를 조용히 받아들여지는 것으로 바꾼다는 점인데, 이것이 바로 ZIP end-of-central-directory 레코드 검증의 강화가 막으려 존재하는 실패 양상입니다. 규칙은 이것입니다. 진행 없음 + 미완료는 절대 EOF가 아니라 잘림 에러입니다
실제 시간을 잡아먹는 빌드 시스템 함정 두 가지
LCL 검색 경로는 FPC 패키지 와일드카드 경로보다 앞서야 합니다. 그렇지 않으면 Free Vision의 Menus 유닛이 같은 이름의 LCL 유닛을 가리고, 둘 중 누구에 대해서도 말하지 않는 PPU 체크섬 불일치를 얻게 됩니다. 설치 뒤에 옮겨진 Lazarus 설치는 fpc.cfg에 낡은 경로를 남길 수도 있으므로, 빌드 진입점은 환경이 주는 무엇이든 상속하는 대신 유닛과 바이너리 경로를 명시적으로 지정합니다
두 번째 함정은 Pascal과 무관합니다. LF 줄바꿈으로 작성된 .cmd 배치 파일은 인터프리터 읽기 버퍼 크기를 넘을 때까지 동작하다가, 그 순간부터 call :label이 배치 레이블이 존재하지 않는다고 주장하며 실패하고, 실패는 경계 너머에 앉은 프로그램마다 나타납니다. 배치 스크립트를 재작성하는 모든 도구는 CRLF로 되돌려 써야 합니다. 그리고 lazbuild --build-all은 컴파일 전에 패키지 유닛 출력 디렉터리를 비우므로, 그 디렉터리에 세워 둔 옵션 파일은 읽히기 전에 삭제됩니다. 밖에 두고, @ 경로는 lazbuild가 컴파일러를 패키지 디렉터리에서 호출하므로 패키지 디렉터리 기준으로 해석된다는 것도 기억하세요
// Lazarus 그리드 익스포트: TGridToXLS는 Lazarus 패키지에 실려 나오므로
// 같은 DB 그리드 익스포트 코드가 LCL 애플리케이션에서 동작합니다
var
Exporter: TGridToXLS;
begin
Exporter := TGridToXLS.Create(nil);
try
Exporter.DBGrid := GridOrders;
Exporter.WorksheetName := 'Orders';
Exporter.ExportHeader := True;
Exporter.SetColumnsWidth := True;
Exporter.ExportDBGrid;
Exporter.SaveAs('orders.xls');
finally
Exporter.Free;
end;
end;
컴파일러 경고의 가치
Free Pascal은 Delphi가 보고하지 않는 초기화되지 않은 지역 변수를 보고하며, FPC 빌드를 돌려 본 것이 그 차이를 계산 유닛의 실제 결함 두 개로 바꿨습니다. 한 함수는 사용 전에 할당된 적 없는 카운트 변수를 읽었고, 다른 하나는 그 좌표들을 계산하는 코드가 다른 분기에서 도는 동안 한 분기에서 두 좌표를 사용했습니다. Delphi 아래에서 둘 다 스택이 우연히 담고 있던 것에 따라 동작했는데, 한 머신에서는 재현되고 다른 머신에서는 안 되는 버그의 정의입니다
실무적 결론은 첫 번째 컴파일러를 중심으로 배송하는 제품이라도 두 번째 컴파일러를 루프에 유지할 가치가 있다는 것입니다. FPC 경고 클래스를 주기적으로 훑는 것은 Delphi 코드베이스 위의 값싼 정적 분석 패스이고, 어떤 테스트 스위트도 안정적으로 닿지 않는 부류의 결함을 찾아 냅니다. 이것이 안에 자리한 더 넓은 버전 매트릭스 규율은 크로스 컴파일러 빌드 매트릭스에 기술되어 있습니다
Windows용 Free Pascal과 Lazarus 지원은 Delphi 및 C++Builder 패키지와 함께 Lazarus 패키지로 HotXLS Delphi 스프레드시트 컴포넌트에 실려 나오며, 포크가 아니라 같은 소스 트리에서 빌드됩니다. 그것이 이 작업의 요점입니다. 하나의 엔진, 네 개의 툴체인, 그리고 한 번에 읽을 수 있는 include 파일들에 고립된 컴파일러별 결정들