Stone Design Stone Design
News Download Buy Software About Stone
software

OS X & Cocoa Writings by Andrew C. Stone     ©1995-2003 Andrew C. Stone

Crossing the Line
A Cocoa Programmer's Adventures in Carbon Land
©2002 Andrew C. Stone All Rights Reserved

    For the last 12 years I've been happily coding applications in Cocoa and its predecessors: Yellow Box, OpenStep and NeXTSTEP. As the object technology has matured, I have had to drop down into standard C less and less. In fact, as Cocoa caught up with innovations introduced by third party developers - such as the Foundation kit originally conceived by a company named Lighthouse Design and Object Technology's StringKit - developers could actually remove code from their projects as the provided frameworks incorporated the functionality. No longer is it 'mas macho' to code the most, instead, it's "who can get it done in the fewest lines of code?". The terser the code, the less you have to maintain between major system releases. My philosophy is to use Cocoa as much as possible, leaving the difficult and tedious work to my fellow engineers within Apple.
    
    However, Macintosh programmers now live in the schizophrenic world of two diametrically opposed programming models - the classic Carbon routines which embody procedural programming and Cocoa which embraces the object oriented paradigm. But the best thing about this schizophrenia is that you can mix and match Carbon and Cocoa code, even in the same function or method! Normally you will find me eschewing functional programming because the object model and its data abstractions protect the third party developer from low level changes that Apple may make as the OS X APIs mature. But there are situations where a Cocoa developer has to use Carbon routines because much of the richness of Carbon has not yet been exposed in the Cocoa API, and Carbon calls are the only way to get certain things done. This article will show you how to accomplish a few of these tasks such as launching a URL programmatically, opening a resource fork, and setting the type and creator of a file.

Looks Like Rain - Get Out Your Umbrella!

    OS X introduces the notion of umbrella frameworks, which are "meta-frameworks" that consist of an unspecified set of actual frameworks that get loaded at runtime. Two of the most important umbrella frameworks are the Cocoa framework and the Carbon framework. This is a clever way to allow applications to continue working even if Apple changes what code lives in each framework. Previously, a Cocoa developer would link her project to the AppKit and Foundation frameworks. By linking against the Cocoa umbrella framework, an application will have the resources and object code needed irrespective of the exact framework that contains that code.

    Applications which use both Carbon and Cocoa will have these import statements:

#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>

and you'll add the Cocoa and Carbon frameworks to your ProjectBuilder "External Frameworks and Libraries" folder in the Groups and Files outline view. The one down side of using the umbrella frameworks is that you lose access to all the .h files in the constituent frameworks that make up the umbrella. The workaround is simple - just add, for example, the CarbonCore.framework to your project and then click the bullet indicator on the left side of the outline view which indicates whether the framework is to be included. Now you can browse the headers of CarbonCore easily, but it won't be "hard" linked into the application

You can add frameworks for browsing their headers without including them in the project by clicking the bullet on the left

Programmatic Loading of a Web Site

Our first task is to provide a way to programmatically launch a user's preferred web browser and ask it to load a web address. Recently, Ken Case of Omnigroup provided an object-wrapped solution in the excellent mailing list "macosx-dev@omnigroup.com". I highly recommend that you subscribe to this mailing list because the signal to noise ratio is very high and some of the most experienced and clever Cocoa developers routinely provide great information. Visit www.omnigroup.com -> community -> mailing lists -> developer to subscribe online (http://www.omnigroup.com/community/developer/mailinglists/macosx-dev/).

In keeping with the object model, the actual code to do the work is sweetly hidden in an object, and your calling code is quite simple. You might have an action method such as this that you connect the target of a button in InterfaceBuilder:

- (void)launchStoneSite:(id)sender {
OWInternetConfig *internetConfig = [[OWInternetConfig alloc] init];
[internetConfig launchURL:@"http://www.stone.com"];
[internetConfig release];
}

OWInternetConfig, the actual class that does the work, looks like this:

// OWInternetConfig.h
#import <Foundation/Foundation.h>

@interface OWInternetConfig : NSObject {
id internetConfigInstance;
}
- (void)launchURL:(NSString *)urlString;

@end


// OWInternetConfig.m

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <HIToolbox/InternetConfig.h>
#import "OWInternetConfig.h"

@implementation OWInternetConfig

#define MY_APPLICATION_SIGNATURE FOUR_CHAR_CODE('OWEB')

- init;
{
ICInstance anInstance;

// always check that there is sufficient memory:
if (![super init])
return nil;

// get the Internet Configuration instance:
if (ICStart(&anInstance, MY_APPLICATION_SIGNATURE != noErr))
return nil;

// assign the instance to the class ivar:
internetConfigInstance = anInstance;
return self;
}

- (void)dealloc;
{
// always clean up after yourself - this not Java, folks!
ICStop(internetConfigInstance);
[super dealloc];
}

- (void)launchURL:(NSString *)urlString;
{
const char *urlCString;
long start, length;
OSStatus error;

// convert the NSString (or CFString, same thing!) to a good old char *:
urlCString = [urlString UTF8String];
start = 0;
length = strlen(urlCString);

// make the carbon call:
error = ICLaunchURL(internetConfigInstance, NULL, (Ptr)urlCString, length, &start, &length);

// be good and check if there is an error,
// and if so, raise an exception:
if (error != noErr)
[NSException raise:@"OWInternetConfigException" format:@"ICLaunchURL returned an error while launching URL %@: 0x%x (%d)", urlString, error, error];
}

@end

Core Foundation: The Ties That Bind

Both Carbon and Cocoa are built upon the same substrate - the Core Foundation. This becomes especially useful for tasks such as converting a POSIX style path name such as "/Library/Fonts/Ariel" to the Carbon FSRef file specification. If you are a Cocoa developer and want to access some of the QuickTime functionality or you need to be able to open resource forks, then you'll have to use Carbon calls.

One benefit of Cocoa is that there is a one-to-one mapping between the core C data types of CoreFoundation and the bottom level Cocoa Foundation Kit objects: string, array, dictionary and data. Moreover, you can use this mapping (also known as the Toll Free Bridge) interchangeably. For example, the string data type CFString is exactly the same as Foundation's NSString. So if a function requires an argument of CFStringRef, you can simply pass in an NSString * object.

Here's a function to return an FSSpec from a CFStringRef (remember you can pass in an NSString * if you are working at the Cocoa layer) of an absolute UNIX style path:

FSSpec posixStringToFSSpec(CFStringRef posixPath, BOOL isDirectory) {
FSRef fsRef;
FSSpec fileSpec;

// create a URL from the posix path:
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
            posixPath,
            kCFURLPOSIXPathStyle,
            isDirectory);

// check to be sure the URL was created properly:
if (url == NULL) {
printf("Can't get URL");
return(NULL);
}

// use the CF function to extract an FSRef from the URL:
if (CFURLGetFSRef(url, &fsRef) == NULL){
printf("Can't get FSRef.\n");
CFRelease(url);
return(NULL);
}

// use Carbon call to get the FSSpec from the FSRef
if (FSGetCatalogInfo (&fsRef,
kFSCatInfoNone,
NULL /*catalogInfo*/,
NULL /*outName*/,
&fileSpec,
NULL /*parentRef*/) != noErr) {

printf("Can't get FSSpec.\n");
CFRelease(url);
return(NULL);
}

// We have a valid FSSpec! Clean up and return it:
CFRelease(url);
return fileSpec;
}


Not My Type

In a recent article I wrote on generating AppleScript from any document (MacTech July 2000), at the completion of AppleScript production, I wanted to open the script in the Carbon application "Script Editor". The script is saved via NSString's method
writeToFile:atomically: and then NSWorkspace is asked to open the file. But how does NSWorkspace know which application to use to open it?

    [theScriptString writeToFile:path atomically:YES];
    [[NSWorkspace sharedWorkspace] openFile: path];

We could hard code the name of the Script Editor in the second call like this:

    [[NSWorkspace sharedWorkspace] openFile: path withApplication:@"Script Editor"];

but this has two inherent problems: the first is that Apple tends to change the name of applications between releases - that application was called ScriptEditor (no space) in a previous release! But the second and more serious problem is that we are ignoring the user's preferred application to open the Apple Script. There are some industrial strength Apple Script applications that the user may have installed and would be unhappy if the generic Script Editor was used.

The solution is to apply the Carbon magic of TYPE and CREATOR to our saved script, and then the correct application will be launched to open our script.

    [theScriptString writeToFile:path atomically:YES];
    [[NSFileManager defaultManager] setTypeString:@"TEXT" andCreatorString:@"ToyS" forPath:file];
    [[NSWorkspace sharedWorkspace] openFile: path];

Now, an astute Cocoa programmer will say "Hey - there is no such method in NSFileManager!". This is, of course, true, so we'll take advantage of Objective C's powerful "Category" feature. A Category on an object allows third party developers to add functionality to classes, even if they do not have the source code to the original class. A major caveat emptor concerning categories is that if there are more than one implementation of a method in separate categories, it is indeterminate which method will be used at runtime. This is not a problem here because we are adding a brand new method to the base NSFileManager class. Here's the code, and as a bonus, the parallel query method,
getTypeString:andCreatorString:forPath: is included:

/* NSFileManager_Extensions.h created by andrew on Tue 18-Aug-1998 */

#import <Foundation/Foundation.h>

@interface NSFileManager(NSFileManager_Extensions)

- (int)setTypeString:(NSString *)type andCreatorString:(NSString *)creator forPath:(NSString *)path;
- (int)getTypeString:(NSString **)type andCreatorString:(NSString **)creator forPath:(NSString *)path;

@end

/* NSFileManager_Extensions.m created by andrew on Tue 18-Aug-1998 */
/* Thanks again to OmniGroup! */

#import "NSFileManager-Extensions.h"
#import <sys/mount.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/attr.h>
#include <fcntl.h>

@implementation NSFileManager(NSFileManager_Extensions)

typedef struct {
long type;
long creator;
short flags;
short locationV;
short locationH;
short fldr;
short iconID;
short unused[3];
char script;
char xFlags;
short comment;
long putAway;
} OFFinderInfo;

- (int)getType:(unsigned long *)typeCode andCreator:(unsigned long *)creatorCode forPath:(NSString *)path;
{
struct attrlist attributeList;
struct {
long ssize;
OFFinderInfo finderInfo;
} attributeBuffer;
int errorCode;

attributeList.bitmapcount = ATTR_BIT_MAP_COUNT;
attributeList.reserved = 0;
attributeList.commonattr = ATTR_CMN_FNDRINFO;
attributeList.volattr = attributeList.dirattr = attributeList.fileattr = attributeList.forkattr = 0;
memset(&attributeBuffer, 0, sizeof(attributeBuffer));

errorCode = getattrlist([self fileSystemRepresentationWithPath:path], &attributeList, &attributeBuffer, sizeof(attributeBuffer), 0);
if (errorCode == -1) {
switch (errno) {
case EOPNOTSUPP:
*typeCode = 0;
*creatorCode = 0;
default:
return (errorCode);
}
} else {
*typeCode = attributeBuffer.finderInfo.type;
*creatorCode = attributeBuffer.finderInfo.creator;
}

return (errorCode);
}

- (int)setType:(unsigned long)typeCode andCreator:(unsigned long)creatorCode forPath:(NSString *)path;
{
struct attrlist attributeList;
struct {
long ssize;
OFFinderInfo finderInfo;
} attributeBuffer;
int errorCode;

attributeList.bitmapcount = ATTR_BIT_MAP_COUNT;
attributeList.reserved = 0;
attributeList.commonattr = ATTR_CMN_FNDRINFO;
attributeList.volattr = attributeList.dirattr = attributeList.fileattr = attributeList.forkattr = 0;
memset(&attributeBuffer, 0, sizeof(attributeBuffer));

getattrlist([self fileSystemRepresentationWithPath:path], &attributeList, &attributeBuffer, sizeof(attributeBuffer), 0);

attributeBuffer.finderInfo.type = typeCode;
attributeBuffer.finderInfo.creator = creatorCode;

errorCode = setattrlist([self fileSystemRepresentationWithPath:path], &attributeList, &attributeBuffer.finderInfo, sizeof(OFFinderInfo), 0);
if (errorCode == 0)
return 0;

if (errno == EOPNOTSUPP) {
#define MAGIC_HFS_FILE_LENGTH 82
unsigned char magicHFSFileContents[MAGIC_HFS_FILE_LENGTH] = {
0x00, 0x05, 0x16, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00,
0x00, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00,
0x00, 0x00, 't', 'y', 'p', 'e', 'c', 'r', 'e', 'a', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00};
unsigned int offsetWhereOSTypesAreStored = 50;
NSData *data;
NSString *magicHFSFilePath;

*((int *)(&magicHFSFileContents[offsetWhereOSTypesAreStored])) = typeCode;
*((int *)(&magicHFSFileContents[offsetWhereOSTypesAreStored + 4])) = creatorCode;
data = [NSData dataWithBytes:magicHFSFileContents length:MAGIC_HFS_FILE_LENGTH];
magicHFSFilePath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:[@"._" stringByAppendingString:[path lastPathComponent]]];

if ([self createFileAtPath:magicHFSFilePath contents:data attributes:[self fileAttributesAtPath:path traverseLink:NO]])
return 0;
else
return errorCode;
}
return errorCode;
}

- (unsigned long)codeFromString:(NSString *)fourLetterWord {
unsigned const char *c = [fourLetterWord cString];
if ([fourLetterWord cStringLength] == 4) {
return ((c[0]<<24) + (c[1]<<16) + (c[2]<<8) + c[3]);
}
return 0;
}

- (NSString *)stringFromCode:(unsigned long)code {
unsigned char c[5];
c[0] = code >> 24;
c[1] = (code >> 16) & 0xff;
c[2] = (code >> 8) & 0xff;
c[3] = code & 0xff;
c[4] = '\0';        // don't forget to terminate the cstring!
return [NSString stringWithCString:c];
}

// these are the exposed methods so the caller doesn't have to worry about
// creating correct FOUR_CHAR_CODE's

- (int)setTypeString:(NSString *) type andCreatorString:(NSString *)creator forPath:(NSString *)path {
return [self setType:[self codeFromString:type] andCreator:[self codeFromString:creator] forPath:path];
}

- (int)getTypeString:(NSString **)type andCreatorString:(NSString **)creator forPath:(NSString *)path {
unsigned long t,c;
int result = [self getType:&t andCreator:&c forPath:path];
if (result == 0) {
*type = [self stringFromCode:t];
*creator = [self stringFromCode:c];
} else {
*type = @"";
*creator = @"";
}
return result;
}

@end



Conclusion

A Mac OS X programmer can choose to use only Cocoa methods or Carbon functions. However, we are not limited to just one or the other. We can mix and match as needed, or even use the lower level C API of CoreFoundation. Because some functionality is available only under Carbon, it behooves the Cocoa developer to become familiar with this rich, albeit legacy, API. By wrapping your Carbon calls in Objective C objects and methods, you hide the complexity and fully embrace the object oriented design pattern.


Andrew Stone is founder of Stone Design Corp <http://www.stone.com/> and divides his time between farming on the earth and in cyperspace.

PreviousTopIndexNext
©1997-2005 Stone Design top