如何通过编程检测代码是在共享DLL中还是exe中运行?

4
我有一个C#类,简化了全局热键的处理。 该类使用Win32-API函数RegisterHotKey()注册热键。
根据MSDN的说明,当从应用程序调用时,此函数需要在0x0000到0xBFFF范围内的ID值,并且当从共享DLL调用时,需要在0xC000到0xFFFF范围内的ID值。如果在DLL中运行,可以使用GlobalAddAtom()来获取ID。
为了隐藏这种区别,使得该类的用户无需关心ID范围,类本身应该决定在注册热键时使用哪个ID范围。那么,该类必须能够检测其代码是在应用程序内部运行还是在共享DLL内部运行。
但是如何实现呢?什么是最好的C#/.NET方法?
3个回答

3

这是你的类 - 你知道你要把它放在哪里。

如果你不需要共享它,那么只需选择一个小于0xBFFF的ID即可。

如果你的类属于可以被多个应用程序共享的DLL... 或者仅仅可以被你无法控制并且因此无法为其排序ID的代码共享... 那么使用GlobalAddAtom()来获取一个ID(记得在注销热键之后调用GlobalDeleteAtom())。


说明

也许值得花一分钟时间思考为什么有两个不同的ID范围,以及为什么API文档建议在共享DLL中使用GlobalAddAtom()获取后者范围内的ID。让我们从RegisterHotKey()参数的文档开始:

id
    [in] 指定热键的标识符。如果hWnd参数为NULL,则热键与当前线程而不是特定窗口相关联。如果已经存在具有相同hWnd和id参数的热键,请参见备注中采取的操作。

由此可知,热键通过两个潜在的信息对之一唯一标识:线程或窗口句柄和任意16位数字。如果指定了窗口句柄(HWND),则消息将发送到该窗口;否则,它将发送到线程。

所以......如果你只为给定窗口注册了一个热键,那么ID并不重要1;没有人可以为该窗口注册热键,其他窗口的热键事件将发送到这些窗口。同样地,如果你只为给定线程注册了一个无窗口热键,你只会收到该热键的消息。如果你控制应用程序的所有代码,你可以选择任何你想要的热键ID,使用任何你想要的技术来分配它们;没有人会干扰它们,因为你拥有所有可能干扰它们的代码!但是,如果你正在编写一个通用的例程,可以被其他代码调用呢?你不能可靠地选择一个常量ID,因为调用者可能已经在使用该ID,如果他们也在使用相同的窗口或线程,你最终会重新定义他们的热键。或者,如果(如你的情况)你不知道有多少个热键将在运行时注册呢?
你需要一种方法来确保在运行时选择的ID是没有人使用的。这就是GlobalAddAtom()发挥作用的地方:你传递一个字符串给它,它会给你一个保证与该字符串对应且独一无二的ID;除非有人传递相同的字符串,否则这对于系统来说是有效的唯一标识 - 你可能可以想出一个独特的字符串;只需使用公司名称或社会安全号码以及每个所需新原子的前缀进行递增。或者,如果你真的很谨慎,可以使用GUID。

真相背后的真相

有了这个,让我试着澄清一点混淆:Windows实际上并不关心调用RegisterHotKey()的代码是否在DLL中。它不能。考虑以下例程:

void RegisterSuperHappyFunHotKey(HWND hWnd, int id, 
                                 unsigned int fsModifiers, unsigned int vk)
{
   RegisterHotKey(hWnd, id, fsModifiers, vk);
}

这个例程除了将其参数转发到WinAPI函数外,什么也不做,其中没有标识调用模块的任何参数。如果它位于DLL中,则其行为与编译进应用程序本身相同。Windows没有可靠的方法来确定调用来源,而快捷键本身与窗口和线程(或仅线程)相关联,其中任何一个都可以由DLL内外的代码控制。当然,您可能有自己的应用程序或库特定要求:如果您的DLL创建窗口并为其设置热键,则在销毁窗口时要小心注销该热键...但这是您自己的问题,按需处理。

MSDN指定了两个ID范围的原因是为了鼓励DLL作者避免使用应用程序作者使用的ID。如果你是应用程序作者,那么世界就在你手中(大部分情况下),你可以控制加载和执行在你的应用程序进程中的代码,并因此决定使用哪些ID,无论如何都可以:从0开始递增每个新的热键都是可以接受的。但一旦你进入ID的上限范围,你将不得不像DLL一样使用GlobalAddAtom() - 否则你将有可能与从DLL加载的第三方代码以这种方式生成的ID发生冲突。这是某种社会契约。

摘要:

"共享DLL"这一点在这里是一个转移话题;如果你能知道应用程序注册的所有热键的ID,那么只需选择一个小于0xBFFF的数字并使用它即可。如果你不能,因为你的代码将被多个调用者使用(就像你的代码一样...),那么使用GlobalAddAtom()获取一个ID并使用它。

建议

出于以下原因,我建议您在设计类时使用GlobalAddAtom(),因为目前看来您不知道它是否将被构建到自己设计的应用程序中(在这种情况下,您可以控制使用的ID),还是被其他应用程序加载的DLL(在这种情况下,您不能)。不要担心-只要遵循为DLL调用者设置的规则,您就不会违反合同而假装成DLL。


1好的,所以有一些系统定义的热键ID需要注意...


嗯,如果他正在打包源代码呢? - Tim
当然,我知道在哪里使用它。但是,如果这个类的用户必须处理ID并考虑ID的范围,那么这不会是一个好的设计。因此,我想在类内部隐藏这个问题。重用性也是另一个关键点。 - Habi
好的,我已经尽力更好地解释了。基本上,要么你可以可靠地选择ID并知道它,要么你不能并知道你不能 - 在前一种情况下,请使用低范围;在后一种情况下,请使用GlobalAddAtom()。从来没有一个场景可以让你在运行时决定使用哪个ID范围;如果你无法提前决定,你将始终使用高范围(和GlobalAddAtom())。 - Shog9
你可以在应用程序中使用 GlobalAddAtom()。但如果是你的应用程序,你知道哪些低范围内的ID可用,可以直接从其中选择,而不一定非要使用 GlobalAddAtom()。MSDN的意思是,“如果你不是该应用程序,请勿覆盖为该应用程序保留的ID”,并建议使用 GlobalAddAtom() 作为可靠的选择ID的方法,以避免与其他应用程序冲突。如果无法保证你的代码不会被使用其他选择ID的应用程序使用,则使用 GlobalAddAtom() 来选择它们。 - Shog9
就此说吧,这种模式对于其他几个Windows API来说很常见,其中最熟悉的可能是窗口消息:如果您需要通用的窗口消息 并且 您控制应用程序,则可以在范围内选择一个消息ID WM_APP 到0xBFFF。否则,您可以使用 RegisterWindowMessage() 从字符串生成0xC000到0xFFFF范围内的ID。如果这些范围看起来很熟悉,那是因为 RegisterWindowMessage() 在幕后只是调用 GlobalAddAtom() ...;-) - Shog9
显示剩余3条评论

3

试试这个:

bool isDll = this.GetType().Assembly.EntryPoint == null;

MSDN:

Assembly.EntryPoint 属性

“属性值:表示此程序集入口点的 MethodInfo 对象。如果未找到入口点(例如,该程序集是 DLL),则返回 null 引用(在 Visual Basic 中为 Nothing)。 ”


1

完成Philip的回答:

你需要获取调用你函数的程序集的引用,所以代码应该像这样:

Assembly assembly = Assembly.GetCallingAssembly();
Boolean isDll = assembly.EntryPoint == null;

希望这能有所帮助。
Ricardo Lacerda Castelo Branco

如果这段代码在dll内的公共方法中,但是从可执行文件中调用 - 第一行会返回什么? - Philip Wallace

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接