By Tim Baas


2019-09-11 09:03:33 8 Comments

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.

1 comments

@canton7 2019-09-11 09:10:08

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 delegates from it when you uninstall the corresponding hook)

Let's 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 in <>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.

@Tim Baas 2019-09-11 09:38:36

Thanks for the clear and detailed explanation! I bet I will be using SharpLab more often, nice tool. I got it working now by creating a List<GetMsgProc> which stores all references of the created procs. I clear it when the program gets terminated. Too bad this bad code isn't failing at compile time. Must be a reason for that though.

@canton7 2019-09-11 09:51:54

@TimBaas When everything's in the managed .NET world, the compiler and runtime can work together to make sure that everything's safe. However the minute you start doing unsafe things -- p/invoke, unsafe, etc -- you're on your own.

@canton7 2019-09-11 09:53:04

@TimBaas Also bear in mind that List<T> isn't thread-safe. Static methods are generally assumed to be thread-safe in C#. You probably want to use a thread-safe collection (e.g. ConcurrentBag)

@canton7 2019-09-11 09:55:07

@TimBaas There's no point in clearing that list when the application terminates - it's being destroyed anyway. If your house is being demolished, there's no point in tidying the kitchen.

Related Questions

Sponsored Content

28 Answered Questions

[SOLVED] Get int value from enum in C#

  • 2009-06-03 06:46:39
  • jim
  • 1364746 View
  • 1644 Score
  • 28 Answer
  • Tags:   c# enums casting int

26 Answered Questions

[SOLVED] Cast int to enum in C#

  • 2008-08-27 03:58:21
  • lomaxx
  • 1232646 View
  • 2937 Score
  • 26 Answer
  • Tags:   c# enums casting

15 Answered Questions

[SOLVED] How to convert C# nullable int to int

  • 2011-05-13 16:59:22
  • KentZhou
  • 340793 View
  • 433 Score
  • 15 Answer
  • Tags:   c# nullable

1 Answered Questions

Set position of TabTip keyboard c# is not working

1 Answered Questions

2 Answered Questions

[SOLVED] How to pass c# string to delphi .dll PChar type?

2 Answered Questions

[SOLVED] How to kill an alert window in Windows using C#?

1 Answered Questions

[SOLVED] Calling GetGUIThreadInfo via P/Invoke

  • 2009-04-05 22:40:31
  • Joe White
  • 2783 View
  • 12 Score
  • 1 Answer
  • Tags:   c# pinvoke

Sponsored Content