9

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.

4

3 回答 3

10

好的,这是一个似乎对我有用的经过大幅改写的答案。

因此,您的问题与主要课程无关。您应该将主要课程保留为NSApplication.

正如我之前提到的,主要问题是您没有向 NSApplication 注册适当的应用程序委托。更改主要课程不会解决此问题。一个 NIB 文件可以设置应用程序委托,但这对于这个问题来说真的是矫枉过正。

但是,实际上有几个问题:

  1. 您的(发布的)代码是使用一些与 OS X 版本不完全一致的 iOS 类和方法编写的。

  2. 您必须确保您的 AppDelegate 类已在系统中注册,然后您必须手动初始化 NSApplication 并设置其应用程序委托。

  3. 事实证明,链接在这里非常重要。您需要有一些外部符号使链接器将 Foundation 工具包和 AppKit 拖入内存。否则,没有像NSObject使用 Objective-C 运行时那样注册类的简单方法。

iOS v OSX 类

OSX 应用程序委托从 派生NSObject,而不是从 派生UIResponder,因此行:

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

应该读:

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

此外,OSX 应用程序代理响应的消息与 iOS 应用程序代理不同。特别是,它们需要响应applicationDidFinishLaunching:选择器(它需要一个NSNotifier类型的对象id)。

线

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

应该读:

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

请注意,参数 ("i@:@@""i@:@") 是不同的。

设置 NSApplication

AppDelegate向 Objective-C 运行时注册有两种选择:

  1. 要么用 声明你的initAppDel方法__attribute__((constructor)),这迫使它被之前的设置代码调用main,要么

  2. 在实例化对象之前自己调用它。

我一般不相信__attribute__标签。它们可能会保持不变,但苹果可能会改变它们。我选择从 拨打initAppDel电话main

一旦你AppDelegate在系统中注册了你的类,它基本上就像一个 Objective-C 类一样工作。您可以像 Objective-C 类一样实例化它,并且可以像id. 它实际上是一个id.

要确保 AppDelegate 作为应用程序委托运行,您必须设置NSAppliction. 因为您正在滚动自己的应用程序委托,所以您真的不能使用它NSApplicationMain来执行此操作。事实证明这并不难:

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"));
}

链接和 Objective-C 运行时

所以,这就是问题的真正实质,至少对我而言。NSObject除非您实际拥有并NSApplication在 Objective-C 运行时中注册,否则上述所有操作都将彻底失败 。

通常,如果您使用的是 Objective-C,编译器会告诉链接器它需要它。它通过在.o文件中放置一堆特殊的未解析符号来做到这一点。如果我编译文件 SomeObj.m:

#import <Foundation/NSObject.h>

@interface SomeObject : NSObject
@end

@implementation SomeObject
@end

使用clang -c SomeObj.m,然后使用 来查看符号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

您会看到所有这些漂亮的_OBJC_CLASS_$_符号,U左侧带有 a,表示这些符号未解析。当你链接这个文件时,链接器会接受它,然后意识到它必须加载 Foundation 框架来解析引用。这迫使 Foundation 框架将其所有类注册到 Objective-C 运行时。如果您的代码需要 AppKit 框架,则需要类似的东西。

如果我编译您的 AppDelegate 代码,我已将其重命名为“AppDelegate_orig.c”,clang -c AppDelegate_orig.c然后 nm在其上运行:

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

您会看到没有会强制 Foundation 或 AppKit 框架链接的未解析符号。这意味着我所有的调用都objc_getClass将返回 NULL,这意味着整个事情都崩溃了。

我不知道你的其余代码是什么样的,所以这对你来说可能不是问题,但解决这个问题让我自己编译一个修改过的 AppDelegate.c 文件一个(不是很实用的)OSX 应用程序中。

这里的秘诀是找到需要链接器引入Foundation和AppKit的外部符号。事实证明这相对容易。AppKit 提供了一个全局变量NSApp来保存应用程序的NSApplication. AppKit 框架依赖于 Foundation 框架,所以我们免费获得它。只需声明对此的外部引用就足够了:

extern id NSApp;

(注意:您必须在某处实际使用该变量,否则编译器可能会将其优化掉,您将失去所需的框架。)

代码和截图:

这是我的 AppDelegate.c 版本。它包括main并且应该设置所有内容。结果并不是那么令人兴奋,但它确实在屏幕上打开了一个小窗口。

#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;
}

像这样编译、安装和运行:

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 是:

<?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>

用截图:

申请截图

于 2012-07-03T22:00:35.713 回答
8

沿着这条路走下去是疯狂的,或者至少是巨大的时间浪费。Cocoa 应用程序本质上设计为基于 Objective-C 并使用 .app 包装器,该包装器具有非常特定设计的子目录层次结构(包括需要诸如 Info.plist 之类的东西,并且可能需要代码签名)。

你认为你遇到的代表问题完全是一个红鲱鱼;真正的问题是你实际上并没有构建一个合适的 Cocoa 应用程序。

而且,不,它在 iOS 下不会很好地工作,因为该平台甚至更需要以非常特定的方式构建应用程序。

如果你想“包装 C”,那么:

  • 从一个基本的 Cocoa 应用程序开始
  • 将你的主函数重命名为 mainC() 或其他东西(如果你真的想要,可以从编译器/链接器命令行完成)
  • [理想情况下]移动主线程的所有C goop,这样你就不会阻塞主事件循环

如果您希望“纯 C”作为您的主要课程,那么:

  • 创建一个实现类的 .m 文件
  • 实现类的方法来调用你的 C 函数

完成——就是这么简单。虽然在你做的时候自己滚动肯定是有教育意义的(不,真的——它是,我鼓励每个人都去探索),它只是重新发明了一个轮子。

tl;dr 如果您正在与系统 API 作斗争,那么您做错了。


当然,但我正在用勺子将 Rich 的基础移植到 OS X。这完全是浪费时间,但至少不会让我气馁

惊人的!

在这种情况下——你会想看看 ruby​​cocoa shell 的实现pythonw和/或(IIRC),它允许从相对非基于 .app 包装器的 shell 脚本实现 GUI。

这个问题远远超出了主要课程。考虑到这[NSBundle mainBundle]确实需要有意义,并且确实需要封装某种资源。

此外,该PyObjC项目在开发基于“shell 脚本”的 Cocoa 应用程序方面进行了许多不同的尝试。您可能想探讨各种示例,因为我认为至少仍然存在一些可以满足您的需求。

谷歌搜索“来自 shell 脚本的 Cocoa 应用程序”等也可能有用,因为在过去的 23 年中这种情况已经出现过多次。

于 2012-07-03T21:24:26.603 回答
1

如果您愿意调用 Objective-C 运行时函数但不愿意使用 @implementation(出于任何疯狂的原因),只需使用objc_allocateClassPair和朋友创建一个类并将其用作您的主类。

于 2012-07-03T21:31:19.133 回答