TweetFollow Us on Twitter

Function Logging
Volume Number:8
Issue Number:7
Column Tag:C Workshop

Function Logging in Think C

For debugging, optimizing, and better understanding a program’s execution path

By Eric Shapiro, Ann Arbor, Michigan

About the author

Eric Shapiro’s works include Business Simulator®, EzTape®, and two new QuickTime™ programs: Spectator™, a screen recorder from Baseline Publishing and VideoBeep™, a silly Control Panel from Sound Source Unlimited.

Eric has taught Mac programming seminars for Apple and developed the course materials for Apple’s Macintosh Device Driver seminar. He is best known, however, as the author of everyones favorite singing trash can, The Grouch.

Overview

This article presents a simple way to log all function calls into a text file using Think C 5.0. The technique presented here is useful for debugging, optimizing, and obtaining a better understanding of a program’s execution path.

History

In the old days before source level debugging, function logging was an important way to debug Mac programs. On one of my first Mac projects, I remember sending function names out the serial port to a machine running a terminal program in order to figure out where the program was crashing. At MacHack this year, Marshall Clow from Hewlett Packard showed how to log function calls to MacsBug in MPW. Preferring Think C to MPW, I decided that I’d do a similar project for Think C. This article presents the results.

To Use the Logger

If all you want to do is use the function logger, here’s what you need to do:

• Add the file LogToFile.c to your Think C project.

• Add the file ANSI or ANSI-Small to your project (for some string utilities).

• Turn both the profiling and stack frame options on for the project.

• Call LogInit during program initialization (with appropriate parameters).

• Call LogDInit during program de-initialization.

• Recompile your code.

That’s all there is to it! You can control logging more precisely as follows:

• The following line of code turns profiling on for an entire file (if it appears outside of all functions) or for a single routine (if it appears within a function):

/* 1 */
 #pragma options( profile, force_frame )

• Likewise, the following line of code turns profiling off:

/* 2 */
 #pragma options( !profile, !force_frame ) 

• You can determine if logging is active at compile-time using:

/* 3 */
      #if _option( profile )

• Call SetLogState( true ) or SetLogState( false ) to turn logging on or off during program execution. You can do this via a menu selection, for example.

• Call SetLogFlush( true ) or SetLogFlush( false ) to turn flushing on or off during program execution. Performance suffers considerably when flushing is active, but it can help find the location where a crash occurs.

Note: Writing to disk when debugging a program can be a dangerous activity. Be sure you have adequate backups before trying any new debugging technique.

Note: By default, the function logger handles calling depths up to 100 levels. This should be sufficient for all but the most recursive programs. You can change the constant kMaxLogDepth to control the maximum depth.

How The Logger Works

When profiling is active, Think C generates a call to the function _profile_ at the beginning of every function call. Actually, only functions that generate stack frames call _profile_, which is why we turn on the stack frame option as well (stack frames are described below). The _profile_ function has the following form:

/* 4 */
void _profile_( void *functionNamePString )

The first version of _profile_ I wrote simply logged the function name to a file. While this is adequate for some uses, I really wanted to create an indented list of function calls so a programmer can tell exactly who called each function. To do this, our code needs to be notified when a function exits as well as when it is entered. Here’s where life gets a bit complicated.

Unfortunately, Think C’s profiler doesn’t generate a function call whenever a function exits. I looked at Think C’s profiler source code and used a similar technique for my logger. The technique involves replacing return addresses on the stack with the address of our own routine. When a function exits via an rts instruction, it will unknowingly call our exit handler. Our exit handler will decrement a depth counter, write some text to the log file, and jump to the original return address. To understand how we trace back the stack to find return addresses, we need to know exactly how Think C handles stack frames.

Stack Frames

Before and during function execution, space must be allocated for the following items:

• The function’s parameters

• The caller’s return address

• The function’s local variables

• Additional information, such as register contents, that the function may want to store

The first two items, the parameters and return address, are placed on the program stack by the calling code. Space for the the local variables and register storage is also allocated on the stack, but by the function itself. For convenience, the compiler uses register A6 to reference local variables while the stack pointer itself can be lowered and raised during function execution. The previous value of A6 is also placed on the stack so the compiler can easily clean up the stack when a function exits.

If FuncA calls FuncB, and FuncB calls FuncC, the stack looks like that shown in Figure 1.

As you can see, register A6 always points to the start of a linked list of previous A6 values. This is how debuggers such as MacsBug can back-trace function calls (using the SC command). In the _profile_ routine, we need to find the return address of the function that called the function that called us. The assembly language code finds this value on the stack at 4 + the previous A6 value. We save the old return address in a global array before modifying the stack so we know where to return to when our exit handler is called.

Note: If Think C’s force stack frame option is not active, the compiler doesn’t generate a stack frame for functions that have no local variables or parameters. Our _profile_ code is not called for these functions.

Sample Output

The code shown below produces the very simple log file also shown below. Note that the recursion in Func1 works properly. The braces in the output allow you to select an entire function using Think C’s Balance command.

/* 5 */

void DoSomething( void )
{
 Func1();
 Func2( 10 );
}

void Func1( void )
{
 static short  x = 0;
 if ( ++x < 2 )
 Func1();
}

short Func2( short x )
{
 return( 2 * x );
}

Here is the sample output from the code:

/* 6 */

DoSomething
{
 Func1
 {
 Func1 {}
 }
 Func2 {}
}

A simple object-oriented example produces the following output. Note how easy it is to follow the calling sequence of the constructors and destructors for both a base class and a child class.

/* 7 */

DoSomeOopStuff
{
 BaseClass::BaseClass {}
 ChildClass::ChildClass {}
 ChildClass::MiscFunc
 {
 BaseClass::MiscFunc {}
 }
 ChildClass::~ChildClass {}
 BaseClass::~BaseClass {}
}

Summary

A fairly simple technique was given to log function names to a text file for any Think C project, including object-oriented ones. The technique can be used to debug and optimize programs as well as to investigate a program’s runtime activity.

Possible future additions include:

• Put more information into the log file, such as the stack and register contents.

• Make the output look better, perhaps by drawing a graph of the program’s runtime activity.

• Do some validity checking on the heap and file id to make sure that the system is in a reasonably safe state for writing.

• Optionally log information to MacsBug using DebugStr() instead of to the log file.

• Support for interrupt-driven code and trap patches

This would involve setting some semaphores, restoring the global variable context, and changing the I/O calls to be asynchronous. I don’t think it would be very difficult, but as most Mac programmers know, nothing is difficult until you have to implement it.

Good luck, and let me know if you find any bugs.

Listing LogToFile.c
/*
 LogToFile.c
 © 1992 Rock Ridge Enterprises. All Rights Reserved.

 Requires Think C 5.0x

 To use this file:
 1a) Turn on the profiling and stack frame options for your    
 entire project from the Edit/Options/Debugging dialog.
 
 1b) Or, if you prefer, add the following line to your files:
 #pragma options( profile, force_frame )
 Place it within a function to profile just that function,
 within a file for just that file, or in a header file.

 2)Call LogInit at program initialization. For example:
 LogInit( NULL, true, false, true );
 
 3)Call LogDInit at program de-initialization. For example:
 LogDInit();

 4)You can call SetLogState( true ) or SetLogState( false )
 to turn logging on and off dynamically.
*/

#include <string.h>
#include <files.h>
#include <errors.h>
#include <memory.h>
#include <pascal.h>

#include “LogToFile.h”

#define kMaxLogDepth 100  // maximum 100 calls deep

 // create Think C text files as output
#define kThinkCreatorID   ‘KAHL’
#define kTextFileType‘TEXT’

 // strings written to output file
#define kTabString ((void*) “\t” )
#define kReturnString((void*) “\r” )
#define kOpenBraceString  ((void*) “{“)
#define kCloseBraceString ((void*) “}”)
// exiting a function that called nobody
#define kSimpleExitString “ {}”  

 // write this string the top of every log file
#define kStringForNewFile ((void*) “” )

 // write this string between program runs when appending to
 // the log file
#define kStringForReusedFile((void*)
 “\r\r*****************************\r”)

 // the largest buffer we’ll need (+ a little extra)
#define kMaxStringLength  ( kMaxLogDepth + 100 )

 // the default output file is on the root of the boot disk
#define kBootVol -1
#define kRootDir 2L
#define kDefaultPName“\PLog File.txt”

 // don’t profile our profile handler
#pragma options( !profile )

/*
 Permanent storage for this file
*/
static short   gLogFileID = 0;
// vRefNum of log file’s disk
static shortgLogFileVol = -1; 
// true = call FlushVol after every write
static Boolean   gAlwaysFlush = false; 
// true = logging is active
static Boolean   gLogActive = false; 
static FSSpec  gDefaultFileLoc = { kBootVol, kRootDir,
 kDefaultPName };

/*
 Info on the calling chain
*/
// how many calls deep are we?
static long gDepth = 0;   
// the return addresses
static void *gStack[ kMaxLogDepth ]; 
// are braces needed for this function?
static Boolean   gNeedBraces[ kMaxLogDepth ];

/*
 Internal routine prototypes
*/
void    _profile_( void *pFuncName );
void    StringPtoC( void *source, void *target );
OSErr   WriteOpenBrace( short howDeep );
OSErr   WriteFunctionEntry( void *pString );
OSErr   WriteFunctionExit( void );
OSErr   WriteOpenBrace( short howDeep );

/**********************************
 LogInit - Call at program start
 fileLoc- set to NULL or a valid FSSpec for the                
 output file
 deleteOld- true => truncate old log file
 alwaysFlush- true => call FlushVol a lot (better log          
  file if you crash)
 startLogging  - true => turn logging on, false => don’t       
  log yet
**********************************/
OSErr LogInit( FSSpec *fileLoc, Boolean deleteOld,
 Boolean alwaysFlush, Boolean startLogging )
{
 OSErr  err = noErr;
 BooleancreatedFile = false;
 
 if ( !fileLoc )
 // use default if user doesn’t specify one
 fileLoc = &gDefaultFileLoc;

 // in case user calls init twice
 if ( !gLogFileID )
 {
 /*
 Create the file & open the data fork for writing.
 */
 err = FSpCreate( fileLoc, kThinkCreatorID,
 kTextFileType, 0 );
 if ( !err )
 createdFile = true;
 
 err = FSpOpenDF( fileLoc, fsRdWrPerm, &gLogFileID );
 if ( err ) goto DONE;
 }

 /*
 Clear out the file if the user requests it.
 */
 if ( deleteOld )
 {
 err = SetEOF( gLogFileID, 0L );
 if ( err ) goto DONE;
 }

 /*
 Append to the file
 */
 err = SetFPos( gLogFileID, fsFromLEOF, 0 );
 if ( err ) goto DONE;

 /*
 Setup the globals and write ‘****’ to the file.
 */
 gAlwaysFlush = alwaysFlush;
 gLogActive = startLogging;
 gLogFileVol = fileLoc->vRefNum;
 
 // write a header to the file
 if ( deleteOld || createdFile )
 err = WriteLogString( kStringForNewFile );
 else
 err = WriteLogString( kStringForReusedFile );
 
 DONE:
 if ( err )
 LogDInit();

 return( err );
}

/**********************************
 LogDInit - Call at program exit
**********************************/
OSErr LogDInit( void )
{
 OSErr  err;

 if ( !gLogFileID )
 return( openErr );
 
 /*
 Close the file and flush the data to the disk.
 */
 err = FSClose( gLogFileID );
 if ( !err )
 err = FlushVol( NULL, gLogFileVol );
 
 /*
 Clear out the globals so we know we’re not active.
 */
 gLogFileID = 0;
 gLogActive = false;

 return( err );
}

/**********************************
 SetLogState - Call to start or restart logging. 
 Returns previous state.
**********************************/
Boolean SetLogState( Boolean startLogging )
{
 Booleanresult;
 
 result = gLogActive;
 gLogActive = startLogging;

 return( result );
}

/**********************************
 SetLogFlush - Call to start or restart flushing. 
 Returns previous state.
**********************************/
Boolean SetLogFlush( Boolean startFlushing )
{
 Booleanresult;
 
 result = gAlwaysFlush;
 gAlwaysFlush = startFlushing;

 return( result );
}

/**********************************
 ClearLogFile - Call to zero out the log file
**********************************/
OSErr ClearLogFile( void )
{
 OSErr  err = noErr;
 OSErr  err2;
 
 if ( !gLogFileID )
 return( openErr );
 
 err = SetEOF( gLogFileID, 0L );

 if ( gAlwaysFlush )
 {
 err2 = FlushVol( NULL, gLogFileVol );
 if ( !err )
 err = err2;// return the first error to occur
 }

 return( err );
}

/**********************************
 WriteLogString - Write a C string to the log file
**********************************/
OSErr WriteLogString( void *cString )
{
 OSErr  err = noErr;
 OSErr  err2;
 long   numBytes;
 
 if ( !gLogFileID )
 return( openErr );

 /*
 Write the data to the file
 */
 numBytes = strlen( cString );
 err = FSWrite( gLogFileID, &numBytes, cString );
 
 /*
 Flush the volume if we always flush
 */
 if ( gAlwaysFlush )
 {
 err2 = FlushVol( NULL, gLogFileVol );
 if ( !err )
 err = err2;
 }

 return( err );
}

/**********************************
 StringPtoC - Convert a pascal string to a c string
**********************************/
static void StringPtoC( void *source, void *target )
{
 BlockMove( source, target, 1 + *( (unsigned char*)source ));
 PtoCstr( target );
}

/**********************************
 WriteFunctionEntry - called by _profile_ whenever a function  is entered

 The following string is written to the output:
 <CR> + <TABS> + FunctionName
 Where <CR> is a carriage return and <TABS> indicates 1        
 tab per depth.
**********************************/
static OSErr WriteFunctionEntry( void *pString )
{
 Str255 cString;
 unsigned char   theString[ kMaxStringLength ];
 long   count;
 OSErr  err;

 // convert func name to c string
 StringPtoC( pString, cString );   

 // start with a carriage return
 strcpy( (void*)theString, kReturnString );  
 
 // 1 tab for each level
 for ( count=0; count<gDepth; count++ )
 strcat( (void*)theString, kTabString );
 
 // the function name
 strcat( (void*)theString, (void*)cString );

 // write the string
 err = WriteLogString( theString );
 return( err );
}

/**********************************
 WriteFunctionExit - called whenever a function is exited
 by our exit handler

 If this function called another function, write the
 following string:
 <CR> + <TABS> + }
 Otherwise, write:
 {}
 Where <CR> is a carriage return and <TABS> indicates 1
 tab per depth.
**********************************/
static OSErr WriteFunctionExit( void )
{
 OSErr  err = noErr;
 long   count;
 unsigned char   theString[ kMaxStringLength ];
 
 if ( gNeedBraces[ gDepth ] )
 {
 // start with a carriage return
 strcpy( (void*)theString, kReturnString );
 // indent 1 tab for each level
 for ( count=0; count<gDepth; count++ )
 strcat( (void*)theString, kTabString );
 // then a close-brace
 strcat( (void*)theString, kCloseBraceString );
 }
 else
 {
 // just write “exit”
 strcpy( (void*)theString, kSimpleExitString );
 }

// write the string
 err = WriteLogString( theString );
 return( err );
}

/**********************************
 WriteOpenBrace - adds some tabs and an open brace to the
 output

 The following string is written to the output:
 <CR> + <TABS> + {
 Where <CR> is a carriage return and <TABS> indicates 1
 tab per depth.
**********************************/
static OSErr WriteOpenBrace( short howDeep )
{
 OSErr  err;
 unsigned char   theString[ kMaxStringLength ];
 
 // start with a return
 strcpy( (void*)theString, kReturnString );  
 // 1 tab per level deep
 while( howDeep- > 0 )
 strcat( (void*)theString, kTabString );
 // add an open-brace
 strcat( (void*)theString, kOpenBraceString );

 err = WriteLogString( theString );
 return( err );
}

/**********************************
 _profile_ - Called every time a function is entered
 
 This more complicated version does the following:
 1) Prints the function name to the file in
    an indented list
 2) Saves the return address of the caller’s
    caller into gStack[]
 3) Modifies the stack so that the caller returns
    to our code and not to its caller.
 4) Prints exit info when the function is exited and
    then jumps to the correct return address.
**********************************/
void _profile_( void *pFuncName )
{
 OSErr  err;

 if ( !gLogFileID ) return; // output file not opened
 if ( !gLogActive ) return; // logging is off
 if ( gDepth >= kMaxLogDepth ) return; // we’re too deep

 /*
 We have to put an open brace in the output if the parent      
 function hasn’t called any other functions until now.
 */
 if ( gDepth > 0 )
 if ( !gNeedBraces[gDepth-1] )
 {
 gNeedBraces[gDepth-1] = true;
 err = WriteOpenBrace( gDepth-1 );
 if ( err ) return;
 }

 // write the function name
 err = WriteFunctionEntry( pFuncName );
 if ( err ) return;

 gNeedBraces[ gDepth ] = false;
 

 /*
 Save the return address that the caller will return to.
 Modify the stack so that the caller will return to us         
 instead.
 */
 asm
 {
 ; gStack[ gDepth ] = return address where caller
 ; will return to

 ; A1 = &gStack[ gDepth ]
 lea.l  gStack, A1
 move.l gDepth, D0
 lsl.l  #2, D0
 adda.l D0, A1
 
 move.l (A6), A0 ; A0 = A6 from previous call
 move.l 4(A0), (A1); gStack[ gDepth ] = ret Addr
 
 ; Change the return address on the stack to
 ; point to our code
 lea    @FuncExit, A1
 move.l A1, 4(A0)

 addq.l #1, gDepth ; we’re one level deeper now
 
 ; return to caller
 unlk   A6
 rts
 }

 /*
 This code is executed when a profiled function exits
 */
 FuncExit:
 asm
 {
 move.l D0, -(SP); save return value onto stack
 subq.l #1, gDepth ; we’re one level more shallow

 ; write exit info to the file
 jsr    WriteFunctionExit

 ; get the real return address from our array
 ; and jump to it
 
 ; A0 = &gStack[ gDepth ]
 lea.l  gStack, A0
 move.l gDepth, D0
 lsl.l  #2, D0
 adda.l D0, A0
 
 move.l (SP)+, D0; restore return value
 move.l (A0), A0 ; A0=real return address
 jmp    (A0); jump to real return address
 }
}
 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Apple GarageBand 10.1 - Complete recordi...
The new GarageBand is a whole music creation studio right inside your Mac -- complete with keyboard, synths, orchestral and percussion instruments, presets for guitar and voice, an entirely... Read more
Duplicate Annihilator 5.7.7 - Find and d...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator... Read more
OS X Server 4.1.3 - For OS X 10.10 Yosem...
Designed for OS X and iOS devices, OS X Server makes it easy to share files, schedule meetings, synchronize contacts, develop software, host your own website, publish wikis, configure Mac, iPhone,... Read more
Firefox 39.0 - Fast, safe Web browser. (...
Firefox offers a fast, safe Web browsing experience. Browse quickly, securely, and effortlessly. With its industry-leading features, Firefox is the choice of Web development professionals and casual... Read more
pwSafe 4.1 - Secure password management...
pwSafe provides simple and secure password management across devices and computers. pwSafe uses iCloud to keep your password databases backed-up and synced between Macs and iOS devices. It is... Read more
Kodi 15.0.rc1 - Powerful media center to...
Kodi (was XBMC) is an award-winning free and open-source (GPL) software media player and entertainment hub that can be installed on Linux, OS X, Windows, iOS, and Android, featuring a 10-foot user... Read more
Coda 2.5.11 - One-window Web development...
Coda is a powerful Web editor that puts everything in one place. An editor. Terminal. CSS. Files. With Coda 2, we went beyond expectations. With loads of new, much-requested features, a few surprises... Read more
Bookends 12.5.7 - Reference management a...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Access the power of Bookends directly from Mellel, Nisus Writer Pro, or MS Word (... Read more
Maya 2016 - Professional 3D modeling and...
Maya is an award-winning software and powerful, integrated 3D modeling, animation, visual effects, and rendering solution. Because Maya is based on an open architecture, all your work can be scripted... Read more
RapidWeaver 6.2.3 - Create template-base...
RapidWeaver is a next-generation Web design application to help you easily create professional-looking Web sites in minutes. No knowledge of complex code is required, RapidWeaver will take care of... Read more

This Week at 148Apps: June 29-July 3, 20...
Into July With 148Apps How do you know what apps are worth your time and money? Just look to the review team at 148Apps. We sort through the chaos and find the apps you're looking for. The ones we love become Editor’s Choice, standing out above the... | Read more »
Sonic Runners Guide
Despite its flaws, Sonic Runners' platforming action is actually quite fun. Though it can be a little more complicated than old-school Sonic games. Here's how to make sure you're up to speed when jumping in. [Read more] | Read more »
Rage of Bahamut is Giving Almost All of...
The App Store isn't what it used to be back in 2012, so it's not unexpected to see some games changing their structures with the times. Now we can add Rage of Bahamut to that list with the recent announcement that the game is severely cutting back... | Read more »
Adventures of Pip (Games)
Adventures of Pip 1.0 Device: iOS iPhone Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: ** ONE WEEK ONLY — 66% OFF! *** “Adventures of Pip is a delightful little platformer full of charm, challenge and impeccable... | Read more »
Divide By Sheep - Tips, Tricks, and Stre...
Who would have thought splitting up sheep could be so involved? Anyone who’s played Divide by Sheep, that’s who! While we’re not about to give you complete solutions to everything (because that’s just cheating), we will happily give you some... | Read more »
NaturalMotion and Zynga Have Started Tea...
An official sequel to 2012's CSR Racing is officially on the way, with Zynga and NaturalMotion releasing a short teaser trailer to get everyone excited. Well, as excited as one can get from a trailer with no gameplay footage, anyway. [Read more] | Read more »
Grab a Friend and Pick up Overkill 3, Be...
Overkill 3 is a pretty enjoyable third-person shooter that was sort of begging for some online multiplayer. Fortunately the begging can stop, because its newest update has added an online co-op mode. [Read more] | Read more »
Scanner Pro's Newest Update Adds Au...
Scanner Pro is one of the most popular document scanning apps on iOS, thanks in no small part to its near-constant updates, I'm sure. Now we're up to update number six, and it adds some pretty handy new features. [Read more] | Read more »
Heroki (Games)
Heroki 1.0 Device: iOS Universal Category: Games Price: $7.99, Version: 1.0 (iTunes) Description: CLEAR THE SKIES FOR A NEW HERO!The peaceful sky village of Levantia is in danger! The dastardly Dr. N. Forchin and his accomplice,... | Read more »
Wars of the Roses (Games)
Wars of the Roses 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: | Read more »

Price Scanner via MacPrices.net

13-inch 1.6GHz MacBook Air on sale for $849,...
Amazon has the 2015 13″ 1.6GHz/128GB MacBook Air on sale for $849.99 including free shipping. Their price is $150 off MSRP, and it’s the lowest price available for this model. Read more
RamDisk4Mac App Helps Run Your Mac Faster And...
Ever use a RAM disk? If you’ve come to the Mac in the OS X era, likely not. The Classic Mac OS had a RAM disk function built-in, but that was dropped in the conversion to OS X. What is a RAM disk?... Read more
13-inch 1.6GHz MacBook Air on sale for $849,...
Best Buy has the 2015 13″ 1.6GHz/128GB MacBook Air on sale for $849.99 on their online store this weekend. Choose free shipping or free local store pickup (if available). Sale price for online orders... Read more
Apple Refurbished iMacs available for up to $...
The Apple Store has Apple Certified Refurbished iMacs available for up to $380 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free: - 27″ 3.5GHz 5K iMac – $1949 $... Read more
Apple refurbished 2014 13-inch Retina MacBook...
The Apple Store has Apple Certified Refurbished 2014 13″ Retina MacBook Pros available for up to $400 off original MSRP, starting at $979. An Apple one-year warranty is included with each model, and... Read more
Seagate Backup Plus Drives Feature 200GB of C...
Seagate Technology plc has announced that its Backup Plus family of external storage offerings will now include 200GB of OneDrive cloud storage, a major added value, and the addition of Lyve’s photo... Read more
Canon PIXMA MG3620 Wireless Inkjet All-in-One...
Canon U.S.A., Inc. has announced the PIXMA MG3620 Wireless (1) Inkjet All-in-One (AIO) printer for high-quality photo and document printing. Built with convenience in mind for the everyday home user... Read more
July 4th Holiday Weekend 13-inch MacBook Pro...
Save up to $150 on the purchase of a new 2015 13″ Retina MacBook Pro at the following resellers this weekend. Shipping is free with each model: 2.7GHz/128GB MSRP $1299 2.7GHz/... Read more
27-inch 3.5GHz 5K iMac on sale for $2149, sav...
Best Buy has the 27″ 3.5GHz 5K iMac on sale for $2149.99. Choose free shipping or free local store pickup (if available). Sale price for online orders only, in-store prices may vary. Their price is $... Read more
Apple now offering refurbished 2015 11-inch...
The Apple Store is now offering Apple Certified Refurbished 2015 11″ MacBook Airs as well as 13″ MacBook Airs (the latest models), available for up to $180 off the cost of new models. An Apple one-... Read more

Jobs Board

*Apple* Music Producer - Apple (United State...
**Job Summary** Apple Music seeks a Producer to help shepherd some of the most important content and editorial initiatives within the music app, with a particular focus Read more
Editor, *Apple* News - Apple (United States...
**Job Summary** Editor, Apple News The Apple News team is looking for passionate, knowledgeable editors to help identify and deliver the best in breaking national, Read more
*Apple* Watch SW Application Project Manager...
**Job Summary** The Apple Watch software team is looking for an Application Engineering Project Manager to work on new projects for Apple . The successful candidate Read more
Engineering Project Manager - *Apple* Searc...
**Job Summary** Apple 's new Spotlight Suggestions service provides fast, relevant search results from the Inte et in Spotlight and Safari on iOS and OS X. We are looking Read more
Business Development Manager - *Apple* Pay...
**Job Summary** Apple Pay is seeking an experienced relationship manager to support the ongoing management of partners for the Apple Pay platform. This position will Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.