Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C As Principal Class Or; A Cocoa App Without ObjC

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.

like image 657
CodaFi Avatar asked Jul 03 '12 21:07

CodaFi


3 Answers

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

like image 151
sfstewman Avatar answered Nov 20 '22 22:11

sfstewman


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.

like image 38
bbum Avatar answered Nov 20 '22 21:11

bbum


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.

like image 1
Jesse Rusak Avatar answered Nov 20 '22 23:11

Jesse Rusak