SetWindowsHookEx failing in .NET 4.0 on 32-bit machine with "module not found"?
Asked Answered
P

3

13

I have found similar questions on this page, but I can't seem to figure out how to interpret the answers or figure out if they are truly duplicates.

Here are the possible duplicates I've found, with comments:

The comments in question on the deleted answer to that last one, by Hans Passant, reads:

Are you using .NET 4.0? Its CLR changed the way assemblies are loaded, there is no longer a LoadLibrary call, there won't be module handle for them. Using GetEntryAssembly() instead would be another fix. – Hans Passant May 5 at 19:43

So, what's the word here? Are you using .NET 4.0? Did you try using LoadLibrary("user32.dll") to get a usable DLL handle? – Hans Passant May 6 at 15:43

I'm pretty sure I don't need to do this, but obviously I'm not 100% sure. The question I'm left with if I need to change this, is why it works on 64-bit OS, when compiled for Any CPU, but doesn't work on 32-bit, in any configuration.

If indeed something has changed regarding loading of .NET assemblies, so that I don't get a proper handle for the class library, I have the following questions:

  • Is there any way I can trick this into doing what I want, without having to downgrade to .NET 3.5 or change the hook library to unmanaged?
  • Why does it work when running on 64-bit OS, but not on 32-bit?

Background

I have built a program, in .NET 4.0, that uses SetWindowsHookEx with the WH_KEYBOARD_LL hook type to capture key presses. This runs nicely on my 64-bit Windows 7, but crashes with a "module not found" when the keyboard hook is installed on 32-bit Windows 7.

Here's what I've tried:

  • Compile for x86, run on 64-bit OS, crashes with "module not found"
  • Compile for x86, run on 32-bit OS, crashes
  • Compile for Any CPU, run on 64-bit OS, runs nicely
  • Compile for Any CPU, run on 32-bit OS, crashes
  • Switch to .NET 3.5 and repeat the above four cases, they all work

I'd rather not switch my code to .NET 3.5, since I'm using a few of my class libraries to ease the work, and the latest code is only in .NET 4.0.

You can download a .ZIP-file with everything as a Visual Studio 2010 project if you want, or you can paste in the following two files.

To recreate if you want to go down that route:

  1. Create a new console-project, .NET 4.0
  2. Add another class-library project, also .NET 4.0
  3. Add a reference to the class-library project from the console-program project
  4. Paste in the Program.cs content below into the Program.cs file you have in the console project
  5. Paste in the Hook.cs content below into a file in the class-library project. You can paste it into the Class1.cs default file, or add another file. You can not put this into the console project

Then build and run, test various configurations.

Program.cs

using System;
using HookLib;

namespace HookTest
{
    class Program
    {
        static void Main()
        {
            var hook = new Hook();

            Console.Out.WriteLine("hooking");
            hook.Enable();
            Console.Out.WriteLine("hooked");

            Console.Out.WriteLine("unhooking");
            hook.Disable();
            Console.Out.WriteLine("unhooked");
        }
    }
}

Hook.cs

using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;

namespace HookLib
{
    public class Hook
    {
        private IntPtr _Handle;
        private HookProcDelegate _Hook;

        public void Enable()
        {
            Module module = Assembly.GetExecutingAssembly().GetModules()[0];
            if (module != null)
                Console.Out.WriteLine("found module");
            IntPtr moduleHandle = Marshal.GetHINSTANCE(module);
            if (moduleHandle != IntPtr.Zero)
                Console.Out.WriteLine("got module handle: " +
                    moduleHandle.ToString());
            _Hook = HookProc;
            _Handle = SetWindowsHookEx(WH_KEYBOARD_LL, _Hook, moduleHandle, 0);
            if (_Handle == IntPtr.Zero)
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        public void Disable()
        {
            bool ok = UnhookWindowsHookEx(_Handle);
            _Handle = IntPtr.Zero;
            if (!ok)
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        private delegate int HookProcDelegate(
            int code, IntPtr wParam, IntPtr lParam);

        private int HookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            return CallNextHookEx(_Handle, code, wParam, lParam);
        }

        private const int WH_KEYBOARD_LL = 13;

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(
            int hookType, HookProcDelegate lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern int CallNextHookEx(
            IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    }
}
Polemoniaceous answered 8/9, 2010 at 20:37 Comment(1)
I am well aware that my initial finding that the handles differ, and the comments to that other question by Hans indicates that indeed something changed in the way .NET 4.0 loads assemblies. I'll edit in a better question text.Polemoniaceous
M
17

Yup, I think you understand what's going on. SetWindowsHookEx() requires a valid module handle, and verifies it, but it doesn't actually use it when you set a low-level hook. You just need a valid handle, it doesn't matter which specific one. Calling LoadLibrary("user32.dll") is a good way to get a handle, that DLL will always be loaded anyway since you P/Invoke its methods. And it is always loaded by the CLR bootstrapper (mscoree.dll). Don't bother calling FreeLibrary(), it makes no difference.

Later versions of Windows no longer perform this check. Not exactly sure when that started, somewhere around Windows 7 SP1 I think. Probably meant to be helpful but invokes the "works on my machine, not the customer's" failure scenario.

Maryettamaryjane answered 10/9, 2010 at 11:37 Comment(6)
Aha! So that's why you suggested loading user32.dll in that other answer, I couldn't figure that out because the handle I thought I needed was for my own dll. I will test this asap!Polemoniaceous
And just to make sure. The reason why FreeLibrary is unnecessary is that although there is a "leak" in the sense that I say "I am now using this, so please increase the usage count of the dll", it doesn't matter because the dll will always be unloaded when my process exits anyway. If the DLL I loaded with LoadLibrary was a manual one, that would be the functionality bound up in the pair of LoadLibrary/FreeLibrary? Is this a correct assumption?Polemoniaceous
I was trying to rewrite my code to call the user32.dll functions through LoadLibrary/GetProcAddress, didn't exactly work spectacularly :)Polemoniaceous
Is there a reason why you suggest to call LoadLibrary("user32.dll") instead of just passing IntPtr.Zero? After trying both ways the results seem to be the same.Isometrics
The point was to ensure it still works when your program runs on a machine with a Windows version less than Win7SP1.Maryettamaryjane
Ah great. Thank you very much for the timely answer.Isometrics
P
2

Here is my solution that works both in .net 2 and 4. hInstance is ProcessModule.BaseAddress.

public static class ModuleHelper
    {
        public static ProcessModule GetCurrentModule()
        {
            // need instance handle to module to create a system-wide hook
            Module[] list = System.Reflection.Assembly.GetExecutingAssembly().GetModules();
            System.Diagnostics.Debug.Assert(list != null && list.Length > 0);

            var currentProcess = Process.GetCurrentProcess();
            var modules = currentProcess.Modules;
            ProcessModule mod = null;
            foreach (ProcessModule m in modules)
                            //for .net 2 we will find module here
                if (m.ModuleName == list[0].Name)
                {
                    mod = m;
                    break;
                }

                    //for .net 4 take current module
            if (mod == null)
                mod = Process.GetCurrentProcess().MainModule;

            return mod;
        }
    }
Parachronism answered 22/11, 2010 at 19:56 Comment(1)
I had the same issue on XP with .NET 4.0. This works on both XP and 7.Treed
P
2

In .Net 4.0 for this code to work I had to replace the call:

SetWindowsHookEx(WH_KEYBOARD_LL, _Hook, moduleHandle, 0);

with:

SetWindowsHookEx(WH_KEYBOARD_LL, _Hook, IntPtr.Zero, 0);

this fixed the problem ,This works when the call is made from the same module.

I got this from here

Picture answered 6/3, 2012 at 11:55 Comment(1)
This is only valid if you call SetWindowsHookEx from the same module.Epley

© 2022 - 2024 — McMap. All rights reserved.