Table of Contents

Custom Global Hooks

Custom Implementations of IGlobalHook

If you want an IGlobalHook with a custom dispatch logic, you should extend the SharpHook.GlobalHookBase class. It implements the logic for running, stopping, and disposing of the global hook, as well as raising appropriate events. It also keeps a reference to a running global hook so that it's not garbage-collected. You only need to implement the HandleHookEvent method, and call the DispatchEvent inside it in the way you see fit. You can also call ShouldDispatchEvent before executing any logic to determine whether the event should be dispatched (i.e. it has any subscribers).

As an example, here's the implementation of SimpleGlobalHook (with XML comments removed):

namespace SharpHook;

public sealed class SimpleGlobalHook : GlobalHookBase
{
    public SimpleGlobalHook(
        GlobalHookType globalHookType = GlobalHookType.All,
        IGlobalHookProvider? globalHookProvider = null,
        bool runAsyncOnBackgroundThread = false)
        : base(globalHookType, globalHookProvider, runAsyncOnBackgroundThread)
    { }

    protected override void HandleHookEvent(ref UioHookEvent e) =>
        this.DispatchEvent(ref e);
}

As you can see, it really is exceedingly simple!

GlobalHookBase also provides the BeforeRun and AfterStop methods to execute custom logic before a global hook starts running and after it finishes running. You can also override its Dispose method. You must call the base method inside the overriden method. For example, EventLoopGlobalHook overrides BeforeRun to start a separate thread with an event loop, and Dispose to stop it.

Additionally, GlobalHookBase provides virtual methods that raise appropriate events like OnHookEnabled and OnKeyPressed.

Custom Global Hooks With Other Event Forms

SharpHook provides the IBasicGlobalHook interface and the BasicGlobalHookBase class which act as the roots of the entire hierarchy of global hook classes and interfaces in SharpHook.

IBasicGlobalHook represents basic operations for a global keyboard and mouse hook regardless of the form of its events. It extends IDisposable and defines three methods: Run, RunAsync, and Stop, as well as two properties: IsRunning and IsDisposed. BasicGlobalHookBase is an abstract class which implements these methods and leaves out only the actual handling of the events.

If you want to create a custom global hook which doesn't implement IGlobalHook (e.g. if you want to implement IReactiveGlobalHook or IR3GlobalHook, or have a different interface altogether) then it's highly recommended to extend BasicGlobalHookBase since this way you will only need to implement handling the events, and all other logic (which is actually non-trivial) is handled for you.

The following methods provided in GlobalHookBase are actually provided in BasicGlobalHookBase which GlobalHookBase itself extends: HandleHookEvent, BeforeRun, AfterStop, and Dispose.

As an example, here's a global hook which raises a single event for all hook events instead of different events for each event type:

public sealed class StraightforwardGlobalHook : BasicGlobalHookBase
{
    protected override void HandleHookEvent(ref UioHookEvent e) =>
        this.HookEvent?.Invoke(this, e);

    public event EventHandler<UioHookEvent>? HookEvent;
}

Using Low-Level Functionality Directly

As mentioned above, it's highly recommended to extend BasicGlobalHookBase if you want to implement a custom global hook, but you may nonetheless want to implement everything from scratch. You can read more about that in the article on low-level functionality.

When calling SetDispatchProc, the function must be wrapped into a delegate reference and the reference must be stored to prevent garbage collection. This is because the following code:

provider.SetDispatchProc(someObj.SomeMethod, IntPtr.Zero);

is actually transformed into this code by the C# compiler:

provider.SetDispatchProc(new DispatchProc(someObj.SomeMethod), IntPtr.Zero);

The CLR protects the DispatchProc reference from being garbage-collected only until the SetDispatchProc methods exits (which happens almost instantly). The CLR does not and cannot know that the reference will be used later and so it will happily collect this reference thinking it's not needed anymore. Instead, the following should be done:

DispatchProc dispatchProc = someObj.SomeMethod; // This reference should be stored, e.g., as a field of the object
provider.SetDispatchProc(dispatchProc, IntPtr.Zero);

Additionally, if you need to support Mac Catalyst, there are two conditions that the event callback must satisfy:

  • The method must be static
  • The method must be decorated with the [ObjCRuntime.MonoPInvokeCallback(typeof(DispatchProc))] attribute

BasicGlobalHookBase does all this for you so that you don't have to.

If you look through the source code of BasicGlobalHookBase, you will notice that even though running multiple global hooks at the same time is expressly forbidden and this is emphasized in multiple places in the docs, the class itself still supports running multiple global hooks at the same time. Even though the event callback is static, the class has a dictionary of running global hooks where the key is the object's index (that's incremented in the constructor), and this index is passed as the second parameter of SetDispatchProc (and this behaviour is also discouraged). So why do that? The answer is simple – BasicGlobalHookBase itself just doesn't care that there is such a limitation in place. This is a limitation of libuiohook, and in theory, you could implement a custom IGlobalHookProvider which supports multiple running global hooks, and BasicGlobalHookBase will work without problems with such a provider. Or you could instantiate multiple global hooks with a different provider each, and they will also work without problems.