CSharp program crashes when using external int in proc

by Tim Baas   Last Updated September 11, 2019 09:26 AM

I'm writing a Unity program in C# and the following code makes it crash:

public class WmGetMessages
{
    public delegate void GetMsgProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto, BestFitMapping=false)]
    [ResourceExposure(ResourceScope.Machine)]
    public static extern IntPtr GetModuleHandle(string modName);

    [DllImport("user32.dll")]
    public static extern IntPtr SetWindowsHookEx(int hookType, GetMsgProc hookProc, IntPtr instancePtr, uint threadID);

    private static Process _process;
    private static ProcessModule _module;
    private static IntPtr _handle;

    public static void Install()
    {
        _process = Process.GetCurrentProcess();
        _module = _process.MainModule;
        _handle = GetModuleHandle(_module.ModuleName);

        InstallHook(14);
    }

    public static void InstallHook(int i)
    {
        Debug.Log("Installing: " + i);
        GetMsgProc proc = (code, wParam, lParam) => {
            Debug.Log("SetWindowsHookEx; i: "+i+", code: "+code+", wParam: "+wParam+", lParam: "+lParam);
        };
        IntPtr hook = SetWindowsHookEx( i, proc, _handle, 0);
    }
}

It crashes because I'm using the int i in Debug.Log() which is inside the GetMsgProc proc, when I remove "+i+" from the log it works.

I'd like to know why, does this have something to do with GC? What I can do to prevent it from crashing.

Tags : c#


Answers 1


You instantiate the delegate proc and then pass it to SetWindowsHookEx. However, nothing after that point references proc, and so the GC will collect it.

Native code then tries to invoke that delegate, with bad results.

You need to keep a reference to proc for as long as it might be called.

Since you're creating an anonymous delegate which captures variables from its surrounding scope (most notably i), in practice this means:

  1. Don't use an anonymous delegate - create a single non-capturing delegate which is stored in a static field on WmGetMessages, and is used for all calls to SetWindowsHookEx
  2. If you do need to create a new anonymous delegate per call to InstallHook:
    1. Return a value to your caller, which the caller must store in a field until the hook is uninstalled
    2. Keep some sort of static list of delegates inside the WmGetMessages class (and remove delegate from it when you uninstall a hook)

To dig into why removing "+i+" specifically makes it work: if you don't reference i in the delegate, the delegate is non-capturing: it doesn't capture anything from its surrounding scope. The compiler will create a single GetMsgProc and then cache it:

[Serializable]
[CompilerGenerated]
private sealed class <>c
{
    public static readonly <>c <>9 = new <>c();

    public static GetMsgProc <>9__7_0;

    internal void <InstallHook>b__7_0(int code, IntPtr wParam, IntPtr lParam)
    {
        object[] obj = new object[6];
        obj[0] = "SetWindowsHookEx; code: ";
        obj[1] = code;
        obj[2] = ", wParam: ";
        obj[3] = wParam.ToString();
        obj[4] = ", lParam: ";
        obj[5] = lParam.ToString();
        Debug.WriteLine(string.Concat(obj));
    }
}

public static void InstallHook(int i)
{
    Debug.WriteLine("Installing: " + i);
    GetMsgProc hookProc = <>c.<>9__7_0 ?? (<>c.<>9__7_0 = new GetMsgProc(<>c.<>9.<InstallHook>b__7_0));
    IntPtr intPtr = SetWindowsHookEx(i, hookProc, _handle, 0u);
}

SharpLab

See how the GetMsgProc instance is cached on <>c.<>9__7_0. This means that even though you're (incorrectly) not keeping the delegate instance alive yourself, it just so happens that a compiler optimization is keeping it alive for you.

However if you do reference i, the compiler then needs to generate a new delegate instance every time you call InstallHook (because each instance needs to capture a different value for i):

[CompilerGenerated]
private sealed class <>c__DisplayClass7_0
{
    public int i;

    internal void <InstallHook>b__0(int code, IntPtr wParam, IntPtr lParam)
    {
        object[] obj = new object[8];
        obj[0] = "SetWindowsHookEx; i: ";
        obj[1] = i;
        obj[2] = ", code: ";
        obj[3] = code;
        obj[4] = ", wParam: ";
        obj[5] = wParam.ToString();
        obj[6] = ", lParam: ";
        obj[7] = lParam.ToString();
        Debug.WriteLine(string.Concat(obj));
    }
}

public static void InstallHook(int i)
{
    <>c__DisplayClass7_0 <>c__DisplayClass7_ = new <>c__DisplayClass7_0();
    <>c__DisplayClass7_.i = i;
    Debug.WriteLine("Installing: " + <>c__DisplayClass7_.i);
    GetMsgProc hookProc = new GetMsgProc(<>c__DisplayClass7_.<InstallHook>b__0);
    IntPtr intPtr = SetWindowsHookEx(<>c__DisplayClass7_.i, hookProc, _handle, 0u);
}

SharpLab

See how it now generates a new GetMsgProc every time you call InstallHook.

canton7
canton7
September 11, 2019 09:10 AM

Related Questions