11

For those who question my sanity/time-wasting/ability/motives, this thing is a port of the "Foundation With A Spoon" project, now infamous for being an all-C iOS app.

So, I've got a c file raring to go and be the main class behind an all-C mac-app, however, a combination of limiting factors are preventing the application from being launched. As it currently stands, the project is just a main.m and a class called AppDelegate.c, so I entered "AppDelegate" as the name of the principal class in the info.plist, and to my complete surprise, the log printed:

Unable to find class: AppDelegate, exiting

This would work perfectly well in iOS, because the main function accepts the name of a delegate class, and handles it automatically, but NSApplicationMain() takes no such argument.

Now, I know this stems from the fact that there are no @interface/@implementation directives in C, and that's really what the OS seems to be looking for, so I wrote a simple NSApplication subclass and provided it as the Principal Class to the plist, and it launched perfectly well. My question is, how could one go about setting a c file as the principal class in a mac application and have it launch correctly?

EDIT: SOLVED! The file may be a .m (framework errors, for some reason), but the allocation of class pairs is enough to slip by. You can download the source for the C-Based Mac App here. Happy digging!

The code's been written, signed, sealed, and stamped, but all I need is a way around the NSPrincipalClass requirement in the plist.

Community
  • 1
  • 1
CodaFi
  • 43,043
  • 8
  • 107
  • 153
  • How do you wanna 'set a C file as principal class'? Does your C file derive from NSObject? –  Jul 03 '12 at 21:07
  • It can't subclass any ObjC-Object. C-"classes" are just structs. – CodaFi Jul 03 '12 at 21:08
  • 1
    If I'm to be honest, this sounds like the kind of task where if you have no idea where to begin, you shouldn't even be trying this. My guess is what you'll have to do is construct a valid AppDelegate class using ObjC runtime calls but odds are you're the first person to want to do this. – millimoose Jul 03 '12 at 21:08
  • 1
    @CodaFi see, I know, I'm an experienced C programmer... your question is not quite clear to me. –  Jul 03 '12 at 21:09
  • Yes I know, the thing is written entirely in C, but I am allowed to use C objects in ObjC frameworks. And @millimoose, if that's supposed to discourage me, it didn't work. – CodaFi Jul 03 '12 at 21:10
  • @CodaFi Yes, it was. If you're trying something that involves touching the internals of the platform this deeply, and don't have a single line of code to show for your attempts, you're either lazy or outclassed. Also, why is the limitation stupid? Cocoa is an ObjC framework. What you're describing is akin to making a Swing program without using Java. There's no reason why the platform should accomodate you at all. – millimoose Jul 03 '12 at 21:11
  • @millimoose ObjC being a strict superset of C, this is entirely possible. Like I said, [on iOS, this code would run great](http://stackoverflow.com/questions/10289890/how-to-write-ios-app-purely-in-c#comment13239523_10289913), but the only thing keeping me from running is the need for an NSPrincipalClass. It's more like trying to write a C++ app in C. – CodaFi Jul 03 '12 at 21:16
  • "NSApplicationMain() takes no such argument".. what makes you think so? "that's really what the OS seems to be looking for".. what makes you think so? – hooleyhoop Jul 03 '12 at 21:23
  • "What makes you think so" `NSApplicationMain(int argc, char[]*argv)` ios is declared as `UIApplicationMain(int argc, char[] * argv, NSString *PrincipalClass, NSString*applicationDelegate)`. "What makes you think that” because C doesn't have classes, and a field called NSPrincipalClass doesn't accept bananas. LOL – CodaFi Jul 03 '12 at 21:26
  • The missing bit of this question, I think, is *Why are you trying to avoid Objective-C?* – bbum Jul 03 '12 at 21:31
  • 3
    Nope; his clearly stating that he is doing this for academic purposes on a comment on my answer was exactly what was needed. That sets context; this isn't about producing a "real" Cocoa application, but about building a Cocoa application without Objective-C for learning purposes. I dig it! – bbum Jul 03 '12 at 21:36
  • @CodaFi - i guess you are just trolling. bananas. – hooleyhoop Jul 03 '12 at 21:46
  • @hooleyhoop: I'm not trolling, just wondering why everyone is discouraging me from doing something that should be so obvious to figure out. Code's written, I just need a way to produce a binary that actually runs. – CodaFi Jul 03 '12 at 21:48
  • If you programmatically set the application delegate on `NSApplication` (by sending it a `setDelegate` message), couldn't you just use `NSApplication` as the principal class? – sfstewman Jul 03 '12 at 23:01
  • But I can't conform structs to delegates, and NSApplication does send a message to its delegate at launch, the app would crash. – CodaFi Jul 03 '12 at 23:18
  • 1
    It seems like you can tell the objc runtime that a class does conform to the protocol with `class_addProtocol` – sfstewman Jul 03 '12 at 23:31

3 Answers3

11

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 <Foundation/NSObject.h>

@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 <stdio.h>
#include <stdlib.h>

#include <objc/runtime.h>
#include <objc/message.h>

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:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>en</string>
        <key>CFBundleExecutable</key>
        <string>AppInC</string>
        <key>CFBundleIconFile</key>
        <string></string>
        <key>CFBundleIdentifier</key>
        <string>com.foo.AppInC</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>AppInC</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
        <string>1</string>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.games</string>
        <key>LSMinimumSystemVersion</key>
        <string></string>
        <key>NSPrincipalClass</key>
        <string>NSApplication</string>
</dict>
</plist>

With a screenshot:

Screenshot of application

sfstewman
  • 5,589
  • 1
  • 19
  • 26
  • The problem you stated was that your AppDelegate class couldn't be found. The difference between OSX and iOS is that UIApplicationMain will take the name of the app delegate. On OSX, you *can* set it programmatically by not using `NSApplicationMain`, and rolling your own. If you programmatically initialize the app delegate, why can't you use `NSApplication` as your principal class? – sfstewman Jul 03 '12 at 23:18
  • Because I can't conform to an ObjC delegate with a struct (class) in C. I also can't receive selectors. – CodaFi Jul 03 '12 at 23:48
  • It seems like you can tell the objc runtime that a class does conform to the protocol with `class_addProtocol`. Why would this not work? – sfstewman Jul 03 '12 at 23:49
  • Yes, and I have tried that. Again, another problem that comes up is that I can't cast a struct to id, I'd have to use an NSValue pointer, which makes my life infinitely harder... I'll try it again. – CodaFi Jul 03 '12 at 23:53
  • I am confused. An id is defined in `` as a pointer to a struct with a single entry (`Class isa`). If you have a struct that has `Class isa` as its first definition, you should be able to cast a pointer of that struct to an `id`. Why do you think you cannot cast your struct to an `id`? – sfstewman Jul 03 '12 at 23:58
  • Because that struct _is_ id. Structs just can't be cast willy-nilly to other structs. – CodaFi Jul 04 '12 at 00:20
  • Sure they can. This behavior is used all of the time. It's the foundation for the GLib object system (GObject), and the Objective-C runtime. As long as the layout of beginning of the struct conforms to the layout of `*id`, then you're all good. structs should be cast *with extreme care* when their field formats are not the same, but that's not the situation that you're dealing with. – sfstewman Jul 04 '12 at 00:23
  • 1
    Sorry for the long answer, right after you solved your problem. Perhaps this will have something of value for you, perhaps not. It was a nice problem; thanks for posing it. – sfstewman Jul 04 '12 at 03:06
  • 1
    I didn't quite port a lot of the code yet. Yours should be accepted though, so it is. That's really beautiful, man. I'll post the full code once I get the Core Graphics part up and running. – CodaFi Jul 04 '12 at 03:10
  • Look forward to seeing it. Cheers! – sfstewman Jul 04 '12 at 03:18
8

Down this path lies madness or, at the least, a huge waste of time. Cocoa apps are inherently designed to be Objective-C based and to use a .app wrapper with a sub-directory hierarchy of a very specific design (including requiring things like an Info.plist and, potentially, code signing).

The delegate issue you think you are running into is entirely a red herring; the real issue is that you aren't actually building a proper Cocoa app.

And, no, it wouldn't work perfectly well under iOS because that platform, even more so, requires the application to be constructed in a very specific way.

If you want to "wrap C", then:

  • start with a basic Cocoa application
  • rename your main function to be mainC() or something (can be done from the compiler/linker command line, if you really want)
  • [ideally] move all your C goop of the main thread so you don't block the main event loop

If you want "pure C" as your principal class, then:

  • create a .m file that implements the class
  • implement the methods of the class to call your C functions

Done -- it is that simple. While rolling your own as you are doing is certainly educational (no, really -- it is and I encourage everyone to explore that), it is simply re-inventing a wheel.

tl;dr If you are fighting the system APIs, you are doing it wrong.


Of course, but I'm porting Rich's foundation with a spoon to OS X. It's a complete time-waster, but that doesn't discourage me in the least

Awesome!

In that case -- you'll want to have a look at the implementation of pythonw and/or (IIRC) the rubycocoa shell that allows for the implementation of GUI from a relatively non-.app wrapper based shell script.

The issue goes way beyond the principal class. Consider that [NSBundle mainBundle] really needs to be meaningful and it really needs to encapsulate some resources of some kind.

Also, the PyObjC project had a number of different attempts at doing "shell script" based Cocoa applications. You might want to poke about the various examples as I think there still exists at least a few that do what you want.

A google search for "Cocoa application from shell script" or the like might be useful, too, as this has come up any number of times in the last 23 years.

bbum
  • 162,346
  • 23
  • 271
  • 359
  • The code isn't the problem, the system is the problem. I need an override of the NSPrincipalClass value somehow. My madness is limited, my time is not. – CodaFi Jul 03 '12 at 21:27
  • 1
    "Overiding" NSPrincpalClass isn't going to solve your problem unless you already have a .app w/all the necessary gunk inside. And, if you do, then why the heck are you jumping through such hoops to avoid simply having one or two .m files that provide the glue to your C gunk? Writing `objc_msgSend()` by hand, along with a `struct` that pretends to be an ObjC instance, is a complete waste of time outside of academic pursuits. – bbum Jul 03 '12 at 21:30
  • Of course, but I'm porting Rich's foundation with a spoon to OS X. It's a complete time-waster, but that doesn't discourage me in the least. I could have written this thing in 4 lines in ObjC, but that's no fun whatsoever. – CodaFi Jul 03 '12 at 21:32
  • OK -- gotcha and I fully support you wasting your time in this fashion ! It is truly a useful lesson! – bbum Jul 03 '12 at 21:32
  • I'm honestly surprised you didn't get the reference. – CodaFi Jul 03 '12 at 21:36
  • 1
    My head is large, but that doesn't mean my brain is large. Could be a thick stone covering. – bbum Jul 03 '12 at 21:37
  • Brilliant, I'll take a look. I wonder how much easier a pyObjC-like approach would be in C... – CodaFi Jul 03 '12 at 21:52
1

If you're willing to call the Objective-C runtime functions but not willing to use @implementation (for whatever crazy reason), just make a class using objc_allocateClassPair and friends and use that as your main class.

Jesse Rusak
  • 56,530
  • 12
  • 101
  • 102
  • Yes, I'd tried that, however it doesn't stop the OS from throwing that same error. – CodaFi Jul 03 '12 at 21:39
  • Huh; if you allocated/register it, I would expect it to work. Perhaps they're using `[NSBundle principleClass]` or similar to load it from your bundle directly as opposed to the global namespace. Not sure how to fake that out. – Jesse Rusak Jul 03 '12 at 21:48
  • No, it's looking for an @interface/@implementation call. I tried it, even changed the c to an m and added them in, it ran great! It's just, that's cheating. I have to use all-C in order to properly dig the foundation. – CodaFi Jul 03 '12 at 21:51
  • How about putting the allocate/register inside an __attribute__((constructor)) function? – Jesse Rusak Jul 03 '12 at 23:12
  • Interesting, testing, will get back to you. – CodaFi Jul 03 '12 at 23:18