How To Create an Xcode Plugin: Part 1/3
Get started with exploring app internals as you learn about developing Xcode plugins and some LLDB tips in this first of a three-part tutorial series. By Derek Selander.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Create an Xcode Plugin: Part 1/3
30 mins
x86 Register Dumpster Diving
Now that you’re an official assembly register wizard, it’s time to revisit DVTBezelAlertPanel
‘s initWithIcon:message:parentWindow:duration:
. Hopefully you haven’t moved from the break on this method. If you have, re-run the child Xcode to get there again. Remember, you’re searching for a clue that this class is the class responsible for showing Xcode’s Build Succeeded alert.
While stopped in initWithIcon:message:parentWindow:duration
, type the following in LLDB:
(lldb) re re
This command is an abbreviated way of saying register read
, which will print the significant registers available on your machine.
Using what you’ve learned about reading x86_64 registers, examine the register responsible for the message:
parameter and the 4th objc_msgSend param. Do the contents match the expected alert string?
[spoiler title=””]Yes, by looking at the register $rcx, you can see that this item correlates to the message
parameter displayed on the alert.
Enter the following in the LLDB console to explore this further:
(lldb) po $rcx
Build Failed
Looks like this is the culprit.[/spoiler]
Augment the $rcx register with a new string and see if the alert changes to be 100% sure:
(lldb) po [$rcx class]
__NSCFConstantString
(lldb) po id $a = @"Womp womp!";
(lldb) p/x $a
(id) $a = 0x000061800203faa0
(lldb) re w $rcx 0x000061800203faa0
(lldb) c
The application then resumes. Note the augmented alert message that you changed while in the debugger. You can now safely make the assumption that this class is associated with the build alerts. Took a bit to figure it out, didn’t it?
Code Injection
You’ve found the class that you’re interested in. Now it’s time to inject code to augment DVTBezelAlertPanel
‘s behavior to display a lovely Rayrolling face when an alert occurs.
Time to use method swizzling!
Since you could potentially swizzle numerous methods from different classes, it would be best to use a Category on NSObject
to create a convenience method to perform setup logic.
Select File\New\File… and select the OS X\Source\Objective-C File template. Name the file MethodSwizzler and make it of file type Category and class NSObject.
Open NSObject+MethodSwizzler.m and replace its contents with the code below:
#import "NSObject+MethodSwizzler.h"
// 1
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzler)
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod
{
Class cls = [self class];
Method originalMethod;
Method swizzledMethod;
// 2
if (isClassMethod) {
originalMethod = class_getClassMethod(cls, originalSelector);
swizzledMethod = class_getClassMethod(cls, swizzledSelector);
} else {
originalMethod = class_getInstanceMethod(cls, originalSelector);
swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
}
// 3
if (!originalMethod) {
NSLog(@"Error: originalMethod is nil, did you spell it incorrectly? %@", originalMethod);
return;
}
// 4
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
Taking each numbered comment in turn:
- This is the magical header responsible for declaring the functions used for method swizzling.
-
isClassMethod
indicates if the methods are class methods or instance methods. - When you don’t have the help of the compiler to autocomplete your methods, it’s easy to misspell them. This is a check to make sure that you are declaring your
SEL
s accurately. - This is the function that will switch your implementations around.
Declare swizzleWithOriginalSelector:swizzledSelector:isClassMethod
in NSObject+MethodSwizzler.h like so:
#import <Foundation/Foundation.h>
@interface NSObject (MethodSwizzler)
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod;
@end
Now it’s time to actually swizzle! Create another new Category called Rayrolling_DVTBezelAlertPanel which inherits from NSObject.
Replace the contents of NSObject+Rayrolling_DVTBezelAlertPanel.m with the following:
#import "NSObject+Rayrolling_DVTBezelAlertPanel.h"
// 1
#import "NSObject+MethodSwizzler.h"
#import <Cocoa/Cocoa.h>
// 2
@interface NSObject ()
// 3
- (id)initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4;
@end
// 4
@implementation NSObject (Rayrolling_DVTBezelAlertPanel)
// 5
+ (void)load
{
static dispatch_once_t onceToken;
// 6
dispatch_once(&onceToken, ^{
// 7
[NSClassFromString(@"DVTBezelAlertPanel") swizzleWithOriginalSelector:@selector(initWithIcon:message:parentWindow:duration:) swizzledSelector:@selector(Rayrolling_initWithIcon:message:parentWindow:duration:) isClassMethod:NO];
});
}
// 8
- (id)Rayrolling_initWithIcon:(id)icon message:(id)message parentWindow:(id)window duration:(double)duration
{
// 9
NSLog(@"Swizzle success! %@", self);
// 10
return [self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];
}
@end
Broken down, the code is relatively straightforward:
- Make sure to import the method that can enable swizzling.
- You forward declare all the methods that you intend on using. Although this is not required, it makes the compiler play nice by autocompleting your code. In addition, this trick suppresses any warnings about undeclared methods.
- This is the actual private method you will be swizzling with.
- Since you don’t want to redeclare a private class, you’re opting for a category instead.
- This is the heart of the code injecting “trick”. You’ll perform the injecting in
load
.load
is unique in that it has a “to-many relationship”. That is, multiple categories of the same class can all implement aload
command and have them all execute. - Since
load
can be called multiple times, you usedispatch_once
. - This uses the
NSObject
category method you implemented earlier. Note that you retrieve the private class dynamically at runtime usingNSClassFromString
. - This is the replacement method for the original one. It’s good practice to use a unique namespace convention that only you could have come up with.
- This is a basic test to see if the swizzling worked by printing out to the console.
- Since you’re swizzling this method with the original one, when you call the swizzled method, it will still call the original method. This means you could add code before or after the original method is called, or even go so far as to change the parameters passed to the original function… which you’ll do in just a moment.
Congratulations! You’ve now successfully injected code into a private method of a private class! Build the parent Xcode, and then use the build of the child Xcode to see the added console message which was swizzled in.
Now it’s time to replace all alert images with the Rayrolling face. Download the lovely image created by our resident image swizzler Crispy from here and add the image into the Xcode project. Make sure to select Copy Items if Needed.
Navigate back to Rayrolling_initWithIcon:message:parentWindow:duration
and change its content to the following:
- (id)Rayrolling_initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4
{
if (arg1) {
NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.raywenderlich.Rayrolling"];
NSImage *newImage = [bundle imageForResource:@"IDEAlertBezel_Generic_Rayrolling.pdf"];
return [self Rayrolling_initWithIcon:newImage message:arg2 parentWindow:arg3 duration:arg4];
}
return [self Rayrolling_initWithIcon:arg1 message:arg2 parentWindow:arg3 duration:arg4];
}
This method now checks that an image was passed to the original method, and replaces it with the Rayrolling image. Note that you had to use the +[NSBundle bundleWithIdentifier:]
to load the image because it’s not contained in your mainBundle
.
Build and run the project; quit out of all instances Xcodes and restart fresh.
Beautiful! :]