【问题标题】:C# equivalent of DllMain in C (WinAPI)C# 等效于 C 中的 DllMain (WinAPI)
【发布时间】:2012-01-02 15:39:21
【问题描述】:

我有一个接受 dll 插件的旧应用程序(约 2005 年)。该应用程序最初是为 Win32 C 插件设计的,但我有一个可用的 C# dll 模板。我的问题:我需要做一些一次性初始化,在 Win32 C dll 中会在 DllMain 中完成:

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
  [one-time stuff here...]
}

是否有与此等效的 C#?我拥有的 C# 模板中没有“DllMain”。我尝试了文字 C# 解释,但不行:dll 可以工作,但不会触发 DllMain 函数。

public static bool DllMain(int hModule, int reason, IntPtr lpReserved) {
  [one time stuff here...]
}

【问题讨论】:

  • 一次性初始化什么?由于 DllMain 是程序性的,而 C# 是面向对象的,我想知道您需要在无法从内部调用的类之外​​执行哪些初始化?
  • 它还取决于您正在构建的应用程序的类型、控制台、Web、Windows 客户端、Windows 服务等。
  • 您不应该在DllMain 中进行重要的初始化。
  • @Ritch Melton 考虑诸如插件加载之类的事情,其中​​托管应用程序不知道 DLL 中的确切内容。在这种情况下,您可能希望您的插件向应用程序注册,以便在需要时可以找到它们,而无需将主机与插件绑定。

标签: c# winapi dll


【解决方案1】:

给你的类一个静态构造函数并在那里进行初始化。它会在任何人第一次调用您的类的静态方法或属性或构造您的类的实例时运行。

【讨论】:

  • 这适用于您在 DLL_PROCESS_ATTACH 情况下执行的操作,但其他情况(DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH)呢?
  • @ThomasLevesque 这不是 OP 的问题。 “我需要做一些一次性初始化。” C# 静态构造函数完成一次性初始化。
  • 是的,我知道这不是 OP 所要求的......但这是 所要求的 ;)
  • @ThomasLevesque 然后问一个新问题。不要劫持现有问题。
  • 因为寻找第二个问题答案的人找不到它。这是一个问答网站,不是讨论网站。
【解决方案2】:

我不得不与遗留应用程序进行交互,可能与您的情况相同。我找到了一种在 CLR 程序集中获取 DllMain 功能的 hacky 方法。幸运的是,这并不难。它需要一个额外的 DLL,但它不需要您部署一个额外的 DLL,因此您仍然可以拥有“在该目录中放置一个 DLL,应用程序将加载它”的范例。

首先,您创建一个简单的常规 C++ DLL,如下所示:

dllmain.cpp:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "resource.h"
extern void LaunchDll(
  unsigned char *dll, size_t dllLength, 
  char const *className, char const *methodName);
static DWORD WINAPI launcher(void* h)
{
    HRSRC res = ::FindResourceA(static_cast<HMODULE>(h),
                   MAKEINTRESOURCEA(IDR_DLLENCLOSED), "DLL");
    if (res)
    {
        HGLOBAL dat = ::LoadResource(static_cast<HMODULE>(h), res);
        if (dat)
        {
            unsigned char *dll =
                static_cast<unsigned char*>(::LockResource(dat));
            if (dll)
            {
                size_t len = SizeofResource(static_cast<HMODULE>(h), res);
                LaunchDll(dll, len, "MyNamespace.MyClass", "DllMain");
            }
        }
    }
    return 0;
}
extern "C" BOOL APIENTRY DllMain(HMODULE h, DWORD reasonForCall, void* resv)
{
    if (reasonForCall == DLL_PROCESS_ATTACH)
    {
        CreateThread(0, 0, launcher, h, 0, 0);
    }
    return TRUE;
}

注意线程的创建。这是为了让 Windows 满意,因为在 DLL 入口点内调用托管代码是不允许的。

接下来,您必须创建上面引用的代码的 LaunchDll 函数。这放在一个单独的文件中,因为它将被编译为托管的 C++ 代码单元。为此,首先创建 .cpp 文件(我称之为 LaunchDll.cpp)。然后在您的项目中右键单击该文件并在 Configuration Properties-->C/C++--> 常规Common Language RunTime Support 条目更改为 Common Language RunTime Support (/clr )。你不能有异常、最少的重建、运行时检查以及我可能忘记的其他一些事情,但编译器会告诉你。当编译器报错时,请跟踪您对默认设置有哪些更改,并在 LaunchDll.cpp 文件中更改它们only

LaunchDll.cpp:

#using <mscorlib.dll>
// Load a managed DLL from a byte array and call a static method in the DLL.
// dll - the byte array containing the DLL
// dllLength - the length of 'dll'
// className - the name of the class with a static method to call.
// methodName - the static method to call. Must expect no parameters.
void LaunchDll(
    unsigned char *dll, size_t dllLength,
    char const *className, char const *methodName)
{
    // convert passed in parameter to managed values
    cli::array<unsigned char>^ mdll = gcnew cli::array<unsigned char>(dllLength);
    System::Runtime::InteropServices::Marshal::Copy(
        (System::IntPtr)dll, mdll, 0, mdll->Length);
    System::String^ cn =
        System::Runtime::InteropServices::Marshal::PtrToStringAnsi(
            (System::IntPtr)(char*)className);
    System::String^ mn =
        System::Runtime::InteropServices::Marshal::PtrToStringAnsi(
            (System::IntPtr)(char*)methodName);

    // used the converted parameters to load the DLL, find, and call the method.
    System::Reflection::Assembly^ a = System::Reflection::Assembly::Load(mdll);
    a->GetType(cn)->GetMethod(mn)->Invoke(nullptr, nullptr);
}

现在是真正棘手的部分。您可能注意到 dllmain.cpp:launcher() 中的资源加载。这样做是检索已作为资源插入到此处创建的 DLL 中的第二个 DLL。为此,请通过执行以下操作创建资源文件 右击-->添加-->新建项目-->Visual C++-->资源-->资源文件 (.rc) 的东西。然后,你需要确保有这样一行:

resource.rc:

IDR_DLLENCLOSED DLL "C:\\Path\\to\\Inner.dll"

在文件中。 (很棘手,是吧?)

剩下要做的就是创建Inner.dll 程序集。但是,你已经拥有了!这就是您最初尝试使用旧版应用程序启动的内容。只需确保包含一个带有 public void DllMain() 方法的 MyNamespace.MyClass 类(当然,您可以随意调用这些函数,这些只是值硬编码到上面的 dllmain.cpp:launcher() 中。

因此,总而言之,上面的代码采用现有的托管 DLL,将其插入到非托管 DLL 的资源中,当附加到进程时,将从资源中加载托管 DLL 并调用其中的方法。

留给读者的练习是更好的错误检查,为调试和发布等模式加载不同的 DLL,使用传递给真实 DllMain 的相同参数调用 DllMain 替代(该示例仅针对 DLL_PROCESS_ATTACH),并将内部 DLL 的其他方法硬编码到外部 DLL 中作为传递方法。

【讨论】:

  • 或运行构建后步骤来调整您的 C# 程序集:stackoverflow.com/questions/1915506
  • @mheyman:但新的“wrapper.dll”不能像旧应用程序使用的 inner.dll 那样使用。 inner.dll 是 ac# 程序集,我的旧应用程序在 inner.dll 上调用 Assembly.GetTypes(),如果我使用的是 wrapper.dll,它将返回一些 c++/clr 的东西而不是 inner.dll 的类型。这对我来说是个大问题,如何暴露 inner.dll 的类型?
  • @Blud 你找到解决这个问题的方法了吗?
  • 程序集已加载,因此您可以通过迭代所有已加载的程序集来找到它。但是,除非您需要调用非托管 DLL,否则使用 Fody nuget 包似乎是最干净的解决方案。
【解决方案3】:

从 C# 也不容易做到,你可以有一个 module initializers

模块可能包含称为模块初始化器的特殊方法来初始化模块本身。 所有模块都可能有一个模块初始化器。这个方法应该是静态的,模块的成员,不带参数,不返回值,用rtspecialname和specialname标记,并命名为.cctor。 模块初始化程序中允许的代码没有限制。允许模块初始化程序运行和调用托管和非托管代码。

【讨论】:

  • 你将如何利用它?
  • 我觉得你可能要直接反汇编插入IL代码。
  • parapura 是正确的,如果不直接修改 IL 就无法做到这一点......希望有一天能解决这个问题。
  • 参见stackoverflow.com/a/9739535/240845(使用 Einar Egilsson 的 InjectModuleInitializer)
  • 模块初始化程序并不总是能很好地替代 DllMain 功能,因为在 DLL 中调用方法之前它们不会被调用。如果您遇到遗留应用程序仅加载 DLL 并期望它执行某些操作的情况,模块初始化程序将使您失败(就像他们对我所做的那样)。有关此问题的解决方案,请参阅此问题的其他地方的 stackoverflow.com/a/9745422/240845
【解决方案4】:

即使 C# 不直接支持模块初始化,我们也可以使用反射和静态构造函数来实现它。为此,我们可以定义一个自定义属性并使用它查找需要在模块加载时初始化的类:

public class InitOnLoadAttribute : Attribute {}

private void InitAssembly(Assembly assembly)
{
    foreach (var type in GetLoadOnInitTypes(assembly)){
        var prop = type.GetProperty("loaded", BindingFlags.Static | BindingFlags.NonPublic); //note that this only exists by convention
        if(prop != null){
            prop.GetValue(null, null); //causes the static ctor to be called if it hasn't already
        }
    }
 }

static IEnumerable<Type> GetLoadOnInitTypes(Assembly assembly)
{
    foreach (Type type in assembly.GetTypes())
    {
        if (type.GetCustomAttributes(typeof(InitOnLoadAttribute), true).Length > 0){
            yield return type;
        }
    }
}

public MyMainClass()
{
    //init newly loaded assemblies
    AppDomain.CurrentDomain.AssemblyLoad += (s, o) => InitAssembly(o.LoadedAssembly); 
    //and all the ones we currently have loaded
    foreach(var assembly in AppDomain.CurrentDomain.GetAssemblies()){
        InitAssembly(assembly);
    }
}

在我们需要立即初始化的类上,我们将该代码添加到它们的静态构造函数中(即使属性 getter 被多次访问,该构造函数也会运行一次)并添加我们添加的自定义属性以公开此功能。

[InitOnLoad]
class foo
{
    private static bool loaded { get { return true; } }
    static foo() 
    {
        int i = 42;
    }
}

【讨论】:

  • 这并不能回答问题,因为 OP 正在寻找一种与遗留应用程序交互的方法。
  • @RitchMelton 我不关注。 OP 正在尝试从托管应用程序或托管 CLR 的非托管应用程序对托管 DLL 进行一次初始化。在第一种情况下,您将在启动附近的 MyMainClass 构造函数中添加代码,而在第二种情况下,您很可能将其放入最初传递给ExecuteInDefaultAppDomain 的方法中。无论哪种情况,您都可以获得 OP 正在寻找的功能。
  • @RitchMelton 他正试图在“C# dll”中复制 DllMain 的功能我仍然看不出它是否与托管进程有关。
猜你喜欢
  • 1970-01-01
  • 2013-10-30
  • 2011-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-08
  • 1970-01-01
  • 2012-02-18
相关资源
最近更新 更多