原帖来源:【VB】 在VB6IDE中实现安全子类化 https://www.0xaa55.com/thread-27438-1-1.html (出处:技术宅的结界,转载请保留出处。)
VB6给我们开发者提供了非常简单方便的GUI设计功能,因此受到了大多数开发者的喜爱。可是VB6提供的使用功能却非常少,这个时候就通过子类化等手段绕过VB6的封装,直接取接管底层的系统消息来对其扩展。
然而,VB6IDE的作者当初犯了一个非常胎神的错误,那就是让用户编写的代码直接在IDE进程中运行了,没有通过子进程+进程调试模式(有的胎神看到这里会反驳说着就是VB6优势不用花大量时间去先编译代码,我表示VB6作者完全可以设计一个叫 解释器.exe 的独立可执行程序到VB6安装包中,在我们要调试运行的时候,它直接发代码文本发送给这个子进程不就行了,这样不就既有解释运行的快速启动,又有不崩IDE的风险吗?),这就会导致程序一旦出现致命错误就把整个IDE给崩掉了(很多开发者这个时候都还没来得及保存代码)。
言归正传,现在假如某个开发者本身技术确实比较牛批,不容易写出本身有致命错误的代码,但是在Form、控件被子类化的时候,VB6本身是回调机制就已经自带了极易导致致命错误的风险(哪怕你本身代码写的再好),只要不小心下了断点、点了暂停、结束运行,或有没有On Error的异常都会导致VB6IDE崩掉。
经过我多年的研究终于搞清楚了子类化极易导致IDE崩掉的根本原因:
当我们直接点击IDE的【结束按钮】或用End语句结束执行的时候,VB6释放掉了AddressOf的地址,但是我们没有代码去及时移除子类化,系统会仍然往已释放的地址上回调。
当我们在点击IDE的【中断按钮】或打上断点触发暂停的时候,这时VB6虽然还没有释放AddressOf的地址,但是当有新的窗口消息来时,会导致IDE进入运行状态直到该函数返回再重新恢复暂停状态,但是如果在这里如果又触发了再次断点,就会发生和恐怖的事情:在断点中又去执行新的消息、新的消息有触发断点、新的断点又能接收更新的消息…。最后因为一直递归把堆栈空间消耗完了,导致了堆栈溢出错误,结果在提示堆栈溢出的错误对话框的消息循环中还在继续触发子类化消息回调,最终导致了IDE进程崩掉。
在正常情况下的代码出现错误时,VB6会弹出错误提示对话框,这时我们可以选择 调试 或 停止 两个选项,选择 停止 会触发 1 的流程,选择 调试 则触发 2 的流程,最终还是会导致IDE进程崩掉,最蛋疼是报错对话框是模态,IDE主窗口被锁住了,这时没法点击保存。
为了解决以上各种蛋疼问题,我选择了使用加载外部dll方案,因为:
1、只要子类化回调函数的dll没有被FreeLibrary,回调函数的地址就始终有效。
2、即便VB6IDE进入暂停模式,外部dll并没有被暂停,它仍然可以正常工作(只要这时候它只做自己的工作,不回调给VB6IDE的临时代码就行,我在这时的做法就是判断IDE在暂停状态就直接执行默认处理,运行状态才回调给VB6)。
3、dll使用COM接口机制,可以保证VB6会正确的自动释放对象(哪怕是直接点击IDE的【结束按钮】或用End语句结束执行的时候),这时我就可以利用COM对象的析构函数去实现自动的安全移除子类化监听回调(注意:在VB6中End后,IDE自身类模块中的析构函数是不会再执行的,但外部dll不存在这个问题,仍然会正确执行)。
顺便再说一下传统的SetWindowLong的子类化方式的缺陷:
SetWindowLong 不兼容64位环境
SetWindowLong 没有挂钩顺序管理:假如a先子类化b后子类化,就必须要b先还原子类化a后还原子类化才能正确执行。如果a先还原子类化,这时b就收不到任何消息了,等到b还原子类化的时候却把a的回调地址给设置上去了,使得a重新开始接收了子类化消息(但是这时a的地址是有可能已经释放掉了)。
SetWindowLong 不能给回调函数附加额外数据,回调函数里面只能收到hWnd句柄,但没法直到hWnd句柄对应的是哪个Form或哪个控件(这里肯定又有胎神会提到 SetWindowLong hWnd, GWL_USERDATA, ObjPtr(控件对象) 的操作了,但是想想如果有 a 和 b 两个人都子类化了同一个窗口会怎样?)。
其实SetWindowLong的缺陷巨硬早在二十多年就给我们提供了解决方案,使用新API SetWindowSubclass 来子类化(SetWindowSubclass 保证了32位和64位通用、安全管理了多重挂钩,支持了任意顺序解除、并且提供了指针长度的任意附加数据)。
所以,最后我根据以上原理,使用VC艹封装一个叫 sscls.dll(全称 SafeSubclass 安全子类化)的组件,内部使用轻量级COM机制来实现既不需要注册又能获得COM对象自动安全析构,使用新子类化API SetWindowSubclass 来保证多次子类化的安全和回调对象关联,调用VBA6.DLL的EbMode来监测IDE执行状态,以防止暂停和停止时极易崩掉IDE的问题。
以下则是 SafeSubclass(安全子类化)组件 的使用方法:
1-打开VBAIDE目录
2-复制到VB6安装目录
3-在VB6工程中打开引用对话框
4-添加sscls.tlb文件的引用
5-编写代码进行子类化
6-可以在消息中任意暂停
7-强制结束也可以正常退回IDE设计模式并自动释放子类化对象的内存
8-示例目录中的sscls.dll用于最终发布的exe携带
。。。
具体图片、DLL等可以下载 SafeSubclass.rar 后观看。