Post

macOS Keyboard Emulation with C#

By using the CoreGraphics API in macOS, a developer can emulate a key press without any physical interaction. Doing that is fairly simple in Objective-C or C++, but how can we do the same in C#?

CoreGraphics

CoreGraphics is a system framework standard in macOS. It provides access to “Quartz Event Services”, which we can use to emulate a key press.

Let’s take a quick look at how that works in C++:

1
2
3
CGEventRef e = CGEventCreateKeyboardEvent(NULL, (CGKeyCode) 33, true);
CGEventPost(kCGHIDEventTap, e);
CFRelease(e);

It’s fairly simple - create, post and release the event. This code snippet should type a little [ into whatever window you’re focused on.

Before we start putting this into C#, let’s examine the two important functions here.

CGEventCreateKeyboardEvent

This takes a source reference, virtual key code and boolean (will the key be pressed or unpressed?) and returns a reference to the event.

Event sources are out of scope for this post, but feel free to check out the documentation for CGEventSourceCreate.

CGEventPost

This takes a CGEventTapLocation for the first argument, with the event reference we want to post as the second argument.

I’m not too sure about this, but I think the event tap location is where the event will be placed / processed.

We’ll just use kCGHIDEventTap for now, which will place the event tap “where HID system events enter the window server”1

Unmanaged Interop

Let’s get this into C#!

We’ve already established the functions we need:

  1. CGEventCreateKeyboardEvent
  2. CGEventPost
  3. CFRelease

CoreFoundation and CoreGraphics can both be found in /System/Library/Frameworks. More specifically, we can find the dynamic libraries at /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation and /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics.

Let’s use DllImport to expose these functions.

1
2
3
4
5
6
7
8
public enum CGKeyCode : ushort {
    // ...
}

// Note that these snippets use unsafe code - if you can't use that, use IntPtr instead of void*, etc.

[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
public static extern unsafe void* CGEventCreateKeyboardEvent(void* source, CGKeyCode key, bool keyDown);
1
2
3
4
5
6
7
8
public enum CGEventTapLocation : ushort {
    cghidEventTap = 0,
    cgSessionEventTap = 1,
    cgAnnotatedSessionEventTap = 2
}

[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
public static extern unsafe void* CGEventPost(CGEventTapLocation tap, void* eventRef);
1
2
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern unsafe void CFRelease(void* typeRef);

Basic Usage

As a small example, let’s rewrite the C++ sample from the beginning:

1
2
3
var e = CGEventCreateKeyboardEvent((void*)0x0, (CGKeyCode)33, true);
CGEventPost(CGEventTapLocation.cghidEventTap, e);
CFRelease(e);

Footnotes

  1. https://developer.apple.com/documentation/coregraphics/cgeventtaplocation 

This post is licensed under CC BY 4.0 by the author.

Trending Tags