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!