Skip to content

Instantly share code, notes, and snippets.

@peterc
Created February 4, 2026 20:34
Show Gist options
  • Select an option

  • Save peterc/9558dd9d53e4ef9923e4faa8dd0e7184 to your computer and use it in GitHub Desktop.

Select an option

Save peterc/9558dd9d53e4ef9923e4faa8dd0e7184 to your computer and use it in GitHub Desktop.
A native macOS app written in Ruby using FFI to call libobjc, etc.
#!/usr/bin/env ruby
# A macOS GUI app written in Ruby, calling libobjc directly via Fiddle.
# No gems. No Objective-C. Just Ruby and the runtime.
require "fiddle"
OBJC = Fiddle.dlopen("/usr/lib/libobjc.A.dylib")
APPKIT = Fiddle.dlopen("/System/Library/Frameworks/AppKit.framework/AppKit")
# Core runtime functions
ObjcGetClass = Fiddle::Function.new(OBJC["objc_getClass"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
SelRegisterName = Fiddle::Function.new(OBJC["sel_registerName"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
AllocClassPair = Fiddle::Function.new(OBJC["objc_allocateClassPair"], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_VOIDP)
RegisterClassPair = Fiddle::Function.new(OBJC["objc_registerClassPair"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
ClassAddMethod = Fiddle::Function.new(OBJC["class_addMethod"], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
MSG_SEND = OBJC["objc_msgSend"]
def cls(name) = ObjcGetClass.call(name)
def sel(name) = SelRegisterName.call(name)
# Build a correctly-typed objc_msgSend call each time.
# types: array of Fiddle type constants for the method arguments (not including self/SEL).
# ret: Fiddle type constant for the return value.
def msg(obj, sel_name, *args, types: [], ret: Fiddle::TYPE_VOIDP)
sig = [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP] + types
fn = Fiddle::Function.new(MSG_SEND, sig, ret)
fn.call(obj, sel(sel_name), *args)
end
def nsstr(s)
msg(cls("NSString"), "stringWithUTF8String:", s, types: [Fiddle::TYPE_VOIDP])
end
def alloc_init(class_name)
msg(msg(cls(class_name), "alloc"), "init")
end
# Convenience type constants
T_PTR = Fiddle::TYPE_VOIDP
T_LONG = Fiddle::TYPE_LONG
T_ULONG = Fiddle::TYPE_ULONG
T_DOUBLE = Fiddle::TYPE_DOUBLE
T_BOOL = Fiddle::TYPE_INT
T_VOID = Fiddle::TYPE_VOID
# ---- App setup ----
app = msg(cls("NSApplication"), "sharedApplication")
msg(app, "setActivationPolicy:", 0, types: [T_LONG], ret: T_VOID)
# ---- Menu bar ----
menubar = alloc_init("NSMenu")
app_menu_item = alloc_init("NSMenuItem")
msg(menubar, "addItem:", app_menu_item, types: [T_PTR], ret: T_VOID)
app_menu = alloc_init("NSMenu")
quit_item = msg(
msg(cls("NSMenuItem"), "alloc"),
"initWithTitle:action:keyEquivalent:",
nsstr("Quit"), sel("terminate:"), nsstr("q"),
types: [T_PTR, T_PTR, T_PTR]
)
msg(app_menu, "addItem:", quit_item, types: [T_PTR], ret: T_VOID)
msg(app_menu_item, "setSubmenu:", app_menu, types: [T_PTR], ret: T_VOID)
msg(app, "setMainMenu:", menubar, types: [T_PTR], ret: T_VOID)
# ---- Delegate (so closing the window quits the app) ----
# Create a callback for applicationShouldTerminateAfterLastWindowClosed:
should_terminate = Fiddle::Closure::BlockCaller.new(
T_BOOL, [T_PTR, T_PTR, T_PTR]
) { |_self, _cmd, _sender| 1 }
dc = AllocClassPair.call(cls("NSObject"), "AppDel", 0)
ClassAddMethod.call(dc, sel("applicationShouldTerminateAfterLastWindowClosed:"), should_terminate, "B@:@")
RegisterClassPair.call(dc)
delegate = msg(msg(dc, "alloc"), "init")
msg(app, "setDelegate:", delegate, types: [T_PTR], ret: T_VOID)
# ---- Window ----
# initWithContentRect:styleMask:backing:defer:
# CGRect is 4 doubles passed in registers on ARM64.
window = msg(
msg(cls("NSWindow"), "alloc"),
"initWithContentRect:styleMask:backing:defer:",
200.0, 200.0, 500.0, 300.0, # CGRect as 4 doubles
15, # style mask (titled|closable|miniaturizable|resizable)
2, # backing (buffered)
0, # defer (NO)
types: [T_DOUBLE, T_DOUBLE, T_DOUBLE, T_DOUBLE, T_ULONG, T_ULONG, T_BOOL]
)
msg(window, "setTitle:", nsstr("Hello from Ruby + libobjc"), types: [T_PTR], ret: T_VOID)
# ---- Text label ----
label = msg(
msg(cls("NSTextField"), "alloc"),
"initWithFrame:",
50.0, 100.0, 400.0, 80.0,
types: [T_DOUBLE, T_DOUBLE, T_DOUBLE, T_DOUBLE]
)
msg(label, "setStringValue:", nsstr("Hello, World!\nFrom Ruby, via Fiddle, into libobjc.\nNo gems. No Objective-C."), types: [T_PTR], ret: T_VOID)
msg(label, "setBezeled:", 0, types: [T_BOOL], ret: T_VOID)
msg(label, "setDrawsBackground:", 0, types: [T_BOOL], ret: T_VOID)
msg(label, "setEditable:", 0, types: [T_BOOL], ret: T_VOID)
msg(label, "setSelectable:", 0, types: [T_BOOL], ret: T_VOID)
font = msg(cls("NSFont"), "systemFontOfSize:", 24.0, types: [T_DOUBLE])
msg(label, "setFont:", font, types: [T_PTR], ret: T_VOID)
content_view = msg(window, "contentView")
msg(content_view, "addSubview:", label, types: [T_PTR], ret: T_VOID)
# ---- Show and run ----
msg(window, "makeKeyAndOrderFront:", 0, types: [T_PTR], ret: T_VOID)
msg(app, "activateIgnoringOtherApps:", 1, types: [T_BOOL], ret: T_VOID)
msg(app, "run", ret: T_VOID)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment