Adding Inspectors to Xcode
So last time I blogged about getting data from the target app into gdb. While I considered this to be the hard part of this project I figured that getting the data from gdb to Xcode is way harder. While I think nothing is impossible, I tried to get things running and finally succeeded.
Attached to this post is a zip file with pre-compiled plugins and their sources, as well as the test-project that i’ll use in this post to explain how everything works.
So where should I start? Well, lets start with the current debugging techniques. I created a small app that will execute a method ‘doIt’ after a short delay. Everything is implemented in a subclass of NSApplication like this:
- (void)awakeFromNib { [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(doIt) userInfo:nil repeats:NO]; } - (void)doIt { NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; NSImage* iconImage = [NSApp applicationIconImage]; NSArray* array = [NSArray arrayWithObjects:@"string1", @"string2", @"string3", infoDict, nil]; Class c = [self class]; NSArray* windows = [NSApp windows]; NSLog(@"arry is: %p = %@...windows = %@", array, [array description], [windows description]); }
So this app basically does nothing. But methods like doIt are normal Cocoa methods with objects like arrays, dictionaries or images. The output produced by this method is quite a lot of text. If the data of iconImage would have been added to this array, the output would have been even more. To find useful information in such an output is pretty hard and distracting, but I guess every developer encountered such a scenario already.
If I add a breakpoint at the last line (the one with NSLog()), then I can see the objects in the debugger. But what do I see? I see the pointer-values of each object, but no information whatsoever about their content. If I expand the little arrow next to the pointer i’ll see the isa-pointer and can go deeper into the dark, but it’ll probably not help finding the valued information.
The simple problem is that Xcode only knows the pointers, and doesn’t have the objects themselves. It also can’t just ask the objects for their data (like asking the array for its objects), because they simply don’t exist in Xcode. Thanks to virtual memory, every app in OS X has theoretically 4 GB of RAM for itself. Each app starts its ram at offset 0x1000, so how should Xcode access the other apps memory? There are ways to do this and gdb allows for this, but it’s not a simple task. On intel macs this got even harder.
However, inspecting the values of normal C or C++ structures and classes is not as difficult as inspecting Objective-C objects. Pointers can be dereferenced and arrays can be inspected item-by-item because in C the data has a certain structure, known to the debugger. Objective-C objects are more object oriented. They’re encapsulating the data like objects should do. One can’t tell where an array is storing it’s objects. This is Ok, because nobody should care. Every object only knows about itself and other objects only get the data by asking this object. So if I need some object from an array, I ask the array for this object. I don’t care about where this object is actually located in memory. While this is cool for developing software, it creates the debugging dilemma we’re facing at the moment.
If Xcode can’t ask objects for their data, how should it get to the data? We need to get the objects sent to Xcode and then ask them. This would be great, if all objects are able to encode themselves into data-objects. Unfortunately most objects are not able to do this. The solution would be to ask them in their own application, and then send the data we get from them to Xcode, so it can be displayed.
That’s exactly the way I went. First I created an input manager and this adds the ability of providing data for Xcode to all objects. This data is then read from Xcode and handed over to the inspectors to display them. The problem with input managers is that they don’t load at awakeFromNib, or finishLaunching. They just load some time after this. But as input managers are just normal bundles, these can also be loaded at runtime with system-functions. So no need to fear, no input manager will need to be installed :-)! The only purpose of this bundle is to extend NSObject with some methods and define the BSDebugObject classes in the target-applications.
So with the debug-extension bundle loaded, the objects know how to create debug objects. What are these debug objects? They’re wrapper objects that stores four values:
- a number with the pointer of its target object
- the description of this object
- a short description like “<ClassName pointer>”
- the object itself (as a pointer)
When encoding such an object it will only encode the first three values. As normal objects typically don’t encode, they are just not encoded with the debug object. Arrays are encoded with a different debug object. This array debug object encodes each of its contained objects as a debug object and thus only debug objects are encoded in the end. Dictionaries are encoded in the same way.
After all objects are encoded, the data is provided to gdb as a structure like {size,pointer} where ‘size’ is the amount of data available at ‘pointer’. This data needs to be read by gdb or Xcode and then a BSInspector can inspect this data.
My first approach was to read the data with gdb and send it to Xcode through an NSConnection. While this connection was pretty fragile, it was working. Well it only worked every now and then, and recently it didn’t work at all. Somehow I could request the data through the gdb console of Xcode, but I couldn’t do that in another thread in gdb which was used by the NSConnection. And so I ditched that approach and went for another way of getting the data from gdb to Xcode.
As Xcode communicates with gdb a lot and doesn’t seem to have this kind of problems, I tried to hook up there and try my luck with Xcode’s way. After a lot of digging and searching I finally found out how everything worked and I modified my Xcode plugin. It turned out that I didn’t need a gdb plugin anymore and could just do everything from within Xcode.
So the Xcode plugin loads this data from the target application and sends it to an inspector. If an array/dictionary is supplied, then the array inspector opens, or the dictionary inspector respectively. For any other object, the standard text-inspector opens and shows the object’s description.
While I think that arrays and dictionaries are the most important objects that need a custom inspector, it’s possible to create an inspector for every other class, too. I created a plugin interface for both, the debug extension bundle and the inspectors. Because of the dual nature of this whole stuff, a plugin always consists of two bundles: the debug object and the inspector. The debug extension bundle will load all available debug object plugins and the inspector will load all inspector-plugins.
An example of such a plugin is the BSImageInspector I made. It can open a window which shows the NSImage of the target-application. It is pretty simple and only made out of a handful of methods, so check it out. The only problem with this plugin is, that it’s REALLY slow, so use with caution. If you inspect an NSImage, you’ll see at your cpu-usage that it’s still working, so relax and get a coffee. As soon as it finishes, the inspector window will show the image.
The problem is these fast chips that intel provides. In order to get the data from the target app to Xcode I have to read the data byte by byte (I think reading a 4-byte word would be faster here, but that’s changing the order of the bytes, so it’s not applicable). Each is received as a string that needs to be parsed first. So an image with 500kb is not really a big thing, but 500*1024 strings need to be parsed and this takes ages. I’m sure there might be a better way than the current, but the for normal objects like arrays and dictionaries, this seems fast enough. In fact I already tried to read the memory directly with vm_read(), but due to the aforementioned problem with task_for_pid(), this doesn’t work on intel macs without root privileges.
To simplify things even more, I also made an application to install all these plugins and bundles. The tool is called BSInspectorInstaller and is declared as a viewer for .xcplugin, .bsInspector and .bsDebugObject files. You can just double-click the files and the installer will ask you for each if it should be installed for the current user, or the whole system. Previous versions of the plugin will be moved to the trash before. If anyone out there is good with icon-design, please create better icons for this stuff, the current ones are not really good and hard to see when finder is in list-view.
As I said before, the source-code is included in the attached zip file, so everyone is welcome to modify it, it’s released under MIT-License. I take no responsibility for any problems you encounter. If this stuff crashes your computer and you lose data, this is up to you. I didn’t put any code in these plugins that could harm your computer.
I hope that this thing could be some kind of open source project. I think I won’t work on this stuff much more, cause it’s working to the point that I need it to work 🙂
Have fun with it.
Karsten
BSInspectors.zip
[Update 1 07.03.07 01:00 CET] i just updated the zip file, there was some bug in the gdb-sequences that could crash Xcode
[Update 2 07.03.07 01:44 CET] i just updated the zip file again, there was some serious bug in the plugin-interface preventing the image inspector to work at all.
April 23rd, 2007 at 20:17
Wonderful! I really good use such an ability as you’ve developed, and will give it a shot (I haven’t yet downloaded, only read your text). Bravo and many thanks for making this and the source code available.
September 3rd, 2008 at 3:51
I installed the plugin, but I see no difference. I’m not even really sure what I’m supposed to see now it is installed. I tried clicking on variables and stuff in the debugger in Xcode, but it just looks the same. And is there any gdb command line interface?
September 3rd, 2008 at 6:38
as far as i know, this only works with Xcode 2 so far. I didn’t have time yet to port it to Xcode 3.