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:
-
Your (posted) code is written using some iOS classes and methods that don’t quite line up with the OS X versions.
-
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.
-
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:
-
Either declare your
initAppDel
method with__attribute__((constructor))
, which forces it to be called by the setup code beforemain
, or -
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: