On code injection on MacOSX High Sierra - continued

nil

This the second post in the series on MacOSX code injection using MIP. The latter makes it very trivial to inject code into existing application, let's focus on the following use case: use newly introduced Apple's API for dark appearance windows. For the special case of Terminal.app.

For this, we need to understand a little bit about an Objective-C/Cocoa application lifecycle. I won't make a fuss about it but for the sake of what follows, just consider that it implies many design patterns, and that windows are managed by NSWindowController.

Objective-C has that awesome feature that is, you can extend existing classes using categories. It then ends in finding a hook to replace a relevant method, in this case, - (void)windowDidLoad.

The implementation

Using MIP, code injection ends up in writing a few line of codes compiled as a Bundle, and the corresponding Info.plist in order for MIP's injector to know when to act.

Let's start with the category interface, that simply declares a method that will be used in place of another by the Objective-C runtime.

// Version: $Id$
//
//

// Commentary:
//
//

// Change Log:
//
//

// Code:

#import <AppKit/AppKit.h>

// ///////////////////////////////////////////////////////////////////
//
// ///////////////////////////////////////////////////////////////////

@interface NSWindowController(DarkTerminal)

- (void)injected_windowDidLoad;

@end

//
// DarkTerminal.h ends here

Let's continue with it's implementation. A + (void) load class method is loaded whenever the category is discovered. It replaces the stock implementation with the custom one.

// Version: $Id$
//
//

// Commentary:
//
//

// Change Log:
//
//

// Code:

#import <Cocoa/Cocoa.h>

#import <objc/runtime.h>

#import "DarkTerminal.h"

// ///////////////////////////////////////////////////////////////////
//
// ///////////////////////////////////////////////////////////////////

@implementation NSWindowController(DarkTerminal)

+ (void)load
{
    method_exchangeImplementations(class_getInstanceMethod(self, @selector(windowDidLoad)),
                                   class_getInstanceMethod(self, @selector(injected_windowDidLoad)));
}

- (void)injected_windowDidLoad
{
    NSLog(@"MIP - injected_windowDidLoad");

    self.window.titlebarAppearsTransparent = true;
    self.window.titleVisibility = NSWindowTitleVisible;
    self.window.styleMask |= NSWindowStyleMaskTexturedBackground;
    self.window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
}

@end

//
// DarkTerminal.m ends here

Finally, the mandatory Bundle's Info.plist. Note the MIP* directives.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>MIPUseBlacklistMode</key>
  <false/>
  <key>MIPBundleNames</key>
  <array>
    <string>com.apple.Terminal</string>
  </array>
  <key>MIPExecutableName</key>
  <string>Terminal</string>
  <key>CFBundleExecutable</key>
  <string>DarkTerminal</string>
  <key>CFBundleIdentifier</key>
  <string>local.DarkTerminal</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>DarkTerminal</string>
  <key>CFBundlePackageType</key>
  <string>BNDL</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleVersion</key>
  <string>1</string>
</dict>
</plist>

Using the Makefile as shown on Github, installs the Bundle is MIP's standard location.

Note

Beware that code injection is risky and can lead to undefined behavior. In this case, the hooked method - (void)windowDidLoad has a default empty implementation, which makes our approach safe. However when it is not the case, method swizzling is a preferred pattern, even though I did not get it to work yet.