2026-07-12 修复 cHttpClient 编译 DLL 后 SendPost/SendGet 崩溃
问题现象
cHttpClient 在 VB6 IDE 中以项目引用(联调)方式运行完全正常,但编译为 VBMAN.dll 后,外部项目(csv3)调用 SendPost/SendGet 时第一次成功,第二次崩溃(进程直接终止,无 VB6 错误对话框)。
排查过程
1. 排除二进制兼容性
注册表中发现 VBMAN 类型库有 46 个残留版本(2.0 ~ e0.2),二进制兼容被反复破坏。清理注册表并重新注册后,崩溃依然存在——排除了接口版本不匹配的可能。
2. 排除自定义 Dictionary 类本身
csv3 的 Form_Load 中 Dim a As New Dictionary + For Each x In a 跨 DLL 边界使用 Dictionary 类完全正常,证明 Dictionary 类的暴露和基本功能没有问题。
3. 逐行日志定位
在 Fetch 函数中插入文件日志逐步缩小范围:
- 崩溃发生在
For Each x In RequestHeaders.Keys()这一行 - 第一次调用正常通过,第二次执行到
For Each时进程崩溃 - 改为
hdrKeys = .Keys()+For idx = 0 To UBound(hdrKeys)数组下标迭代后,多次调用均不再崩溃
4. 对比 Scripting.Dictionary 找到根因
| Scripting.Dictionary | 自定义 Dictionary | |
|---|---|---|
Keys() 返回类型 | As Variant | As Variant() |
Items() 返回类型 | As Variant | As Variant() |
As Variant 在 COM 类型库中是 VARIANT,SAFEARRAY 包装在 VARIANT 内部,VB6 运行时对其跨 DLL 边界的生命周期管理完善。
As Variant() 在 COM 类型库中是 SAFEARRAY(VARIANT)*,VB6 把它当原始 SAFEARRAY 指针处理。当 For Each 枚举一个函数返回的临时 SAFEARRAY 时,VB6 编译后的运行时对该临时数组的引用计数管理存在缺陷——数组可能在枚举过程中被提前释放,导致访问已释放内存而崩溃。第一次碰巧内存尚未被覆盖所以正常,第二次则内存已被重用而崩溃。
根因
Dictionary.cls 的 Keys() 和 Items() 返回类型声明为 As Variant() 而非 As Variant,与 Scripting.Dictionary 的 COM 签名不一致。
VB6 编译后跨 DLL 边界调用时,For Each 枚举 As Variant() 函数返回的临时 SAFEARRAY 会触发 VB6 运行时的生命周期管理 bug,导致二次调用崩溃。
IDE 模式下 VB6 运行时内存管理更保守(不释放临时数组),所以联调不会触发。
修复方案
Dictionary.cls 中将 Keys() 和 Items() 的返回类型从 As Variant() 改为 As Variant,与 Scripting.Dictionary 签名完全一致:
' 修复前
Public Function Keys() As Variant()
Public Function Items() As Variant()
' 修复后
Public Function Keys() As Variant
Public Function Items() As VariantcHttpClient.cls 通过 git checkout 恢复原版,无需任何改动。
修改文件
| 文件 | 改动 |
|---|---|
src/Collection/Dictionary.cls | Keys() 返回类型 Variant() → Variant;Items() 返回类型 Variant() → Variant |
src/HttpClient/cHttpClient.cls | 无改动(git checkout 恢复) |
经验总结
VB6 COM 签名必须与被封装的对象一致:封装 Scripting.Dictionary 时,公共方法的返回类型要严格匹配原版签名,尤其是涉及数组的返回值。
As Variant和As Variant()在 VB6 源码层面语义相同,但在 COM 类型库中的 marshalling 行为完全不同。IDE 正常 ≠ 编译正常:VB6 IDE 的运行时对临时对象的内存管理比编译后更宽容。涉及跨 DLL 边界的 COM 调用,必须用编译后的 DLL 做实际测试。
"第一次正常第二次崩溃"是典型的临时对象生命周期 bug 的特征:如果遇到这种模式,优先检查函数返回的临时数组/对象是否在跨 DLL 边界时被错误释放。