体会平台调用中的内存对齐
今天解决了一个关于平台调用的小问题,让本次郁闷之旅闪过一丝微光。
问题
在我们的程序中控制另外一个独立运行的程序。为了方便起见,我们的程序就叫master,那个受控的程序就叫servant。master要做的事情是:看servant是否已经运行,如果没有运行,则运行servant,如果已经运行,则把它还原并调到前台——就这俩事儿。
思路
判断好办,获取进程列表,判断一下是否存在servant进程就可以。第一件事也好办,用Shell函数直接执行servant.exe就可以,并且还可以得到servant的进程ID。但是第二个就麻烦一点了,翻腾了一下MSDN,没找到如果进程已经运行,怎么让它还原到前台的.net方法。所以决定借用Win32API,里面有个ShowWindow好像很符合我的需求。
ShowWindow的原型说明如下:
BOOL ShowWindow(
HWND hWnd,
int nCmdShow
);
参数很少,也很简单,还都是输入参数,比我封装过的那些有字符串,又有结构体,还有包含字符串和结构体指针的结构体的指针(p.s.没写错,SDE API里面这样的函数遍地都是)等等变态参数仁慈多了。
hWnd是窗体句柄,一个32位整型值。nCmdShow是控制窗体风格的一组常量:SW_FORCEMINIMIZE,SW_HIDE,SW_MAXIMIZE,SW_MINIMIZE,SW_RESTORE,SW_SHOW,SW_SHOWDEFAULT,SW_SHOWMAXIMIZED,SW_SHOWMINIMIZED,SW_SHOWMINNOACTIVE,SW_SHOWNA,SW_SHOWNOACTIVATE,SW_SHOWNORMAL。从名字也能大概猜出这些常量的含义。
第一个窗体句柄参数在.net里面可以通过Process.GetProcessById(id).MainWindowHandle得到,里面的进程id用前面说的Shell方法可以得到,MainWindowHandle属性是一个IntPtr类型,也没什么问题。
第二个参数各个常量的定义在WinUser.h里面,对应的具体数值为:
* ShowWindow() Commands
*/
#define SW_HIDE 0
#define SW_SHOWNORMAL 1
#define SW_NORMAL 1
#define SW_SHOWMINIMIZED 2
#define SW_SHOWMAXIMIZED 3
#define SW_MAXIMIZE 3
#define SW_SHOWNOACTIVATE 4
#define SW_SHOW 5
#define SW_MINIMIZE 6
#define SW_SHOWMINNOACTIVE 7
#define SW_SHOWNA 8
#define SW_RESTORE 9
#define SW_SHOWDEFAULT 10
#define SW_FORCEMINIMIZE 11
#define SW_MAX 11
我要的应该就是SW_SHOWNORMAL/SW_NORMAL对应的1。
这样,两个参数的输入值都搞定了,下面只要能在.net里面把这个API声明出来就可以了。
实施
在vb.net中声明ShowWindow函数原型如下:
然后在程序中写代码:
Dim servantWindowHandle As Long = Process.GetProcessById(procid).MainWindowHandle.ToInt64
Dim result As Long = ShowWindow(servantWindowHandle, 1)
Debug.WriteLine(result.ToString())
结果却是……
“异常”
打上引号是因为根本就没有抛出什么异常,但是明明给第二个参数传入的是1,也就是SHOWNORMAL,但每次运行的结果却都是窗口直接被干掉了!只留下一个自己的尸体漂在进程列表里。而且无论我给第二个参数传几,结果都是一样,都相当于给第二个参数传入0(SW_HIDE)。调试输出的result值也是怪异的8976496028190507024这种数字。哪里出问题了?
Debug
nnd,就这么几句话也写不对?API应该是没有用错,跟踪监视procid变量的值也是对的。问题的焦点落在了ShowWindow的声明上,我下意识的把函数重新声明成下面的样子:
相应的,后面也改成
Dim servantWindowHandle As Integer = Process.GetProcessById(procid).MainWindowHandle.ToInt32
Dim result As Integer = ShowWindow(servantWindowHandle, 1)
Debug.WriteLine(result.ToString())
呵呵,这样居然就行了。窗口老老实实的秀出来了。
真相
平台调用里面最麻烦也是最重要的就是内存对齐,对不齐内存很多时候连异常也没有,直接就是错误结果。经常是查了很久也找不到问题,就算判断到了是哪个参数写的不对,也很难写出对的声明,我曾经试图用c#封装ArcSDE C API,但是现在还是有几个函数怎么也搞不定。
在这个程序中,窗体句柄是32位整型值,而在.net中,Long变长了,成了64位的。所以在第一种声明方式下,一个Process.GetProcessById(procid).MainWindowHandle得到的32位句柄被强行延拓成了64位的,也就是把全0值补给了它的高32位,然后传入ShowWindow,然而请注意,原始的ShowWindow函数需要的是两个32位的整型参数,现在传个窗体句柄就一下子塞进去64位,内存栈里面留给这两个参数的地方就全被这个句柄值占满了。而被强行伸长的64位句柄的低32位确是真实的句柄值,也歪打正着的填在了本来应该接受句柄参数的内存区域,但高32位,也就是0,同样没有任何理由的占到了nCmdShow参数的位置上。这样就导致了每次传入64位窗体句柄参数的同时,也给nCmdShow参数强制传入了0。这就是为什么在第一种声明方式下,无论给nCmdShow传入什么参数,ShowWindow的动作都是隐藏窗体的原因所在。
仅有的一丝略带创造性的微光渐渐消失了,周围又变成了一片黑暗,由2个博士和3个硕士组成的IT民工们继续在泥坑中挣扎,还不知要挨多久。