HotPDF 在 Free Pascal 3.2.2 加 Lazarus 下编译并运行,对这次移植最诚实的概括是两句话。文档创建、加载、保存、压缩、解压、加密与解密全部工作在纯 Pascal 后端上,因此 Lazarus 应用可以在没有任何 C 依赖的情况下生产与消费真正的 PDF。可选的原生图像编解码器则不行,因为预构建的 Win64 目标文件使用一种两个 Free Pascal 链接器都无法消化的 COFF 变体,于是在该工具链上这些入口点解析为"失败即关闭"的存根
从"能编译"到"能工作"花了一组具体的修复,而每一项都是陷阱,任何其他迁移到 Free Pascal 的 Delphi 代码库都会撞上。值得按它们伤人的顺序记录下来
为什么单元能编译证明不了什么?
因为 Pascal 单元可以引用一个永远不会做任何有用事情的符号,同时仍然让编译器满意。当全部 113 个库单元在 Free Pascal 下干净构建时,归档容器处理器确实能用——一个打开 CBZ 并转成 PDF 的冒烟测试验证了它。XFA 表单扁平化却完全不能工作,因为扁平化必须解压压缩的 /XFA 包流,而 deflate 入口点还是存根。构建输出里没有任何东西区分这两种情形
由此得出的规则很短。在发布说明里写下某功能在新工具链上可用之前,先写一个运行时探针,在该工具链上端到端地演练这个功能。编译覆盖是前提,从来不是证据。移植覆盖面的整体图景见Free Pascal 与 Lazarus Win64 支持说明
cdecl 存根内的 raise 到不了调用方
这一条值得单独一节,因为症状太具误导性。存根单元像静态库那样暴露 C 入口点,所以存根长这样
// 看起来合理。其实不然。
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
public name 'inflate';
begin
raise ENotSupportedException.Create('codec unavailable');
end;
在面向 Win64 的 Free Pascal 上,该异常不会传播到调用方。没有任何 try..except 处理器能看到它,因为按这种方式声明的 cdecl 边界在展开时不会携带 Pascal 异常帧;进程以退出码 217 终止。从应用一侧看,没有错误、没有消息、没有日志行,只有一个消失的程序。这严格地糟于一个错误答案,因为错误答案尚可处理
诱人的修法是让存根改返回失败码,对 inflate 这是对的,因为 zlib 有明确定义的错误返回。但一般而论它是错的:jpeg_read_header 的存根返回零,等于叫调用方拿着一个没人初始化的结构继续走下去。持久的修法是在 Pascal 入口点设闸,而不是在 C 形状的存根内部,并沿用该 API 已有的失败约定
function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
// 在到达存根之前就拒绝,沿用该 API 自己的
// 失败约定,而不是跨越 cdecl 抛异常
Bitmap := nil;
Result := False;
Exit;
{$ENDIF}
Result := DecodeJPEGNative(Data, Bitmap);
end;
paszlib 不是 zlib,差别是两档文档
Free Pascal 上可用的 Pascal deflate 实现处理两种封装:zlib 包装与裸 deflate。它不处理 gzip 封装——zlib 通过 16 到 31 的 windowBits 值选择它——也不处理 32 到 47 选择的自动检测模式。HotPDF 两者都需要。安全的 SVG 导入路径请求 31,加载器有一个回退阶梯,当流的封装不明时请求 47。漏掉任何一个,一整族文档就打不开了,而解码错误指向的是流本身,而不是缺失的封装
还有一处更尖锐的不兼容。paszlib 声明的 z_stream 记录与 C 版内存布局不同:它的 msg 字段是短字符串而非指针,total_in 与 total_out 是 64 位,而 C ABI 里是机器字。因此调用方记录无法直接传入。可行的安排是把 paszlib 状态藏在公共记录早已预留的 state 指针后面,并在每次调用前后把公共字段拷进拷出。gzip CRC 与八字节长度尾块也在同一个垫片层里处理——那是它们的自然归属,因为封装决策本来就归它管
把动态数组传给无类型 var 参数
这是眼下最可能就藏在你代码里的 bug。把动态数组传给无类型 var 参数时,被调方收到的是数组变量的地址——也就是一个指针的地址,而不是载荷的地址。于是向它读入会覆写变量本身以及紧挨着它的东西
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// 错误:交出的是 FBuffer 变量的地址
FStream.Read(FBuffer, Length(FBuffer));
// 正确:交出的是第一个载荷字节的地址
FStream.Read(FBuffer[0], Length(FBuffer));
end;
在 Delphi 上,错误写法常常看起来能用,因为它破坏的是一个此后无人读取的相邻栈槽。在 Free Pascal 上,同一行代码在首次使用时段错误。肉眼难辨的原因是静态数组没有这个问题——静态数组变量本身就是自己的载荷——所以同一个文件里两种写法都可能正确,取决于几百行之外的声明
没有 System.Zip 的 ZIP 容器
Free Pascal 没有 RTL zip 单元的等价物,而现有的替代品 API 表面不同,又不支持旧容器格式仍在使用的传统加密,所以写一个库内的小读取器反而比适配它更短。两个格式细节花掉了时间,而且容易弄错
第一个是加密头校验字节。它的第十二个字节通常是 CRC 的高字节,但当通用标志位 3 置位时——意味着尺寸存放在尾随数据描述符中、CRC 尚不可知——校验字节改来自修改时间的高字节。只实现 CRC 形式,则每个以流模式写出的归档都会拒绝正确密码。第二个是 ZIP64 扩展字段:它的三个 64 位字段按固定顺序出现,但只在对应的 32 位字段饱和时才写入,所以在固定偏移处读取,在你测过的归档上有效,到下一个就失败。要按哪些 32 位字段饱和来做位置解析
一个值得知道的便捷特性:Free Pascal 的解压流构造器可传第二个参数跳过 zlib 头,这正是 ZIP 条目需要的,因为它们存的是裸 deflate。这条路径完全不经过库的 zlib 垫片,因此不受缺失 C 后端的影响
LCL 下彩色字形的透明度
读取光栅化彩色字形的 alpha 通道是唯一没有直接对应的图形细节。LCL 的 PNG 类没有暴露 alpha 的扫描行访问器,把 PNG 赋给位图又会丢弃它,于是彩色 emoji 到达时完全不透明,合成时背后带着一个黑框。可行的路线是接口图像:从 PNG 创建它,再经颜色访问器读像素——记住其分量是 16 位,要右移八位才变成字节。那个表面还使用自然的自上而下行序,所以 VCL 扫描行代码需要的 Height - 1 - Y 反转必须移除而不是照搬
提 bug 之前先看两条构建系统说明
完整重建偶尔会以一个未定义符号失败,符号名以 $crc 后缀加十六进制值结尾。该后缀由参数类型计算而来,当一次构建在同一遍里针对两个不同接口版本编译同一单元时就会对不上。重跑构建即可消除;签名本身没有错
其次,Free Pascal 3.2.2 没有匿名方法,所以库中原本用闭包连接并行管线的地方,Free Pascal 构建改用确定性的串行回退。输出相同,吞吐不同;如果你的部署依赖并行页面渲染,这就是眼下留在 Delphi 的理由,管线设计见并行渲染管线一文。图像编解码是另一个工具链选择改变能力而不仅是速度的地方,因此 Lazarus 部署应据此规划自己的图像格式;当前各工具链矩阵见 HotPDF Delphi PDF component 产品页