Vincent Gable’s Blog

August 19, 2010

The Most Useful Objective-C Code I’ve Ever Written

Actually, it’s the most useful code I’ve extended; credit for the core idea goes to Dave Dribin with his Handy NSString Conversion Macro.

LOG_EXPR(x) is a macro that prints out x, no matter what type x is, without having to worry about format-strings (and related crashes from eg. printing a C-string the same way as an NSString). It works on Mac OS X and iOS. Here are some examples,

LOG_EXPR(self.window.screen);

self.window.screen = <UIScreen: 0x6d20780; bounds = {{0, 0}, {320, 480}}; mode = <UIScreenMode: 0x6d20c50; size = 320.000000 x 480.000000>>

LOG_EXPR(self.tabBarController.viewControllers);

self.tabBarController.viewControllers = (
“<UINavigationController: 0xcd02e00>”,
“<SavingsViewController: 0xcd05c40>”,
“<SettingsViewController: 0xcd05e90>”
)

Pretty straightforward, really. The biggest convenience so far is having the expression printed out, so you don’t have to write out a name redundantly in the format string (eg. NSLog(@"actionURL = %@", actionURL)). But LOG_EXPR really shows it’s worth when you start using scalar or struct expressions:

LOG_EXPR(self.window.windowLevel);

self.window.windowLevel = 0.000000

LOG_EXPR(self.window.frame.size);

self.window.frame.size = {320, 480}

Yes, there are expressions that won’t work, but they’re pretty rare for me. I use LOG_EXPR every day. Several times. It’s not quite as good as having a REPL for Cocoa, but it’s handy.

Give it a try.

How It Works

The problem is how to pick a function or format string to print x, based on the type of x. C++’s type-based dispatch would be a good fit here, but it’s verbose (a full function-definition per type) and I wanted to use pure Objective-C if possible. Fortunately, Objective-C has an @encode() compiler directive that returns a string describing any type it’s given. Unfortunately it works on types, not variables, but with C99 the typeof() compiler directive lets us get the type of any variable, which we can pass to @encode(). The final bit of compiler magic is using stringification (#) to print out the literal string inside LOG_EXPR()‘s parenthesis.

The Macro, Line By Line

1 #define LOG_EXPR(_X_) do{\
2 	__typeof__(_X_) _Y_ = (_X_);\
3 	const char * _TYPE_CODE_ = @encode(__typeof__(_X_));\
4 	NSString *_STR_ = VTPG_DDToStringFromTypeAndValue(_TYPE_CODE_, &_Y_);\
5 	if(_STR_)\
6 		NSLog(@"%s = %@", #_X_, _STR_);\
7 	else\
8 		NSLog(@"Unknown _TYPE_CODE_: %s for expression %s in function %s, file %s, line %d", _TYPE_CODE_, #_X_, __func__, __FILE__, __LINE__);\
9 }while(0)
  1. The first and last lines are a way to put {}‘s around the macro to prevent unintended effects. The do{}while(0); “loop” does nothing else.
  2. First evaluate the expression, _X_, given to LOG_EXPR once, and store the result in a _Y_. We need to use typeof() (which had to be written __typeof__() to appease some versions of GCC) to figure out the type of _Y_.
  3. _TYPE_CODE_ is c-string that describes the type of the expression we want to print out.
  4. Now we have enough information to call a function, VTPG_DDToStringFromTypeAndValue() to convert the expression’s value to a string. We pass it the _TYPE_CODE_ string, and the address of _Y_, which is a pointer, and has a known size. We can’t pass _Y_ directly, because depending on what _X_ is, it will have different types and could be of any size.
  5. VTPG_DDToStringFromTypeAndValue() returns nil if it can’t figure out how to convert a value to a string.
  6. Everything went well, print the stringified expression, #_X_, and the string representing it’s value, _STR_.
  7. otherwise…
  8. The expression had a type we can’t handle, print out a verbose diagnostic message.
  9. See line 1.

The VTPG_DDToStringFromTypeAndValue() Function

See the source in VTPG_Common.m:

It’s derived from Dave Dribin‘s function DDToStringFromTypeAndValue(), and is pretty straightforward: strcmp() the type-string, and if it matches a known type call a function, or use +[NSString stringWithFormat]:, to turn the value into a string.

The First Step Twords Fixing Your Macro Problem is Admitting it…

So yeah, maybe I went a little wild with macros here…

But it took out some WET-ness of the original code, and prevents me from accidentally mixing up types in a long wall of ifs, eg.

else if (strcmp(typeCode, @encode(NSRect)) == 0)
{
    return NSStringFromRect(*(NSRange *)value);
}
else if (strcmp(typeCode, @encode(NSRange)) == 0)
{
    return NSStringFromRect(*(NSRange *)value);
}

If I were cool, I’d use NSDictionarys to map from the @encode-string to an appropriate format string or function pointer. This is conceptually cleaner; less error-prone than using macros; and almost certainly faster. Unfortunately, it gets a little tricky with functions, since I need to deference value into the proper type.

One final note from my testing, I could do away with the strcmp()s, because directly comparing @encode string pointers (eg if(typeCode == @encode(NSString*)) works. I don’t know if it will always work though, so relying on it strikes me as a profoundly Bad Idea. But maybe that bad idea will give someone a good idea.

Limitations

Arrays

C arrays generally muck things up. Casting to a pointer works around this:

char x[14] = "Hello, world!";
//LOG_EXPR(x); //error: invalid initializer
LOG_EXPR((char*)x); //prints fine

__func__

Because it is a static const char [], __func__ (and __FUNCTION__ or __PRETTY_FUNCTION__) need casting to char* to work with LOG_EXPR. Because logging out a function/method call is something I do frequently, I use the macro:

#define LOG_FUNCTION()	NSLog(@"%s", __func__)

long double (Leopard and older)

On older systems, LOG_EXPR won’t work with a long double value, because @encode(long double) gives the same result as @encode(double). This is a known issue with the runtime. The top-level LOG_EXPR macro could detect a long double with if((sizeof(_X_) == sizeof(long double)) && (_TYPE_CODE_ == @encode(double))). But I doubt this will ever be necessary.

I haven’t actually written any code that uses long double, because I use NSDecimal, or another base-10 number format, for situations that require more precision than a double.

Scaling and Frameworks

Growing LOG_EXPR to handle every type is a lot of work. I’ve only added types that I’ve actually needed to print. This has kept the code manageable, and seems to be working so far.

The biggest problem I have is how to deal with types that are in frameworks that not every project includes. Projects that use CoreLocation.framework need to be able to use LOG_EXPR to print out CoreLocation specific structs, like CLLocationCoordinate2D. But projects that don’t use CoreLocation.framework don’t have a definition of the CLLocationCoordinate2D type, so code to convert it to a string won’t compile. There are two ways I’ve tried to solve the problem

Comment-out framework-specific code

This is pretty self-explanatory, I’ll fork VTPG_Common.m and un-comment-out code for types that my project needs to print. It works, but it’s drudgery. Programmers hate that.

Hardcode type info

The idea is to hard-code the string that @encode(SomeType) would evaluate to, and then (since we know how SomeType is laid out in memory) use casting and pointer-arithmetic to get at the fields.

For example:

//This is a hack to print out CLLocationCoordinate2D, without needing to #import <CoreLocation/CoreLocation.h>
//A CLLocationCoordinate2D is a struct made up of 2 doubles.
//We detect it by hard-coding the result of @encode(CLLocationCoordinate2D).
//We get at the fields by treating it like an array of doubles, which it is identical to in memory.
if(strcmp(typeCode, "{?=dd}")==0)//@encode(CLLocationCoordinate2D)
	return [NSString stringWithFormat:@"{latitude=%g,longitude=%g}",((double*)value)[0],((double*)value)[1]];

This Just Works in a project that includes CoreLocation, and doesn’t mess up projects that don’t. Unfortunately it’s horribly brittle. Any Xcode or system update could break it. It’s not a tenable fix.

Areas for Improvement

If there’s some type LOG_EXPR can’t handle that you need, please jump right in and improve it!

When I have time, I plan to write a general parser for @encode()-strings. This will let me print out any struct, which mostly solves the type-defined-in-missing-framework problem, and would let LOG_EXPR Just Work with types from all kinds of POSIX/C libraries.

Using LOG_EXPR() in Your Project

Download VTPG_Common.m and VTPG_Common.h from my github repository, and add them to your Xcode project.

Now just add the line #import "VTPG_Common.h" to your prefix file (named <ProjectName>_Prefix.pch by default), after the #ifdef __OBJC__, for example:

#ifdef __OBJC__
    #import <Foundation/Foundation.h>
    // maybe other files, depending on project  template...
    #import "VTPG_Common.h"
#endif

Now LOG_EXPR() will work everywhere in your project.

17 Comments »

  1. Here’s the complete struct handler from a similar (but incomplete) hack:
    http://gist.github.com/539041
    It’s a basic recursive descent parser design. DecodeValue() (not included) has the same signature as DecodeStruct(), looks at encoding[*cursor] and dispatches to other functions with the same signature – much like VTPG_DDToStringFromTypeAndValue(), except a full parser only needs to look at one character for dispatch, so it uses a switch statement.

    Comment by Ahruman — August 19, 2010 @ 5:02 pm

  2. Oh yeah, maybe I should point out why it’s incomplete. ;-) Apart from problematic types like bit fields, it doesn’t deal with padding – for instance, in struct { char a; void *b; } b is pointer-aligned. My intended fix was to use a flag to DecodeValue() to indicate it was needed, plus a bunch of nasty ABI-specific rules to handle the details.

    Comment by Ahruman — August 19, 2010 @ 5:18 pm

  3. How about removing the final semi-colon after while (0)? See What’s the best way to write a multi-statement macro?

    Comment by Sigjuice — August 20, 2010 @ 12:10 pm

  4. Thanks Sigjuice, good catch!

    The worst part is that I knew it was the right thing to do, but I’m so used to there being a semi-colon at the end of a do{}while loop, that I never noticed it when I looked at the code.

    …Now that it’s fixed, I wonder how many of my projects will “break” because I forgot the ; at the end of a LOG_EXPR().

    Comment by Vincent Gable — August 20, 2010 @ 12:37 pm

  5. @Ahruman,

    Thanks for the pointer to the code, and especially for explaining what problems you ran into :-). Keeping up with the ABI specifics of every iOS device is sounds scary. If there’s a simple way to detect probable padding then demanding struct-specific ToString() functions for those cases might end up being simpler over all.

    Comment by Vincent Gable — August 20, 2010 @ 12:55 pm

  6. It’s not as hard as it sounds. Each architecture has a padding requirement for each primitive type, and generally it’s “round up to a multiple of the size of the type” except in some cases for types bigger than a pointer.

    The easiest way would probably be to stick the type handler in a struct { handler function pointer, unsigned alignment, char signature }, put those in an array, and bsearch them; the alignments would be declared as a list of constants in #ifs for each architecture. You might need a special case for nested structs, though; I’m not sure offhand whether all architectures align structs based on the first member.

    Comment by Ahruman — August 20, 2010 @ 2:29 pm

  7. Well, I’ve fished mine out of the experiment pile, made it actually work and given it a simple interface like yours. There are several limitations listed in the header, but I think it handles everything that can currently be encoded by @encode(). (There are several things that can’t be.) There are probably interesting failure cases with complex aggregates.

    It doesn’t work on PPC-64 or ARM, but this should be a simple matter of finding the alignment enums using the FindAlignment.c tool in the repo.

    Also, it’s really, really ugly. It even uses exceptions for internal control flow. Ugh.

    Comment by Ahruman — August 21, 2010 @ 2:17 pm

  8. …that is to say, everything that can be encoded other than bitfields. I just couldn’t be bothered with those.

    Comment by Ahruman — August 21, 2010 @ 2:20 pm

  9. Oops, forgot the link. Duh. Anyway, I’ve now blagulated it.

    Incidentally, my macro has no problem with strings, but does fail on literal numbers or function results – it requires an lvalue.

    Comment by Ahruman — August 21, 2010 @ 2:45 pm

  10. Looks very useful. What’s the license on this code? Thanks!

    Comment by Jay P — August 23, 2010 @ 4:51 pm

  11. All my code that I share is Public Domain. Do with it what you will. I always like attribution of course :-). But there’s no legal requirement.

    Comment by Vincent Gable — August 23, 2010 @ 6:41 pm

  12. Thanks for sharing this :-) , very interesting description of the Macro.

    Comment by Nicolas — September 10, 2010 @ 1:44 pm

  13. Is it possible to make LOG_EXPR( ) accept multiple arguments? I’ve tried to do it with va_args, but since that needs a type for every argument it wouldn’t work.

    Comment by jonas — September 22, 2010 @ 7:57 pm

  14. I’m getting compile errors on — I think — a CoreGraphics dependency (this project doesn’t use CG). Opened issue on github. Let me know if you need more info.

    Comment by Clay Bridges — September 25, 2010 @ 11:07 am

  15. Hi all,

    For those interested by this great piece of code, I’ve forked Vincent’s GitHub repo to extract only the LOG_EXPR out of its common code. It can be found here:

    https://github.com/MonsieurDart/LOG_EXPR

    Let me know if this is useful for you…

    Comment by MonsieurDart — April 24, 2012 @ 11:48 am

  16. Thanks Monsieur for the update, I pulled the old VTPG_Commons from an old Xcode project and it wasn’t compiling under the version of Xcode I have installed (4.3.2). This fixed it, and its also just the code I needed for LOG_EXPR.

    Comment by Charles Feduke — April 26, 2012 @ 8:00 pm

  17. Has anyone figured out a way to do something like LOG_EXPR while debugging in the LLDB command line interface? LLDB isn’t very smart about printing types, and in some cases just garbles it completely. Look at this case:

    Here’s what happens when I type in LLDB:

    (lldb) p (CLLocationCoordinate2D)[self mapSetPointLatLon]
    (CLLocationCoordinate2D) $4 = {
    (CLLocationDegrees) latitude = 42.4604
    (CLLocationDegrees) longitude = 42.4604
    (double) easting = 42.4604
    (double) northing = -71.5179
    }

    Here’s what happens (at the same breakpoint) when I compile

    LOG_EXPR(self.mapSetPointLatLon);

    into my code:

    2013-01-26 14:02:17.555 S6E11[79116:c07] self.mapSetPointLatLon = {latitude=42.4604,longitude=-71.5179}

    Notice the redundant and wrong lines added by lldb.

    Just for thoroughness, at the same breakpoint if I try to invoke LOG_EXPR from the command line, this is what happens:

    (lldb) expr LOG_EXPR(self.mapSetPointLatLon);
    error: use of undeclared identifier ‘LOG_EXPR’
    error: 1 errors parsing expression
    (lldb)

    Comment by Phill Apley — January 26, 2013 @ 2:13 pm

RSS feed for comments on this post.

Leave a comment

Powered by WordPress