这个post主要用来share我的IdentityScope类
在Windows Vista运行Visual Studio 2005(without SP1)时,最好使用Vista的Run as administrator的功能,否则有些功能就不能正常工作。而在我们开发的.NET程序内部,有时也会碰到需要临时提高权限的情形。ASP.NET程序员经常问的一个问题是,我的代码为什么没有权限创建一个文件?基本的解决方案有三个:
- 提升ASPNET帐户(在Windows 2003则是Network Service)的权限(不推荐)
- 为目标文件或文件夹设置ASPNET帐户的读写权限(如果需要访问的文件路径是固定的)
- 在web.config设置impersonate,以另一个帐户的身份运行程序,比如Administrator...
论坛和新闻组里面常出现的另一个问题是,如何访问网上邻居或者映射的网络驱动器?常见的答案是使用Process.Start方法调用cmd.exe执行一个net use命令,这个方案是可行的但是显然不科学... 这里同样是权限问题。
.NET类库自带了个WindowsImpersonationContext类,可以用来进行用户身份模拟。不过创建这个对象的唯一办法调用WindowsIdentity.Impersonate(IntPtr userToken)方法,而userToken却只有通过Windows API调用而来(根本就是没打算让人用的)... MSDN上语焉不详,而实际上Windows SDK解释的更清楚,在LogonUser页有完整的说明和示例。于是使用P/Invoke封装了LogonUser、ImpersonateLoggedOnUser、RevertToSelf这几个API以及相关的一些枚举类,实现了一个IdentityScope类。演示代码:
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
using (new IdentityScope("SUMA-LP", "Administrator", "********"))
{
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
}
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
Console将输出:
Redmond\V-Wexia
SUMA-LP\Administrator
Redmond\V-Wexia
你可以看到在这个using代码块里面执行者身份成功的扮演了本机的Administrator。在访问网上共享资源的时候这个类同样有效,比如:
using (new IdentityScope("Domain", "User", "Password", LogonType.NewCredentials, LogonProvider.WinNT50))
{
File.Copy("file.ext", "\\shared\folder\file.ext");
}
这里使用LogonType.NewCredentials登陆类型意味着本地身份不变,访问网络资源时使用扮演的身份。
IdentityScope类的源代码见:http://blog.joycode.com/sunmast/articles/identityscope_class.aspx
这些天我在家里用C#实现一个资源管理器控件,功能和Windows自己的那个类似,我想把它实现的和Windows的尽量接近 — 当然是越接近越好啦。
问题马上就来了,.NET目前似乎并不是很擅长搞这个,或者说那些相关的Windows API还没被收入进FCL(WinFX/.NET 3.0应该已经收录了吧),这下可麻烦了:
- 地址栏
MFC里面有个CComboBoxEx类,可以用来做带层次缩进和图标的ComboBox,实现资源管理器的地址栏。但是.NET没有,不过还好可以继承System.Windows.Forms.ComboBox类,然后重写OnDrawItem方法,搞定
- 文件图标
.NET 2.0终于开始支持这个功能了,就是Icon.ExtractAssociatedIcon方法,不过它返回的总是文件的大图标.... 而Windows XP里面一个文件可以包含大中小三种尺寸的图标(每种还有三等色深所以一共是九种),所以还是得调用SHGetFileInfo这个API;
还有些图标是在shell32.dll里头的(比如文件夹的图标),那样还得把它们枚举出来... 这个得用ExtractIconEx;
此外还要维护一个ImageList,不然占用太多Icon句柄,会发生GDI Failue的... 这个还好,FCL已经有这个类支持了
- 文件类型信息
每种文件类型都有一个Description,比如说.doc就是Microsoft Word Document,也记录在注册表里头。这个问题我在CSDN已经回答了N遍,不想再打一遍了
- 文件夹背景图
这个暂时还没功夫去研究,并且我认为这个功能相对不是那么的重要。还有文件夹缩略图之类的... 不是一般的麻烦,日后再说
- 左边的树形视图
.NET自带了TreeView控件,只要有Icon,剩下的工作就很容易了
- 右键菜单
菜单里有很多项目都是记录在注册表里面的。这个又是个很大的话题... 总而言之就是麻烦得要死。不过那些Cut、Copy、Delete、Properties什么的操作的实现倒是简单,可以直接用ShellExecuteEx这个API
- 快捷方式
解析.lnk文件到目标文件,可以直接打开文件流读取里面的字符,在某个偏移量的某个位置是可以找到的,不过这样不“安全”,还是用API吧。MSDN说可以使用IShellLink和IPersistFile这两个COM接口,代码虽然很ugly但是不算很长。问题又来了,IShellLink没有继承IDispatch,只是继承了IUnknown,这意味着tlbimp.exe也帮不了忙(貌似),得自己声明那些接口和组件原型... 阿门,我今天搞了一晚上,还是不大对头,想睡觉了...
- 主文件视图
这个可以用.NET自带的ListView控件应付,和Windows一样它也带了5种浏览模式(Title、List、Detail等等),不用自己实现
- 文件/文件夹的Tooltip
Tooltip的内容并不是固定的... 比如说doc文件的Tooltip还会包括Author等信息,mp3文件则会有Duration之类的,这个要用Structure Storage
相关的COM组件,超级麻烦,暂时不想管了~
- .....
.....
不过今天也不是没有收获,在Google上找到了这么个网站:
http://www.vbaccelerator.com/home/NET/Code/Libraries/Shell_Projects/index.asp
他们把很多Shell相关的东西给封装了一下,比如我一直郁闷的文件快捷方式的问题,他们已经提供了解决方案。早知道有这个,我就不用那么费劲了。
还是说P/Invoke,不过这回不大一样,因为我找到了一个工具来帮忙了
,而不是用极易出错的方式手工进行操作,这个工具就是SWIG:Simplified Wrapper and Interface Generator。
HOME:http://www.swig.org/
WIKI:http://www.dabeaz.com/cgi-bin/wiki.pl
WIKIPEDIA:http://en.wikipedia.org/wiki/SWIG
这玩意儿居然可以根据C/C++的头文件直接生成出各种脚本语言的包装类,包括Lua、Tcl、Perl、Python、Ruby、Guile、Chicken、PHP、Java、C#、Ocaml(见WIKIPEDIA上面的介绍)。不仅如此,它和我以前看到的几个类似的工具不同,它似乎还带了一个C/C++编译器前端,可以利用语义分析过程处理各种#include、#define、typedef等等造成的复杂情况,和一般的依赖于文本替换的类似工具根本就不在一个重量级。
对于C#,它自动生成的代码大量应用了P/Invoke,产生了大量的static extern声明。不过,它们对包装类的使用者来讲是透明的。比如,原来C++的class会被自动对应到一个C#的class,你能看到的就是它的property和method而已,和普通C#的class没什么两样。
我在它的Example文件夹下面看到了好多例子,关于C#的有class、enum、funcptr、reference、simple、template、variables。注意到,很多原本很难通过P/Invoke调用的Native元素比如C++类型、方法指针、C++模版类/方法,它都能支持,摆脱了P/Invoke只能用于Invoke结构体和C导出函数的局限。也许C++有很多更复杂的语法或用法它还没能支持(我不确定,这个还有待继续研究),但也已经相当NB了。
真是相见恨晚啊,早知道有这个东西,就不会有这个post存在了
。
BTW,
中国的太监们居然把WIKIPEDIA这么好的网站都给阉了,实在是匪夷所思。好在通过IP还是可以访问;再懒一点,可以改一下你的hosts文件(%windir%\system32\drivers\etc),在最后面接上这么几行:
145.97.39.155 www.wikipedia.org
145.97.39.155 en.wikipedia.org
145.97.39.155 zh.wikipedia.org
145.97.39.155 upload.wikimedia.org
学新东西的时候,WIKIPEDIA是很有用的,特别是那些专业词汇,找起来比GOOGLE更精确,嗯。
我成了C#和C++程序员的翻译...
最近有点烦
,客户要求我们用C#实现一个关于串口通讯的模块,这事儿本身没那么麻烦,只是他们给了数年前写的几千行的C++代码
,要我们给改成C#版的... 我不爽,于是我说,用VC.NET吧(还不是VS2005不能用C++/CLI
),Refactor后重新编译一下基本上就能给.NET用了(整个项目是基于.NET/C#的),我也干过这事儿,貌似不难。但是客户坚持不要额外的DLL,项目又是用C#写的,ILMerge工具也不能合并用VC.NET写的DLL,再加上市场部的哥们已经和客户说了,“我们什么软件都写得出来”,所以... 
理论上是可行的
从头到尾看了他们的代码一遍之后,发现虽然是VC6的Project,但并没有用到多少MFC里面的类(用到的也就是CString之类的,无视),阿门,上帝保佑,阿弥陀佛,大部分都是在直接调用标准库和Windows API。所以我觉得最起码理论上可行,不就是P/Invoke嘛,把内存给我对齐就行了,所以说,“没问题”。
程序员都是过于乐观的一族,对于这个缺点,我好像特别严重。答应下来之后,放下电话,我怀着内疚的心情看了看项目组的反应,不由的心生胆怯
。不久后PM走到跟前,语重心长地说,“Vista Xia(我的无敌的英文名),这个任务就交给你了。下一个Release的Dead Line就快到了,赶紧啊。”
(我靠连个Assistant都没有)
这是一个用C#语言编写的C++项目
这事儿比较特别,完全违反了公司惯用的流程,没有需求文档,没有HL和DL的设计文档,也没有测试用例(待会儿再说为什么)。公司的SQA不知道管不管这事儿,要管的话项目估计又要从CMM5直接跌到CMM1了
。总而言之,我只是写了个TODO List,就开始了:
- 在.h文件中找到所有的struct定义,先改成C#的。用Marshal.SizeOf得到其大小,初步进行测试:看看它和在C/C++程序中用sizeof得到的大小是否相等
- 对应所有C++的类,先写出所有C#的类的原型,包括成员字段和函数。所有函数里先放着throw new NotImplementedException()这么一句,省得影响编译
- 对应于C++类的析构函数,在C#用Dispose Pattern实现
- 找到所有用到的Windows API和其他Native API,在C#用static extern方法声明之,然后对不确定都否工作的那些(比如参数里头有n个*的那种)进行初步测试
- 在.h文件找到所有宏定义的常量,改成const的字段;而宏方法只能直接改成普通方法(C#没有inline关键字)。对于用宏改了名字的类型,专门记载到一个文档中,以后直接用文本替换(ctrl+h)搞定之
- 把.cpp文件的代码逐步copy到C#代码中,一个函数一个函数的改。基于C/C++标准库和语言本身特性的代码语句,一般都可在FCL和C#中找到对应的做法,这也包括指针操作;至于API的调用,前面已经准备好了
- 编译通过,测试一下

走火入魔
这个模块并不是要实现什么很复杂的功能,不过,仅仅是声明API和结构体的C#代码就一千多行了(还不算注释)。我想99%的.NET程序员都没做过这种事情 — 我认为愿意写这种代码的人一定有受虐倾向,除我之外
。
在Visual Studio,习惯了把所有代码写完,然后按F5就知道能不能跑了。然而我在这里忽略了一点,就是调试这个程序还需要相关硬件设备的配合(好像是客户开发的一些设备),需要连上我的机器安装,调试,才可运行。可客户在遥远的广东,公司里也不大可能有这种设备的,看来麻烦了
。
客户那边的程序员看起来比我还要乐观,他似乎觉得只要程序写对了就应该能跑,不需要调试的
。那么除了让代码能通过编译,我能做的就是再review几遍代码而已了
。他大概不知道,在C#作P/Invoke时,编译器能做的工作是很有限的:
- 它不能检查API声明是否有效;也就是说,是否和API原型兼容
- 托管struct被Marshal到本机struct的时候能否对齐内存,只有在运行时才会抛出一个毫无提示作用的异常(比如会莫名其妙的抛出空引用异常)
- 有些托管struct,看起来很对,编译也能通过,但在运行时不给Marshal,我在前面的blog曾有提到。这是.NET 1.x的bug,2.0已经修好了
- 对于指针类型的函数参数,在C#有好多好多种声明的方法,比如StringBuilder、string、byte[]、int、uint、IntPtr、ref xxx、out xxx,等等。只要保证Marshal时是一个32位的值就可以了(对于32位系统)。这视乎具体情况而定,编译器不管这个细节(事实上编译器也管不了这个),要程序员自己处理。有时候会有点复杂,比如数组的元素又是子数组时,你只能选择IntPtr或者IntPtr[],并且需要手工copy内存
- .NET的字符串在内存中总是以Unicode方式编码的,但对于API函数需要string的编码,有时取决于Windows版本,有时需要显式设定,有时又需要显式转换。同样,编译器也管不了这个细节
- 和C/C++编译器一样,这里也不会有越界检查和内存泄露等方面的保证
如履薄冰
且不说这些问题,就算编译器能帮上忙,对于这几千行几乎全部基于P/Invoke的代码,不测试就成功运行,也太NB了一点了吧
。不过,这算是我的疏忽,早知本地无法测试(问题是客户也不早说),大概就不会答应这么做了,sigh。
不管怎么说,在反复review好几遍之后,我向VSS签入了代码,然后听天由命了(不过,我后来又给客户发了个mail说了一下risk,打个预防针先)。看来到客户现场调试已不可避免,嗯,不管了
。
怎么说P/Invoke呢
毫无疑问,这是一个非常重要的技术,事实上,FCL本身也大量应用了P/Invoke,它是CLR的几种基础技术之一。但在我看来,这并不代表微软推荐你也把这项技术作为你的应用程序的基础。适当使用就可以了,比如偶尔需要调用一两个API的时候。而像我手上的这个东西,显然应该用VC.NET的(现在的C++/CLI),实现起来要容易得多,也可靠得多。
BTW,
不知道是去年还是前年,有人居然用Javascript实现了Web上的《星际争霸》,着实很牛,但你不觉得它也属于走火入魔之列吗?现在流行用牛刀杀鸡,但不要用杀鸡刀宰牛啊!
1、Array in stack
对于这样的struct:typedef struct { int XY[2]; } Point2D;
要在.NET为一个非托管函数传递这样一个结构体,原来得这样定义:
struct Point2D

...{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=2)]
public int[] XY;
}
现在可以这么写(不过得用unsafe上下文):
unsafe struct Point2D

...{
public fixed int XY[2];
}
不过这个功能还非常有限,不知道是出于什么原因考虑,只允许在strcut里面定义这样的数组,并且只能使用bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float和double这样的primitive类型。
也可以把数组作为局部变量分配在堆栈上,只是语法不太一样,那就是stackalloc关键字:int* fib = stackalloc int[100];,也要unsafe上下文。这可以提高不少效率。这是.NET 1.x就有的功能,只是似乎没人用这个...
PS. 对于只允许使用primitive类型,我认为是没道理的,最起码应该允许所有值类型的栈内数组。设计者们为啥这么考虑呢?怕堆栈溢出?据我测试.NET的堆栈空间也是1M左右,大部分情况下这么大的栈空间都被浪费了。
2、Function pointer as a return value
在.NET 1.x做P/Invoke时,对于那些回调函数,可以使用Delegate类型的参数作为函数指针传入。但有些非托管函数的返回值也是个函数指针,此时.NET 1.1变得无能为力,要调用这个函数,你得用native代码再写个包装,总之很麻烦。
.NET 2.0的System.Runtime.InteropServices.Marshal类为此需求新增了两个方法:
public static Delegate GetDelegateForFunctionPointer (
IntPtr ptr,
Type t
);
public static IntPtr GetFunctionPointerForDelegate (
Delegate d
);
3、Marshal过程支持更多的类型
这是一个很细的问题,比如这样一个非托管struct:typedef struct { Point2D XYZ[3]; } Point2DX3;
对应到C#,你也许会这样写:
struct Point2D

...{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=2)]
public int[] XY;
}
struct Point2DX3

...{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=3)]
public Point2D[] XYZ;
}
事实上这是不可行的。在.NET 1.x,结构体内嵌定长数组的类型必须是primitive类型,否则不能进行marshal过程。事实上调用Marshal.SizeOf时,会弹出异常说“无法得到大小”云云...
.NET 2.0把这个问题给改了(与其说是个增强,还不如说是修正了这个bug),Marshal.SizeOf(typeof(Point2DX3))现在可以正常运行,输出24 == 3 * 2 * sizeof(int)。
让Marshal.SizeOf正常工作非常重要,得不到对象的大小,内存对齐都无法保证,marshal过程根本就不可行。
此外,正常工作的Marshal.SizeOf可以使这种“序列化”方式也总能正常工作(这在我以前的blog贴过,这里是Generic版本):
unsafe class BinarySerializer

...{
public static byte[] Struct2Bytes<T>(T obj)

...{
int size = Marshal.SizeOf(obj);
byte[] bytes = new byte[size];
fixed (byte* pb = &bytes[0])

...{
Marshal.StructureToPtr(obj, (IntPtr)pb, true);
}
return bytes;
}
public static T Bytes2Struct<T>(byte[] bytes)

...{
fixed (byte* pb = &bytes[0])

...{
return (T)Marshal.PtrToStructure((IntPtr)pb, typeof(T));
}
}
}
和.NET内置的序列化机制相比,这个方案效率要高得多。和.NET内置的序列化机制相似,这里需要对象或结构体内部的所有成员都能使用Marshal.SizeOf得到其大小,对应于“对象或结构体内部的所有成员都带有[Serializable]特性或者实现了ISerializable接口”。
PS. 这个序列化方案效率虽高,但不如.NET内置的方案可靠。.NET的编译器可以检查一个类型能否支持序列化,而这个方案得由程序员判断Marshal.SizeOf能否工作(比如Marshal.SizeOf绝对不会知道string类型实际占用多少内存,也就是说类型实例的大小必须是固定的才行)。