C As Principal Class Or; A Cocoa App Without ObjC-Collection of common programming errors

Okay, here is a substantially rewritten answer that seems to work for me.

So, your problems are pretty unrelated to the principal class. You should leave the principal class as NSApplication.

The major problem, as I alluded to before, is that you don’t register an appropriate application delegate with NSApplication. Changing the principal class will not fix this. A NIB file can set the application delegate, but that’s really overkill for this problem.

However, there are actually several problems:

  1. Your (posted) code is written using some iOS classes and methods that don’t quite line up with the OS X versions.

  2. You have to make sure that your AppDelegate class is registered in the system, and then you have to manually initialize NSApplication and set its application delegate.

  3. Linking turns out to be very very important here. You need to have some external symbol that makes the linker drag the Foundation kit and the AppKit into memory. Otherwise, there’s no easy way to register classes like NSObject with the Objective-C runtime.

iOS v OSX classes

OSX application delegates derive from NSObject, not from UIResponder, so the line:

AppDelClass = objc_allocateClassPair((Class) objc_getClass("UIResponder"), "AppDelegate", 0);

should read:

AppDelClass = objc_allocateClassPair((Class) objc_getClass("NSObject"), "AppDelegate", 0);

Also, OSX application delegates respond to different messages than iOS application delegates. In particular, they need to respond to the applicationDidFinishLaunching: selector (which takes an NSNotifier object of type id).

The line

class_addMethod(AppDelClass, sel_getUid("application:didFinishLaunchingWithOptions:"), (IMP) AppDel_didFinishLaunching, "i@:@@");

should read:

class_addMethod(AppDelClass, sel_getUid("applicationDidFinishLaunching:"), (IMP) AppDel_didFinishLaunching, "i@:@");

Notice that the parameters ("i@:@@" and "i@:@") are different.

Setting up NSApplication

There are two choices for registering AppDelegate with the Objective-C runtime:

  1. Either declare your initAppDel method with __attribute__((constructor)), which forces it to be called by the setup code before main, or

  2. Call it yourself before you instantiate the object.

I generally don’t trust __attribute__ labels. They’ll probably stay the same, but Apple might change them. I’ve chosen to call initAppDel from main.

Once you register your AppDelegate class with the system, it basically works like an Objective-C class. You instantiate it like an Objective-C class, and you can pass it around like an id. It actually is an id.

To make sure that AppDelegate is run as the application delegate, you have to set up NSAppliction. Because you’re rolling your own application delegate, you really cannot use NSApplicationMain to do this. It actually turned out to not be that hard:

void init_app(void)
{
  objc_msgSend(
      objc_getClass("NSApplication"), 
      sel_getUid("sharedApplication"));

  if (NSApp == NULL)
  {
    fprintf(stderr,"Failed to initialized NSApplication... terminating...\n");
    return;
  }

  id appDelObj = objc_msgSend(
      objc_getClass("AppDelegate"), 
      sel_getUid("alloc"));
  appDelObj = objc_msgSend(appDelObj, sel_getUid("init"));

  objc_msgSend(NSApp, sel_getUid("setDelegate:"), appDelObj);
  objc_msgSend(NSApp, sel_getUid("run"));
}

Linking and the Objective-C runtime

So, here’s the real meat of the problem, at least for me. All of the above will fail, utterly and completely, unless you actually have NSObject and NSApplication registered in the Objective-C runtime.

Usually, if you’re working in Objective-C, the compiler tells the linker that it needs that. It does this by putting a bunch of special unresolved symbols in the .o file. If I compile the file SomeObj.m:

#import 

@interface SomeObject : NSObject
@end

@implementation SomeObject
@end

Using clang -c SomeObj.m, and then look at the symbols using nm SomeObj.o:

0000000000000000 s L_OBJC_CLASS_NAME_
                 U _OBJC_CLASS_$_NSObject
00000000000000c8 S _OBJC_CLASS_$_SomeObject
                 U _OBJC_METACLASS_$_NSObject
00000000000000a0 S _OBJC_METACLASS_$_SomeObject
                 U __objc_empty_cache
                 U __objc_empty_vtable
0000000000000058 s l_OBJC_CLASS_RO_$_SomeObject
0000000000000010 s l_OBJC_METACLASS_RO_$_SomeObject

You’ll see all those nice _OBJC_CLASS_$_ symbols with a U to the left, indicating that the symbols are unresolved. When you link this file, the linker takes this and then realizes that it has to load the Foundation framework to resolve the references. This forces the Foundation framework to register all of its classes with the Objective-C runtime. Something similar is required if your code needs the AppKit framework.

If I compile your AppDelegate code, which I’ve renamed ‘AppDelegate_orig.c’ with clang -c AppDelegate_orig.c and then run nm on it:

00000000000001b8 s EH_frame0
000000000000013c s L_.str
0000000000000145 s L_.str1
000000000000014b s L_.str2
0000000000000150 s L_.str3
0000000000000166 s L_.str4
0000000000000172 s L_.str5
000000000000017e s L_.str6
00000000000001a9 s L_.str7
0000000000000008 C _AppDelClass
0000000000000000 T _AppDel_didFinishLaunching
00000000000001d0 S _AppDel_didFinishLaunching.eh
                 U _class_addMethod
00000000000000c0 t _initAppDel
00000000000001f8 s _initAppDel.eh
                 U _objc_allocateClassPair
                 U _objc_getClass
                 U _objc_msgSend
                 U _objc_registerClassPair
                 U _sel_getUid

You’ll see that there are no unresolved symbols that would force the Foundation or AppKit frameworks to link. This means that all my calls to objc_getClass would return NULL, which means that the whole thing comes crashing down.

I don’t know what the rest of your code looks like, so this may not be an issue for you, but solving this problem let me compile a modified AppDelegate.c file by itself into a (not very functional) OSX application.

The secret here is to find an external symbol that requires the linker to bring in Foundation and AppKit. That turned out to be relatively easy. The AppKit provides a global variable NSApp which hold the application’s instance of NSApplication. The AppKit framework relies on the Foundation framework, so we get that for free. Simply declaring an external reference to this was enough:

extern id NSApp;

(NB: You have to actually use the variable somewhere, or the compiler may optimize it away, and you’ll lose the frameworks that you need.)

Code and screenshot:

Here is my version of AppDelegate.c. It includes main and should set everything up. The result isn’t all that exciting, but it does open a tiny window on the screen.

#include 
#include 

#include 
#include 

extern id NSApp;

struct AppDel
{
    Class isa;
    id window;
};


// This is a strong reference to the class of the AppDelegate
// (same as [AppDelegate class])
Class AppDelClass;

BOOL AppDel_didFinishLaunching(struct AppDel *self, SEL _cmd, id notification) {
    self->window = objc_msgSend(objc_getClass("NSWindow"),
      sel_getUid("alloc"));

    self->window = objc_msgSend(self->window, 
      sel_getUid("init"));

    objc_msgSend(self->window, 
      sel_getUid("makeKeyAndOrderFront:"),
      self);

    return YES;
}

static void initAppDel() 
{
  AppDelClass = objc_allocateClassPair((Class)
    objc_getClass("NSObject"), "AppDelegate", 0);

  class_addMethod(AppDelClass, 
      sel_getUid("applicationDidFinishLaunching:"), 
      (IMP) AppDel_didFinishLaunching, "i@:@");

  objc_registerClassPair(AppDelClass);
}

void init_app(void)
{
  objc_msgSend(
      objc_getClass("NSApplication"), 
      sel_getUid("sharedApplication"));

  if (NSApp == NULL)
  {
    fprintf(stderr,"Failed to initialized NSApplication...  terminating...\n");
    return;
  }

  id appDelObj = objc_msgSend(
      objc_getClass("AppDelegate"), 
      sel_getUid("alloc"));
  appDelObj = objc_msgSend(appDelObj, sel_getUid("init"));

  objc_msgSend(NSApp, sel_getUid("setDelegate:"), appDelObj);
  objc_msgSend(NSApp, sel_getUid("run"));
}


int main(int argc, char** argv)
{
  initAppDel();
  init_app();
  return EXIT_SUCCESS;
}

Compile, install, and run like this:

clang -g -o AppInC AppDelegate.c -lobjc -framework Foundation -framework AppKit
mkdir -p AppInC.app/Contents/MacOS
cp AppInC AppInC.app/Contents/MacOS/
cp Info.plist AppInC.app/Contents/
open ./AppInC.app

Info.plist is:





        CFBundleDevelopmentRegion
        en
        CFBundleExecutable
        AppInC
        CFBundleIconFile
        
        CFBundleIdentifier
        com.foo.AppInC
        CFBundleInfoDictionaryVersion
        6.0
        CFBundleName
        AppInC
        CFBundlePackageType
        APPL
        CFBundleShortVersionString
        1.0
        CFBundleSignature
        ????
        CFBundleVersion
        1
        LSApplicationCategoryType
        public.app-category.games
        LSMinimumSystemVersion
        
        NSPrincipalClass
        NSApplication


With a screenshot: