Reverse Engineering CoreDisplay API
Background
In this article, we’ll review methods for changing display brightness on a Mac.
I personally find it irritating when my display shuts off, or my computer locks down, while I’m still looking at it. This happens frequently when watching videos, following a recipe or slowly trying to grok something on the screen. Therefore, I have my display set to turn off at an interval that’s over 1 hour. I know this makes me a bad person.
I wrote a daemon that dims my display’s backlight if my computer becomes idle and my face isn’t within the viewing area of my built-in FaceTime webcam.
You can check it out here.
Adjusting Brightness on macOS
Unlike iOS, there is no convenient screen brightness API for macOS.
Here’s how you’d change your screen’s brightness programmatically with Swift in iOS, where someBrightnessFloat
is a float
in the interval [0.0, 1.0]
:
UIScreen.mainScreen().brightness = someBrightnessFloat
It would be convenient if that API were available, but to accomplish this on a Mac, we’ll need to jump through some hoops.
Eventually, through some research, I came across the relevant APIs in the IOKit
Framework. Great!
Bridging Between Objective-C and Python
I decided to build my daemon with pyObjc
and Python 3.7.0, simply because developing command line daemons with Python is fun.
pyObjc
tends to have wrappers for Apple’s development frameworks, except none exist for IOKit
. That’s fine, we can wrap it with pyObjc
or even ctypes
if we’re desperate.
Let’s look at the relevant Objective-C code. In Objective-C, we’d want to do something along these lines:
float brightness = ourBrightnessFloatGoesHere;
CGDirectDisplayID display = CGMainDisplayID();
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(display));
IODisplaySetFloatParameter(service, kNilOptions, CFSTR(kIODisplayBrightnessKey), brightness);
We’ll need to translate that to Python code with pyObjc
.
BridgeSupport
With ctypes
, we need to provide the library with correct arg_types
and return types. This can be tedious.
Thankfully, BridgeSupport files can be generated from the library with macOS system tools. These files keep function signatures and typing information in an XML structure. pyObjc
can use BridgeSupport data so we don’t have to manually annotate each function we want to call.
We’ll need to grep
around IOKit.framework
to find the values we need, which are:
kIOMasterPortDefault
, a constant.kIODisplayBrightnessKey
, a constant.IOServiceMatching
, a function.IOServiceGetMatchingService
, a function.IODisplaySetFloatParameter
, a function.
After poking around the framework, we’ll find that information we need is located in graphics/IOGraphicsLib.h
, IOKitLib.h
, and graphics/IOGraphicsTypes.h
.
Now, we’ll need to generate a BridgeSupport file.
export BRIDGESUPPORT=".bridgesupport"
gen_bridge_metadata -c "-l/System/Library/Frameworks/IOKit.framework/IOKit
-I/System/Library/Frameworks/IOKit.framework/Headers/graphics
-I/System/Library/Frameworks/IOKit.framework/Headers" \
/System/Library/Frameworks/IOKit.framework/Headers/graphics/IOGraphicsLib.h \
/System/Library/Frameworks/IOKit.framework/Headers/IOKitLib.h \
/System/Library/Frameworks/IOKit.framework/Headers/graphics/IOGraphicsTypes.h > "$BRIDGESUPPORT"
If you’re on MacOS 10.13+, you’ll get an error. gen_bridge_metadata
will look in a directory that doesn’t exist, so we’ll need to symlink the directory we need to the location that doesn’t yet exist.
export TOOLCHAINS_DIR="/Applications/Xcode.app/Contents/Developer/Toolchains"
export WORKING_DIR=`pwd`
# fix issue where gen_bridge_metadata cannot find toolchain
# https://trac.macports.org/ticket/54939
if defaults read loginwindow SystemVersionStampAsString | grep '10.13'
then
cd "$TOOLCHAINS_DIR"
if [ ! -e OSX10.13.xctoolchain ]
then
sudo ln -s XcodeDefault.xctoolchain OSX10.13.xctoolchain
fi
cd "$WORKING_DIR"
fi
Rerun the gen_bridge_metadata
command again, and we’ll get a nice BridgeSupport file.
We aren’t in the clear yet. pyObjc
supports a certain dialect in BridgeSupport files, and the one we generated will have values the library doesn’t understand. We’ll need to scrub the BridgeSupport file of those values.
IOTimingInformation
has a weird signature, and it’s irrelevant to our use case, so let’s get rid of it.
sed -i "" -e "/name='IOTimingInformation'/d" "./$BRIDGESUPPORT"
Changing Brightness
Great, now we’ve generated a BridgeSupport file for the utilities in IOKit
we’ll use. Now, we can write some application code.
Let’s import our CFString
manipulation libraries:
from CoreFoundation import CFStringCreateWithCString, CFRelease, kCFStringEncodingASCII
Then, let’s write a function to import IOKit
.
from __future__ import annotations
from typing import Callable, Any
from pathlib importh Path
import objc
IOKIT_FRAMEWORK: str = "/System/Library/Frameworks/IOKit.framework"
BRIDGESUPPORT_FILE: str = ".bridgesupport"
DISPLAY_CONNECT: bytes = b"IODisplayConnect"
def import_iokit(iokit_location: str = IOKIT_FRAMEWORK,
namespace: dict[str, Any] = None):
if namespace is None:
namespace = globals()
bridgesupport_file = Path(BRIDGESUPPORT_FILE).read_text()
objc.parseBridgeSupport(bridgesupport_file,
namespace,
objc.pathForFramework(iokit_location))
When we run import_iokit()
it will read the BridgeSupport file into a buffer, and pyObjc
will parse and load our wrapped functions and values into global namespace.
Now, let’s write a function that uses IOKit
to change our Mac’s brightness:
def set_brightness_iokit(brightness: float) -> int:
brightness /= 100
service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching(DISPLAY_CONNECT))
return IODisplaySetFloatParameter(service,
0,
kIODisplayBrightnessKey,
brightness)
Calling set_brightness_iokit(50)
will dim our display to 50% brightness. Great!
Undocumented CoreDisplay API
Well, if you’re on macOS 10.13+, things aren’t so great. You’ll notice that brightness adjustments using IOKit
’s API reset after a certain period. That is, on macOS 10.13, your brightness will go back to its default setting after N seconds/minutes/hours.
This is because Apple has deprecated IOKit
’s brightness functionality in favor of a private, undocumented API. The words ‘private undocumented API’ should instill fear in any developer who encounters them on their journey to solve a problem. They’re the Here be dragons of software development and dragons are less scary than undocumented features.
After doing some digging, it appears nobody has documented this API, nor is there a convenient Python bridge for it. The closest I’ve come to seeing developers use this API in the open is the hardcore macOS hacking community, who have patched the framework to support older hardware with newer displays & resolutions. Nobody’s touched it in an effort to change screen brightness.
Normally, this is the point where I throw my hands up and say, “well, that was a waste of time” and focus my energy somewhere else. However, I built out a pretty good solution around the IOKit
enabled functionality and I am not ready to give that up.
Reverse Engineering CoreDisplay
First, let’s see if there’s anything we can glean from this framework. We could break out a hex editor, disassembler and all sorts of fun things, but I don’t want to get too crazy, lest it be for naught.
We need to determine which functions handle setting brightness, or if they even exist.
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ grep -rn Brightness .
Binary file ./CoreDisplay matches
Cool, while there aren’t any header files or code we can use, there seems to be something related to brightness in CoreDisplay.framework
.
Looking in CoreDisplay.framework
, we don’t have much to work with: PList
files and compiled libraries.
Let’s dig into CoreDisplay.framework/CoreDisplay
a little further:
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ strings CoreDisplay | grep Brightness | wc -l
49
There are 49 relevent lines. This is a good thing. Let’s take a deeper look:
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ strings CoreDisplay | grep Brightness | grep Set
void CoreDisplay::Display::SetUserBrightness(double)
void CoreDisplay::Display::SetLinearBrightness(double)
void CoreDisplay::Display::SetLinearBacklightBrightness(double)
void CoreDisplay::Display::SetBacklightBrightness(double)
void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID, double)
void CoreDisplay_Display_SetDynamicLinearBrightness_Server(CGDirectDisplayID, double)
Skip SetLinearBrightness on offline display 0x%08x
void CoreDisplay::Display::SetDynamicLinearBrightness(double)
SetDynamicLinearBrightness
void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID, double)
void CoreDisplay_Display_SetLinearBrightness_Server(CGDirectDisplayID, double)
void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID, double)
void CoreDisplay_Display_SetUserBrightness_Server(CGDirectDisplayID, double)
Skip SetUserBrightness on offline display 0x%08x
void CoreDisplay_Display_SetAutoBrightnessIsEnabled(CGDirectDisplayID, bool)
void CoreDisplay_Display_SetAutoBrightnessIsEnabled_Server(CGDirectDisplayID, bool)
Skip SetAutoBrightnessIsEnabled on offline display 0x%08x
void CoreDisplay::Display::SetAutoBrightnessIsEnabled(bool)
void CoreDisplay::EDRDisplay::ScheduleSetBrightnessIn(double)_block_invoke
ScheduleSetBrightnessIn_block_invoke
We’ve got function signatures! This was easy, no need for breaking out heavier tools and methods.
Inspecting futher, it seems like we’ve got a few functions that are relevant to the problem at hand:
void CoreDisplay::Display::SetUserBrightness(double)
void CoreDisplay::Display::SetLinearBrightness(double)
void CoreDisplay::Display::SetLinearBacklightBrightness(double)
void CoreDisplay::Display::SetBacklightBrightness(double)
void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID, double)
void CoreDisplay_Display_SetDynamicLinearBrightness_Server(CGDirectDisplayID, double)
Skip SetLinearBrightness on offline display 0x%08x
void CoreDisplay::Display::SetDynamicLinearBrightness(double)
SetDynamicLinearBrightness
void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID, double)
void CoreDisplay_Display_SetLinearBrightness_Server(CGDirectDisplayID, double)
void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID, double)
That last line looks pretty relevant to me, so let’s try to call that function from Python.
Using ctypes
to Call CoreDisplay’s Private API
Python’s ctypes
gives us a convenient foreign function interface. Here’s how we’d use it for our problem:
from ctypes import CDLL, c_int, c_double
CoreDisplay = CDLL("/System/Library/Frameworks/CoreDisplay.framework/CoreDisplay")
Now, we’ve got a handle on CoreDisplay
through ctypes
.
ctypes
needs to know how to call CoreDisplay_Display_SetUserBrightness
CoreDisplay.CoreDisplay_Display_SetUserBrightness.argtypes = [c_int, c_double]
Let’s test it out:
CoreDisplay.CoreDisplay_Display_SetUserBrightness(0, 0.5)
Yes! This successfully changes the brightness on display 0
to 50%!
Finally, we’ll wrap it in a function so that we can easily call it elsewhere from Python :
def set_brightness_coredisplay(display: int, brightness: float) -> int:
brightness /= 100
return CoreDisplay.CoreDisplay_Display_SetUserBrightness(display, brightness)
Conclusion
We’ve learned how to use pyObjc
and BundleSupport files to wrap macOS libraries that aren’t conveniently wrapped for us. We poked around undocumented libraries to locate functionality we’re interested in, and we’ve also learned how to use ctypes
to call out to those private macOS frameworks.
Apparently, we’ve also learned that there’s no limit to the amount of hoops I’ll jump through to make inconsequential things work.
In conclusion, we built the foundation for my daemon that uses facial recognition to manipulate your screen’s brightness. You should check it out!