Created
February 4, 2026 20:26
-
-
Save peterc/64b2e96f5857c6d777656ba3389faff0 to your computer and use it in GitHub Desktop.
A macOS app entirely in plain C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // A macOS GUI app written in pure C, using the Objective-C runtime directly. | |
| // No Objective-C syntax anywhere. Just raw objc_msgSend and runtime calls. | |
| // This is cursed. Enjoy. | |
| // Compile with clang -Wall -Wextra -o hello_runtime hello_runtime.c \ | |
| // -framework Cocoa -lobjc | |
| #include <objc/objc.h> | |
| #include <objc/message.h> | |
| #include <objc/runtime.h> | |
| #include <stdbool.h> | |
| typedef struct { double x, y, width, height; } CGRect; | |
| typedef unsigned long NSUInteger; | |
| // Every objc_msgSend call needs a cast to the correct function pointer type. | |
| // These typedefs save some sanity. | |
| typedef id (*send_t)(id, SEL); | |
| typedef id (*send_id_t)(id, SEL, id); | |
| typedef id (*send_rect_t)(id, SEL, CGRect); | |
| typedef id (*send_long_t)(id, SEL, long); | |
| typedef id (*send_double_t)(id, SEL, double); | |
| typedef id (*send_bool_t)(id, SEL, bool); | |
| typedef id (*send_cstr_t)(id, SEL, const char *); | |
| typedef id (*send_init_window_t)(id, SEL, CGRect, NSUInteger, NSUInteger, bool); | |
| typedef void (*send_void_t)(id, SEL); | |
| typedef void (*send_void_id_t)(id, SEL, id); | |
| typedef void (*send_void_bool_t)(id, SEL, bool); | |
| typedef void (*send_void_long_t)(id, SEL, long); | |
| #define cls(name) (id)objc_getClass(name) | |
| #define sel(name) sel_registerName(name) | |
| // Delegate callback: quit when the window closes | |
| static BOOL should_terminate(id self, SEL cmd, id sender) { | |
| (void)self; (void)cmd; (void)sender; | |
| return YES; | |
| } | |
| int main(void) { | |
| // NSApplication *app = [NSApplication sharedApplication]; | |
| id app = ((send_t)objc_msgSend)(cls("NSApplication"), sel("sharedApplication")); | |
| // [app setActivationPolicy:NSApplicationActivationPolicyRegular]; | |
| ((send_void_long_t)objc_msgSend)(app, sel("setActivationPolicy:"), 0); | |
| // --- Menu bar (so Cmd+Q works) --- | |
| // NSMenu *menubar = [[NSMenu alloc] init]; | |
| id menubar = ((send_t)objc_msgSend)(cls("NSMenu"), sel("alloc")); | |
| menubar = ((send_t)objc_msgSend)(menubar, sel("init")); | |
| // NSMenuItem *appMenuItem = [[NSMenuItem alloc] init]; | |
| id app_menu_item = ((send_t)objc_msgSend)(cls("NSMenuItem"), sel("alloc")); | |
| app_menu_item = ((send_t)objc_msgSend)(app_menu_item, sel("init")); | |
| ((send_void_id_t)objc_msgSend)(menubar, sel("addItem:"), app_menu_item); | |
| // NSMenu *appMenu = [[NSMenu alloc] init]; | |
| id app_menu = ((send_t)objc_msgSend)(cls("NSMenu"), sel("alloc")); | |
| app_menu = ((send_t)objc_msgSend)(app_menu, sel("init")); | |
| // NSString *quitTitle = @"Quit"; | |
| id quit_title = ((send_cstr_t)objc_msgSend)(cls("NSString"), sel("stringWithUTF8String:"), "Quit"); | |
| id quit_key = ((send_cstr_t)objc_msgSend)(cls("NSString"), sel("stringWithUTF8String:"), "q"); | |
| // NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"]; | |
| id quit_item = ((send_t)objc_msgSend)(cls("NSMenuItem"), sel("alloc")); | |
| quit_item = ((id (*)(id, SEL, id, SEL, id))objc_msgSend)( | |
| quit_item, sel("initWithTitle:action:keyEquivalent:"), | |
| quit_title, sel("terminate:"), quit_key | |
| ); | |
| ((send_void_id_t)objc_msgSend)(app_menu, sel("addItem:"), quit_item); | |
| ((send_void_id_t)objc_msgSend)(app_menu_item, sel("setSubmenu:"), app_menu); | |
| ((send_void_id_t)objc_msgSend)(app, sel("setMainMenu:"), menubar); | |
| // --- App delegate (so closing the window quits the app) --- | |
| // Create a new class at runtime: @interface AppDelegate : NSObject <NSApplicationDelegate> | |
| Class delegate_class = objc_allocateClassPair( | |
| (Class)objc_getClass("NSObject"), "AppDelegate", 0 | |
| ); | |
| // Add method: - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender | |
| class_addMethod(delegate_class, | |
| sel("applicationShouldTerminateAfterLastWindowClosed:"), | |
| (IMP)should_terminate, "B@:@" | |
| ); | |
| objc_registerClassPair(delegate_class); | |
| id delegate = ((send_t)objc_msgSend)((id)delegate_class, sel("alloc")); | |
| delegate = ((send_t)objc_msgSend)(delegate, sel("init")); | |
| ((send_void_id_t)objc_msgSend)(app, sel("setDelegate:"), delegate); | |
| // --- Window --- | |
| // NSWindow *window = [[NSWindow alloc] initWithContentRect:... styleMask:... backing:... defer:NO]; | |
| id window = ((send_t)objc_msgSend)(cls("NSWindow"), sel("alloc")); | |
| window = ((send_init_window_t)objc_msgSend)( | |
| window, sel("initWithContentRect:styleMask:backing:defer:"), | |
| (CGRect){200, 200, 500, 300}, | |
| 15, // NSWindowStyleMaskTitled | Closable | Miniaturizable | Resizable | |
| 2, // NSBackingStoreBuffered | |
| false | |
| ); | |
| // [window setTitle:@"Hello from Pure C"]; | |
| id title = ((send_cstr_t)objc_msgSend)(cls("NSString"), sel("stringWithUTF8String:"), "Hello from Pure C"); | |
| ((send_void_id_t)objc_msgSend)(window, sel("setTitle:"), title); | |
| // --- Text label --- | |
| // NSTextField *label = [[NSTextField alloc] initWithFrame:...]; | |
| id label = ((send_t)objc_msgSend)(cls("NSTextField"), sel("alloc")); | |
| label = ((send_rect_t)objc_msgSend)(label, sel("initWithFrame:"), (CGRect){50, 100, 400, 80}); | |
| // [label setStringValue:@"Hello, World!\nWritten in pure C.\nNo Objective-C syntax. Just vibes."]; | |
| id text = ((send_cstr_t)objc_msgSend)(cls("NSString"), | |
| sel("stringWithUTF8String:"), | |
| "Hello, World!\nWritten in pure C.\nNo Objective-C syntax. Just vibes." | |
| ); | |
| ((send_void_id_t)objc_msgSend)(label, sel("setStringValue:"), text); | |
| // Make it look like a label, not a text field | |
| ((send_void_bool_t)objc_msgSend)(label, sel("setBezeled:"), false); | |
| ((send_void_bool_t)objc_msgSend)(label, sel("setDrawsBackground:"), false); | |
| ((send_void_bool_t)objc_msgSend)(label, sel("setEditable:"), false); | |
| ((send_void_bool_t)objc_msgSend)(label, sel("setSelectable:"), false); | |
| // [label setFont:[NSFont systemFontOfSize:24]]; | |
| id font = ((send_double_t)objc_msgSend)(cls("NSFont"), sel("systemFontOfSize:"), 24.0); | |
| ((send_void_id_t)objc_msgSend)(label, sel("setFont:"), font); | |
| // [[window contentView] addSubview:label]; | |
| id content_view = ((send_t)objc_msgSend)(window, sel("contentView")); | |
| ((send_void_id_t)objc_msgSend)(content_view, sel("addSubview:"), label); | |
| // --- Show and run --- | |
| // [window makeKeyAndOrderFront:nil]; | |
| ((send_void_id_t)objc_msgSend)(window, sel("makeKeyAndOrderFront:"), nil); | |
| // [app activateIgnoringOtherApps:YES]; | |
| ((send_void_bool_t)objc_msgSend)(app, sel("activateIgnoringOtherApps:"), true); | |
| // [app run]; | |
| ((send_void_t)objc_msgSend)(app, sel("run")); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment