Reverse engineering Xcode with dtrace
Both OS X and iOS tend to have a love-hate relationship with Xcode. Crashing, lacking solid refactoring tools, some UX failures, and so on.
For me, it is/was the lack of a good autocomplete that really ticks me off. Xcode’s autocomplete is prefix-based, so Xcode will only show completions where the start of the completion matches your incomplete word. This is rather frustrating, as the prefix for completion items tends to be the same.
For example, if you wanted to complete
NSAccessibilityRoleDescriptionForUIElement, you’d probably go:
- and finally
Of course, you can always use arrow keys to scroll through the list at some point to select it, but using arrow keys isn’t a great solution.
It would be great if Xcode supported fuzzy autocompletion (like Sublime Text, AppCode, etc etc). I mentioned this to @alanjrogers one Friday afternoon and he told me I should write an Xcode plugin for it.
Step 1: Making a plugin for Xcode 5
I found BlackDog Foundry’s Creating an Xcode 4 Plugin which got me off to a good start where I was able to get code loaded into Xcode 5, with a couple of Xcode 5-specific
Info.plist UUID tweaks from KFCocoaPodsPlugin.
Step 2: Figure out what to hook into
class-dump, I dumped
Xcode.app’s classes and started searching for likely places to hook into. I stumbled across the
DVTTextCompletion* class cluster which was a good place to start. Before you can use the dumped headers, you need to remove the
- (void).cxx_destruct method declaration if it exists. You also need to remove some
#import statements that cause the build to fail, like
#import <objc/NSObject.h> and
JRSwizzle with CocoaPods and started to poke at some classes I thought would be a good start, like
Unfortunately, this didn’t get me very far. Picking random methods to swizzle was a time consuming process which involved lots of restarting of Xcode. I needed a better way to figure out what I should hook into.
Some googling turned up the
NSObjCMessageLoggingEnabled environment variable, where it would log every message send to
/tmp/msgSend-<pid>. Unfortunately, starting up Xcode generated about 15 million lines of data (a whopping 659MB!!), so that clearly wasn’t what I was after.
dtrace to the rescue
I found a little
dtrace snippet on Stackoverflow which would let you probe message sends of a certain class, which gave me a much better indication of what I should be looking at (after tracing
DVTTextCompletionSession), however it only output the method calls without any context.
Further googling turned up an article by Jon Haslam on DTrace and Visualisation, where he described using the
flowindent option on
dtrace to show a call tree. This was better, but it was limited to showing the method name only unless you used
printf to show the class. It was messy and not ideal.
I eventually found this script that essentially reimplemented
flowindent but with a better visualisatino of Objective-C method calls.
I modified it to provide better indentation and allow scoping down to a certain class. Script is below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
sudo ./trace_msg_send.sh -p <pid of app> <objc class>
This produced a much better looking and easier to understand call tree:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
A quick breakdown of a
dtrace probe is defined by
provider:module:function:name. In the above script, I use
$target is a macro variable which is filled in by
pid passed in by the command line via the
$2 and so forth are the arguments passed into
sudo ./trace_msg_send.sh -p 12345 DVTTextCompletionSession
- Provider: objc12345
- Module: DVTTextCompletionSession
- Function: [everything]
- Name: entry
This will match all method entry events within
DVTTextCompletionSession for pid 12345.
dtrace supports wildcards:
* for multiple characters,
? for a single character.
For a more detailed rundown on
dtrace, see Hooked on DTrace
Step 3: Find out what Xcode is doing
A quick swizzle of
setAllCompletions: showed that Xcode sets the completion list to whatever is autocompletable at a particular scope. On a new line, it would set all the constants, C methods, macros, etc etc, which ended to be about 40,000. When autocompleting on
[self on one of my plugin classes, it set the completion list to a much more managable 278.
After some digging, I found that
_setFilteringPrefix:forceFilter: did what I expected and was called every time the user typed. A few hours of trial and error later, I managed to hook up Xcode’s own
IDEOpenQuicklyPattern class to perform fuzzy filtering of the completions, but I still relied on calling the original fuzzy matching method because otherwise the autocompletion list window would not update correctly. This was adding 30-50ms of execution time on the main thread, which is not ideal, so the next step is to figure out what Xcode is doing behind the scenes to update this.
DVTTextCompletionListWindowController is probably what I wanted to look at, but using the same script wasn’t useful as it was filled with
-[DVTTextCompletionListWindowController tableView:objectValueForTableColumn:row:] calls, and it was missing a return call so the indentation kept growing.
I wrote another script that let me filter out the method calls I didn’t want polluting my trace, and added the ability to only trace message sends within a certain method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
I added timestamps to the result because the output kept ending out of order.
I ran with
sudo ./trace_within_method_and_filter.sh -ppidof xcode
> output.txt, then fixed the order and removed timestamps with
cat output.txt | sort -n | cut -c 17-200.
Output with calls to stuff to properties (
showingWindow) with no inner method calls removed for brevity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
The call stack is missing some return traces and I’m not sure why yet, but we’ve found some relevant methods:
_updateSelectedRow. I wasn’t having issues with the row updating, so I grabbed a reference to
[self _listWindowController] and call
-_updateCurrentDisplayState. This solved the display issue!
dtrace is pretty awesome, but editing
dtrace scripts was annoying. If I have to do more of this, I’d probably write a wrapper using ruby-dtrace to help automate filtering etc.
FuzzyAutocomplete Plugin for Xcode
The end product is FuzzyAutocomplete (github.com/chendo/FuzzyAutocompletePlugin), which works in Xcode 5. It shouldn’t conflict with any existing plugins like KSImageNamed as most plugins expose additional completion items rather than change the filtering.