Win32의 Free Pascal은 모든 cdecl; external 임포트에 접두사 언더스코어를 자동으로 붙이는 반면, public name은 여러분이 쓴 문자열을 글자 하나까지 그대로 익스포트합니다. HotPDF는 같은 소스 트리에서 이 두 규약을 모두 만족시켜야 합니다. Delphi 빌드가 이미 언더스코어를 손으로 직접 적어 넣은 임포트 선언을 배송하고 있기 때문입니다. 이 비대칭을 잘못 다루면 아무도 작성한 적 없는 심볼 이름을 가리키는 링크 에러가 나옵니다
Delphi 라이브러리를 Free Pascal로 확장하는 일은 보통 이식성 문제로 불리고, Win64에서는 실제로 대체로 그렇습니다. Win32는 다릅니다. 32비트 x86 Windows ABI에는 C 심볼을 어떻게 적는가, 스택을 누가 정리하는가, 번역 단위(translation unit)가 어떤 컴파일러 전용 헬퍼를 가정해도 되는가에 관해 30년 쌓인 관습이 실려 있고, 그 각각은 언어에 대해서는 일치하는 두 Pascal 컴파일러가 오브젝트 파일에 대해서는 여전히 어긋날 수 있는 자리입니다
같은 심볼이 Win64에선 풀리고 Win32에선 실패하는 이유는?
언더스코어 접두사는 Free Pascal이 임포트에는 적용하지만 익스포트에는 적용하지 않는 32비트 관습이기 때문입니다. function deflate(...): Integer; cdecl; external;라고 선언하면 FPC는 Win32 오브젝트 파일에서 _deflate를, Win64에서는 deflate를 찾습니다. C 컴파일러가 내보내는 것과 일치하는 올바른 동작입니다. 함정은 브리지 반대편에 있습니다. public name 'deflate'로 표시된 루틴은 양쪽 타깃 모두에서 접두사 없이 정확히 deflate를 익스포트합니다
여기에 얘기를 구체적으로 만드는 역사적 디테일을 얹으면, Delphi 빌드는 이미 일부 진입점을 이름에 언더스코어를 적어 넣은 채 선언하고 있습니다. 자기 오브젝트 파일에 그것이 들어 있기 때문입니다. 같은 선언을 Win32 FPC에 먹이면 컴파일러는 성실하게 접두사를 한 번 더 붙이고, 링커는 __deflate, 즉 아무도 익스포트하지 않는 심볼을 헤매게 됩니다. 직관적인 수정책인 모든 곳에 언더스코어 하나 추가는 이미 올바르게 적힌 임포트를 깨뜨립니다
동작하는 것은 접두사 상수 하나가 아니라 한 쌍입니다. HPDFFPCZLib과 HPDFFPCCodecStubs는 평범한 C 임포트에는 하나의 접두사를, 이미 Delphi 쪽 접두사를 달고 있는 임포트에는 다른 접두사를 쓰고, Win64에서는 두 상수 모두 비어 있어 기존 링크 이름이 그대로 살아남습니다. 하나 대신 상수 두 개, 그것이 수정의 전부이고, 임포트 규칙과 익스포트 규칙을 분리해 놓고 보면 당연한 얘기입니다
// 접두사는 둘입니다: 평범한 C 임포트와 손으로 적은 Delphi 접두사를 이미
// 달고 있는 임포트는 FPC/Win32에서 서로 다르게 데코레이트됩니다
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
CPrefix = '_'; // cdecl external에는 FPC가 스스로 추가
DelphiCName = ''; // 소스에 이미 언더스코어가 적혀 있음
{$ELSE}
CPrefix = '';
DelphiCName = '';
{$IFEND}
// 익스포트 쪽: 'public name'은 모든 타깃에서 문자 그대로
procedure hpdf_codec_free(P: Pointer); cdecl;
public name 'hpdf_codec_free';
WIN32는 아키텍처를 말해 줄 뿐 ABI를 말해 주지 않습니다
가장 긴 디버깅 꼬리를 자랑하는 조건부 컴파일 실수이며, 담백하게 적어 둘 가치가 있습니다. WIN32와 WIN64는 타깃 아키텍처를 기술할 뿐 어떤 컴파일러 전용 런타임 헬퍼가 존재하는지는 말하지 않습니다. Free Pascal은 해당 Windows 타깃에서 Delphi와 정확히 같은 방식으로 두 심볼을 정의합니다. 따라서 Delphi 런타임 헬퍼를 호출하는 코드를 감싼 {$IFDEF WIN32} 가드는 FPC에서 컴파일되고 링크 시점에 실패합니다
구체적으로, 세 부류의 코드가 이 함정에 빠집니다. System.@_ll 헬퍼로 도달하는 Delphi 64비트 정수 트램펄린, MSVC Win32 어셈블리 지원 루틴, 그리고 그에 딸린 임포트 슬롯은 모두 Delphi 빌드가 링크하는 프리컴파일 C 오브젝트를 섬기기 위해 존재합니다. Free Pascal은 그 오브젝트들을 링크하지 않으므로 그 장치들이 전혀 필요 없고, 참조하는 모든 부분이 사라져야 합니다. 미묘한 점은 선언과 구현을 함께 제외해야 한다는 것입니다. 하나만 제외하면 컴파일러는 아무것과도 매칭되지 않는 식별자에 관해 도움 안 되는 소리를 합니다
떨어져 나오는 규칙은 짧습니다. 질문이 ABI나 런타임 지원에 관한 것이면 컴파일러로 가드하고, 포인터 폭이나 레지스터 개수에 관한 것이면 아키텍처로 가드하세요. 하나가 다른 하나를 대변하게 두지 마세요
선언과 구현을 함께 가드하기
interface 섹션의 조건부 블록은 알아차리지 못한 채 빠지기 쉽고, 그 결과 나오는 에러 메시지는 원인이 아니라 온 잡것들을 가리킵니다. 클래스 인터페이스에 메서드 선언을 추가할 때 자연스러운 자리는 관련 메서드들 옆이고, 그 이웃들이 우연히 기존 {$IFDEF} 블록 안에 앉아 있기 전까지는 전혀 문제없습니다. 조건부 디렉티브는 들여쓰기가 없으므로 40줄 위에서 열린 블록은 주변 선언을 읽는 동안 사실상 보이지 않습니다
다음에 일어나는 것은 한 툴체인에서 성공하는 컴파일이 다른 툴체인에서 연쇄 실패를 낳는 일입니다. 감싼 가드가 Free Pascal이 만족하지 않는 Delphi 버전 검사라면, 선언은 FPC에서 사라지는데 무조건적 구현은 남고, 컴파일러는 기대했는데 찾지 못한 메서드 식별자들에 대한 긴 불평 목록을 냅니다. 메시지 어디도 원인인 조건부 블록을 언급하지 않습니다
두 가지 습관이 이 부류의 실패 전체를 막아 줍니다. interface 섹션에 삽입하기 전에 눈에 보이는 묶음을 믿지 말고 위로 올라가 가장 가까운 열린 조건부를 찾으세요. 그리고 초록불인 Delphi 테스트 스위트는 Delphi에 관한 증거로만 취급하세요. Free Pascal 라이브러리 빌드는 별개의 게이트이며, 통과함을 아는 유일한 방법은 같은 변경의 일부로 build-Win32-Lib-FPC.cmd와 build-Win64-Lib-FPC.cmd를 돌려 보는 것입니다
32비트 산술 코드에서 깨지는 것
딱 하나의 언어 제약이 변화에 가장 무관심한 코드에 정확히 나타납니다. 32비트 Free Pascal은 UInt64를 for 루프 제어 변수로 받아들이지 않습니다. X25519와 X448을 실은 타원곡선 유닛에서 limb 배열을 걷는 루프들은 파일 안의 다른 모든 것이 64비트라는 이유 하나로 64비트 카운터로 작성되어 있었습니다
수정은 수술적이어야 합니다. 체(field) 산술에서 변수의 폭은 정당성 논증의 일부이기 때문입니다. 루프 인덱스는 Integer가 됩니다. limb 배열은 원소가 몇 개 안 되고 어떤 인덱스도 32비트 범위에 접근하지 않으니까요. 산술에 참여하는 것들, 즉 limb 자체와 캐리 전파와 마스크는 UInt64에 남습니다. 그중 하나라도 좁히면 체 소수 기준 결과가 조용히 달라지기 때문입니다
// 32비트 FPC는 UInt64 루프 변수를 거부합니다. 좁히는 것은 인덱스뿐.
// limb, 마스크, 캐리는 폭을 유지해야 체 산술이 변하지 않습니다
var
I: Integer; // 원래는 UInt64
Carry, Mask: UInt64;
begin
Carry := 0;
for I := 0 to High(Limbs) do
begin
Limbs[I] := Limbs[I] + Carry;
Carry := Limbs[I] shr 51;
Limbs[I] := Limbs[I] and Mask;
end;
end;
이런 변경의 검증은 왕복 테스트일 수 없습니다. 같은 망가진 구현으로 암호화하고 복호화하면 자기 자신과 완벽하게 일치합니다. 그래서 known-answer 테스트 벡터가 여기서는 협상 불가능합니다. 공개된 X25519와 X448 테스트 벡터를 돌려 정확한 출력 바이트를 비교하세요. 올바른 구현과 자기 일관적인 잘못된 구현을 가르는 유일한 검사이며, Free Pascal deflate와 AES 코덱 경계에서 다루는 대칭 프리미티브에도 똑같이 적용됩니다
Win32 Free Pascal 빌드의 가치
실무적 보수는 32비트 Windows를 타깃으로 하는 Lazarus 애플리케이션이 별도의 바이너리 계약을 유지하지 않고도 Delphi 형제와 같은 문서 엔진을 얻는 것입니다. 사람들이 별로 말하지 않는 배포에서 가장 빛을 냅니다. 산업용 컨트롤러, POS 단말기, 오래 사는 사업용 소프트웨어. 여기서 32비트 런타임은 레거시 선택이 아니라 하드웨어 제약입니다
Win64 이야기가 먼저였고 Win64 지원을 위한 Free Pascal과 Lazarus에 정리되어 있습니다. Win32는 그것을 그대로 되풀이한 판이 아닙니다. Win64는 호출 규약이 하나, 네임 데코레이션이 없고, 우회할 Delphi 전용 정수 헬퍼도 없어서 이 글의 거의 모든 것이 32비트 타깃 고유의 것입니다. 루프 변수 변경이 필요했던 산술 유닛들은 NIST 곡선 위의 Montgomery 산술에서 다룬 것들과 같으며, 폭 규율은 거기서 더 깊이 설명합니다
일반 교훈은 크로스 컴파일러 이식성 작업의 본질이 언어 기능이 아니라는 것입니다. 두 컴파일러는 여기서 같은 Object Pascal을 받아들입니다. 다른 것은 오브젝트 파일입니다. 심볼을 어떻게 적는가, 런타임이 제공한다고 가정되는 헬퍼 루틴이 무엇인가, 링크에 어떤 프리컴파일 오브젝트가 들어가는가. HotPDF는 HotPDF Delphi PDF 컴포넌트에서 Free Pascal과 Lazarus 패키지를 Delphi 및 C++Builder 패키지와 함께 배송하므로, 같은 소스 트리가 컴파일러별로 포킹되는 대신 모든 툴체인을 먹입니다