Filename and Line Number with NSLog: Part II

In the previous post I demonstrated a simple debug class that I wrote to wrap some additional code around NSLog. The code allows for displaying additional information beyond the date/time stamp and process ID that NSLog outputs, specifically, the filename which calls the debug routine, and the line number where the call was invoked. I also added a few additional configuration options including an option to disable all debug messages.

The figure below shows the debug routine called from within two separate source files. Notice the filename and line number references:

There is also an option to show the entire file path for the source code where the debug statement was included:

Interface Code:

Let’s start with the interface definition, which is shown below. I begin with the two configurable options: showing the full path to the filename, and enabling/disabling debug output. This is followed by a variadic macro definition which is the debug statement used inside an application to display messages. Continue reading to see an example of how to call/use this macro.

The variadic macro wraps the call to the singleton class DebugOutput, calling the method ‘output’. __FILE__ and __LINE__ are predefined macros in the C language that expand to the current file and line number, respectively.

Notice how I enable/disable all debug messages using the #define DEBUG statement. When debug is off (0), all references to the debug statements expand to “nothing.”

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
/************************************************************************
 * DebugOutput.h
 *
 * Definitions for DebugOutput class
 ************************************************************************/
 
// Show full path of filename?
#define DEBUG_SHOW_FULLPATH YES
 
// Enable debug (NSLog) wrapper code?
#define DEBUG 1
 
#if DEBUG
  #define debug(format,...) [[DebugOutput sharedDebug] output:__FILE__
                            lineNumber:__LINE__ input:(format), ##__VA_ARGS__]
#else
  #define debug(format,...)
#endif
 
@interface DebugOutput : NSObject
{
}
+ (DebugOutput *) sharedDebug;
-(void)output:(char*)fileName lineNumber:(int)lineNumber
        input:(NSString*)input, ...;
@end

The last few lines define the class interface, including one class and one instance method.

Implementation Code:

The next block of code is the implementation file for DebugOutput.m:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/************************************************************************
* DebugOutput.m
*
* Singleton class for wrapping NSLog messages, adding functionality
* for displaying filename and line numbers. Also includes configuration
* options to "turn off" all debug (NSLog) messages
************************************************************************/
#import "DebugOutput.h"
 
@implementation DebugOutput
 
static DebugOutput *sharedDebugInstance = nil;
 
/*---------------------------------------------------------------------*/
+ (DebugOutput *) sharedDebug
{
  @synchronized(self)
  {
    if (sharedDebugInstance == nil)
    {
      [[self alloc] init];
    }
  }
  return sharedDebugInstance;
}
 
/*---------------------------------------------------------------------*/
+ (id) allocWithZone:(NSZone *) zone
{
  @synchronized(self)
  {
    if (sharedDebugInstance == nil)
    {
      sharedDebugInstance = [super allocWithZone:zone];
      return sharedDebugInstance;
    }
  }
  return nil;
}
 
/*---------------------------------------------------------------------*/
- (id)copyWithZone:(NSZone *)zone
{
  return self;
}
 
/*---------------------------------------------------------------------*/
- (id)retain
{
  return self;
}
 
/*---------------------------------------------------------------------*/
- (void)release
{
  // No action required...
}
 
/*---------------------------------------------------------------------*/
- (unsigned)retainCount
{
  return UINT_MAX;  // An object that cannot be released
}
 
/*---------------------------------------------------------------------*/
- (id)autorelease
{
  return self;
}
 
/*---------------------------------------------------------------------*/
-(void)output:(char*)fileName lineNumber:(int)lineNumber input:(NSString*)input, ...
{
  va_list argList;
  NSString *filePath, *formatStr;
 
  // Build the path string
  filePath = [[NSString alloc] initWithBytes:fileName length:strlen(fileName)
                               encoding:NSUTF8StringEncoding];
 
  // Process arguments, resulting in a format string
  va_start(argList, input);
  formatStr = [[NSString alloc] initWithFormat:input arguments:argList];
  va_end(argList);
 
  // Call NSLog, prepending the filename and line number
  NSLog(@"File:%s Line:%d %@",[((DEBUG_SHOW_FULLPATH) ? filePath :
           [filePath lastPathComponent]) UTF8String], lineNumber, formatStr);
 
  [filePath release];
  [formatStr release];
}
 
@end

I’ve written this class as a singleton, the specifics for doing so are in lines 12 – 69. The workhorse of this class is the ‘output:’ method near the bottom of the code listing. It’s pretty straight forward. Essentially, using the parameters passed in, create a string that represents the file name, create a format string and call NSLog() to glue everything this together.

The code that I wrote to demonstrate the output (see the figures above) is as follows:

CreatingClasses.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>
#import "SomeClass.h"
#import "DebugOutput.h"
 
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  SomeClass *ptr = [[SomeClass alloc] initWithStrAndDate:"Fubar" date:[NSDate date]];
  NSString *str = [[NSString alloc] initWithString:@"Test string"];
  int x = 99;
 
  debug(@"ptr:%@ x:%d str:%@", ptr, x, str);
 
  [ptr release];
  [str release];
  [pool drain];
  return 0;
}

SomeClass.m

1
2
3
4
5
6
7
8
9
10
11
12
...
-(id)initWithStrAndDate: (NSString *)inString date:(NSDate *)inDate
{
  debug(@"inString:%@ inDate:%@", inString, inDate);
  if (self = [super init])
  {
    [self setStr:inString];
    [self setDate:inDate];
  }
  return self;
}
...

You can download the source code (Xcode project) for this example from here.

I want to thank Mark Dalrymple at Borkware who wrote an article on A Better NSLog() that was the inspiration for my approach.

  1. Thanks John–I’ll definitely be using this code in my own projects!

    A quick question, though: Why is it necessary to explicitly override and synchronize the +allocWithZone: method? I understand that +allocWithZone: is called by alloc (so in this case you must ensure it’s thread-safe), but your +debugShare: method is already synchronized. Couldn’t you just assign the singleton inside +debugShare: and skip overriding +allocWithZone:?

    I’m somewhat new to ObjC so if I’m missing something here, a quick explanation would be a greatly-appreciated lesson.

  2. Thanks John for an interesting article. I’m a complete Cocoa newbie but come from a programming background. I downloaded your project and gave it a whirl. This would be very useful for me while I plough through Cocoa Programming For Mac OS X. Could you quickly explain how to include this code in other projects. Thanks for your time.

  3. Chrissy,

    To use these classes, create two new files DebugOutput.h and DebugOutput.m in an Xcode project. Copy the code from above into the appropriate file.

    From within a file that you want to use the debug statement, import the debug header as follows:

    #import “DebugOutput.h”

    Then, call the debug statement as shown below changing ‘someString’ and ‘someValue’ as needed to output a debug statement:

    debug(@”someString:”, someValue);

    Hope that helps.

    John

  4. One way to archive the same functionality using only macros:

    #define debug() NSLog(@”File:%s, Line:%d : %s”, [ [ [ [NSString alloc] initWithBytes:__FILE__ length:strlen(__FILE__) encoding:NSUTF8StringEncoding] lastPathComponent] UTF8String ] ,__LINE__,__FUNCTION__);

    You can implement the other functions, like full path or other one you require, just changing the all code on the macros. One good idea is define many macros, so you can call one macro inside other.

  5. Alternatively, simply toss the following into AppName_Prefix.pch file and don’t worry about setting preprocessor macro flags in the target build (as some tutorials are suggesting). it’s the same output as NSLog, but that’s the trade off for not having to write and manage a new class/singleton.

    —————————————-
    //Toggle DebugLog() Visibility
    #define ShowDebugLogs 0

    //DebugLog() Visibility
    #if ShowDebugLogs
    #define DebugLog(…) NSLog(__VA_ARGS__)
    #else
    #define DebugLog(…)
    #endif

    //ReleaseLog() Visibility
    #define ReleaseLog(…) NSLog(__VA_ARGS__)
    —————————————-

  6. Put this in your project’s .pch file:
    [BEGIN]

    #if DEBUG_ASSERT_PRODUCTION_CODE
    #define NSLogDebug(format, …)
    #else
    #define NSLogDebug(format, …) \
    NSLog(@” %s, ” format, \
    strrchr(“/” __FILE__, ‘/’) + 1, __LINE__, __PRETTY_FUNCTION__, ## __VA_ARGS__)
    #endif

    [END]

Comments are closed.