How the Ranger process model relates to Objective-C (and, by inheritance, much of UIKit / AppKit on Apple platforms). This is a design comparison, not a plan to reimplement the ObjC runtime inside Ranger.
See also:
- PROCESS_LIFECYCLE.md — operators,
start/stop/hibernate/wakeup - PROCESS_STATUS.md — compiler / runtime checklist and hard gaps
- PROCESS_MVP.md — MVP sufficiency vs app orchestration
Apple’s stack solved the same shape of problems realtrainer cares about:
- Objects with identity, parent/child relationships, and teardown order
- Asynchronous delivery of work onto a serial “main” executor (run loop)
- Loose coupling between UI controllers, timers, and network via messages, protocols, and notifications
- Lifecycle hooks when views/controllers appear and disappear
Ranger’s @process MVP is closer to “know the object tree and stop subtrees” than to a full ObjC runtime. app-ranger ProcessKernel is closer to a custom run loop + message pump sitting above plain objects.
Objective-C is worth studying because it separates mechanism (runtime messaging, retain counts) from policy (view-controller lifecycle, delegate patterns) — and Ranger is still deciding how much of each layer to encode in the language vs the kernel.
| Objective-C idea | Typical Apple usage | Ranger / app-ranger today | Gap |
|---|---|---|---|
| Object / instance | UIViewController *vc |
@process class instance, Process in kernel |
Typed processes vs one fat Process class |
| Identity | Pointer equality | __rangerId (app-wide) |
Same role as processId; not a pointer |
| Message send | [receiver selector:arg] |
Direct fn calls (MVP); sendMessage queue (kernel) |
No universal dynamic perform yet |
| Selector | SEL (name + arity) |
messageType:string + typed message classes |
Strings today; could be stronger |
| Protocol | @protocol Foo <Bar> |
None in Ranger; kind:string on Process |
No compile-time protocol conformance |
| Class cluster / factory | +[NSString string] |
new + wrapped register |
spawn local not implemented |
| Delegation | weak id<UITableViewDelegate> |
EventBus handlers, registerStreamHandler |
Similar intent, different wiring |
| NSNotification | Broadcast by name | EventBus.emit with event.type |
Close cousin |
| Retain / release | ARC, weak for delegates |
Registry track / untrack; app holds fields |
No weak; stale refs possible |
| dealloc | Final cleanup when refcount → 0 | fn stop() + __rangerUnregister |
Explicit proc_stop, not GC |
| viewWillAppear / … | VC lifecycle | proc_start / proc_stop / hibernate |
Fewer named phases |
| NSRunLoop / main queue | Serial UI thread | Host calls kernel.tick + VirtualClock |
Same contract, host-owned |
| KVO | Key-path → callback | pageSetValue → ui.propChanged |
Property-level, not key-path |
| Blocks | Async completion | Not in Ranger; host JS/Swift | Completion handlers live outside |
Every instance is a heap object; identity is the pointer. You compare with == (same instance) vs isEqual: (value). Hierarchy is expressed with superclass (UIViewController → UIResponder → NSObject).
Instances are typed classes extending RangerProcessBase. Identity for registry and proc_stop(id) is __rangerId, allocated from ProcessIdRegistry — an integer, not an address. Parent link is __rangerParent (object reference where the analyzer allows) plus __rangerParentId snapshot at registration.
What ObjC got right: one clear notion of “this instance” for lifetime and messaging.
What Ranger does differently: IDs are stable for logging and proc_stop(42) even if the host copies references around; stopped instances set __rangerId = 0 (sentinel) instead of freeing memory like dealloc.
Could add:
- Optional
weak-style references in the language or codegen for parent/child fields (avoid retaining stopped pages). isLive/isStoppedas generated helpers instead of exposing__rangerId == 0everywhere.
A message send is dynamic:
[timerProcess tick]; // might not exist → forward or crash
[(id)target performSelector:@selector(pause) withObject:nil];The runtime looks up implementation at send time (objc_msgSend). Selectors are named, arity-sensitive keys; forwarding (forwardInvocation:) allows proxies and lazy work.
Properties are often syntactic sugar for setFoo: / foo messages.
| Layer | Style |
|---|---|
| Compiler MVP | Static calls: t.spawnTick(), proc_stop old — like C++ or Java, not ObjC |
| app-ranger kernel | Queued messages: sendMessage(processId, msg) with msg.messageType, drained in tick |
Kernel example (conceptually):
; ObjC-ish mental model:
; [chatProcess handleUserMessage:msg];
; Ranger kernel:
sendMessage(chatId, userMsg)
tick(chatId) ; "run loop turned" — drain inboundMessages
What to learn from ObjC:
- Decouple sender from receiver timing — sender should not assume the receiver is mid-
boot(); queue + later drain matches[performSelector:afterDelay:]and run-loop sources. - One serial executor for UI-facing processes — ObjC’s main thread; Ranger’s host must call
tickon one thread (documented in APP_PROCESS.md). Same invariant as “only touch UIKit on main.” - Typed envelopes, loose dispatch — ObjC uses
SEL+ untypedid; kernel usesmessageTypestring then switches inprocessPageMessages. Ranger could keep strings for extensibility but add typed message classes checked at compile time where possible (already started withAIChatUserMessage,PageActionMessage).
What is different:
- Ranger prefers static
fncalls inside a process for clarity and analyzer checking; messages are for cross-boundary input (host → process, process → bus), not every internal call. - No forwarding chain — unknown
messageTypeis typically left on the queue or dropped, not forwarded to a parent automatically.
Could add:
performMessageoperator:performMessage proc "pause"→ compiler-known selector table or runtime dispatch table per@processclass.- Default handler on base class:
fn onMessage(msg:ProcessMessage):booleanoverride, return true if handled (likerespondsToSelector:+ single entry point). - Async reply path: ObjC delegates often use callbacks; align with existing
streamId+registerStreamHandleras the typed “reply channel.”
@protocol TimerDisplaying <NSObject>
- (void)setRemainingSeconds:(NSInteger)s;
@optional
- (void)timerDidFinish:(id)sender;
@end
@interface WorkoutViewController : UIViewController <TimerDisplaying>
@end- Protocol = interface only; class conforms at compile time (warnings) and runtime (
conformsToProtocol:). - Optional methods — caller must
respondsToSelector:before send. - Composition over inheritance — common in UIKit delegates.
- No
protocolkeyword in Ranger for processes. - app-ranger uses
kind:string(kindUIPage,kindAIChat, …) and bigProcesswith many fields — closer to one god object + kind switch than to small protocol-shaped APIs. @processclasses are full classes with fields and methods — conformance is “you extended the right base and implementedfn stop.”
What to learn:
- Split “what callers may invoke” from “how it is implemented” — e.g.
TimerDisplayingvsTimerProcessimplementation. - Optional capabilities — pause/resume only if the process implements them; avoids kernel
if kind == …growing without bound. - Delegate as protocol + weak ref —
id<WorkoutTimerDelegate>is how ObjC avoids retain cycles; maps to weak parent or EventBus subscription cleaned onstop.
Could add (language / codegen):
protocol TimerDisplaying {
fn setRemainingSeconds:void (sec:int)
fn timerDidFinish:void () @optional
}
class TimerProcess @process(true) implements TimerDisplaying extends RangerProcessBase {
...
}
- Compiler checks that required methods exist.
- Kernel or host holds
TimerDisplaying-shaped view adapter, not concrete class. @optional→ codegen forhasTimerDidFinish+ safe call (likerespondsToSelector).
Even a lighter step: @processCapability("timer") annotation generating a dispatch table — protocols without full ObjC runtime.
Delegation: one-to-one (usually), weak delegate, callbacks like tableView:didSelectRowAtIndexPath:.
NSNotificationCenter: one-to-many broadcast by name; observers register with selectors or blocks.
| Pattern | Ranger analogue |
|---|---|
| Delegate | registerStreamHandler(streamId, …), registerProcessEventHandler, parent emitToParent |
| Notification | EventBus.emit(AppEvent) with ev.type string |
What to learn:
- Delegate = directed, typed, lifecycle-bound (tear down observer when process killed).
- Notification = loose coupling for cross-cutting concerns (
ui.propChanged,process.lifecycle).
Could add:
- On
proc_stop, auto-unregister all handlers for thatprocessId/streamId(ObjC removes observers indealloc/viewDidDisappear— easy to forget in Ranger). - Explicit
delegatefield codegen:def delegate@(weak):TimerDelegatewith compiler-enforced protocol type.
Typical view-controller chain:
init → loadView → viewDidLoad
→ viewWillAppear → viewDidAppear
→ … user interaction …
→ viewWillDisappear → viewDidDisappear
→ dealloc
Child VCs are added as children; removal triggers disappear + teardown. Not identical to proc_stop, but the user-visible “leave screen → stop children” matches process_page_lifecycle.rgr.
| UIKit phase | Ranger analogue (today) |
|---|---|
init / alloc |
new → register in tree + registry |
viewDidLoad |
proc_start → fn start() |
viewWillDisappear |
beginning of proc_stop / __rangerStopSubtree |
dealloc |
fn stop() + untrack; __rangerId = 0 |
| State restoration | hibernate / wakeup (sketched, underused) |
What to learn:
- Named phases help hosts and tests (
assert phase == Appeared); Ranger could add optionalfn willStop/fn didStopor document mapping to existingstop. - Child containment —
addChildViewController:maps to__rangerRegisterChildwhennewruns under a live parent; breaking that (child created at top level) is like adding a view without a parent VC — works sometimes, breaks teardown expectations.
Could add:
- Compiler hook:
onNavigateAwaygenerated from route layer (still callsproc_stopunder the hood). spawn local(PROCESS_LIFECYCLE.md) ≈ “reuse child VC if already present” instead of new instance everyboot().
- NSRunLoop on main thread: sources, timers, performSelector requests.
- GCD:
dispatch_async(main_queue, ^{ ... })— still serial on main. - NSTimer — retained by run loop until invalidated; must invalidate in
dealloc/ disappear (classic leak footgun).
- No run loop inside Ranger — host drives
kernel.tickandVirtualClock.advance. - Timer in fixture — synchronous
tick()method onTimerProcess, not a run-loop source. - Network —
scheduleNetworkTimeoutFor+ clock snapshot; closer to timer fire date than toNSTimer.
What to learn:
- Timer ownership — ObjC timer is tied to run loop mode; Ranger timer should be tied to **
processId+ pageId** and cancelled inkill/proc_stop`. - Coalesced UI updates — run loop often batches layout;
pageSetValuebursts could be batched per tick (oneui.propChangedflush at end oftick).
Could add:
scheduleOnProcess(processId, delayMs, message)— performSelector:afterDelay equivalent, implemented viaVirtualClock, not OS timer inside Ranger.invalidateTimergenerated whenfn stopruns.
Key-value observing: observe remainingSeconds on a model; UI updates when value changes. Fragile (string key paths) but decoupled.
Explicit push: pageSetValue(pageId, nodeId, key, value) → ui.propChanged. No automatic observe of def timeLeft on @process.
What to learn:
- ObjC KVO is magic and hard to static-check; Ranger’s explicit push fits a compiler-checked UI tree better long term.
- Middle ground:
@observable def timeLefton@processfields → codegen callspageSetValuewhen assigned (KVO-like, but explicit and typed).
Could add:
- Field annotation
@binds("circularTimer", "remainingSec")onTimerProcess.timeLeft. - Or reactive layer in PROCESS_STATUS “UI sync” gap — learned from KVO’s goal, not its mechanism.
| ObjC feature | Why hesitate in Ranger |
|---|---|
| Fully dynamic every call | Conflicts with Ranger’s static analyzer and multi-backend codegen |
performSelector: everywhere |
Hard to emit safe Kotlin/Swift/JS |
| Swizzling | No runtime rewrite of method tables |
| Key-path strings | Prefer node id + key from UIComponentTree |
| NSObject everywhere | Prefer small typed @process classes |
Ranger targets Kotlin, Swift, JS from one .rgr source — the sweet spot is ObjC’s process architecture, not its runtime tricks.
Inspired by ObjC / UIKit, compatible with PROCESS_STATUS.md roadmap:
| Priority | Feature | ObjC inspiration | Fits where |
|---|---|---|---|
| 1 | Message queue + tick on @process |
Run loop + performSelector | Extend ProcessRuntime or fold kernel into compiler |
| 2 | Auto-remove bus handlers on proc_stop |
dealloc / removeObserver |
ProcessKernel / generated stop |
| 3 | protocol for process capabilities |
@protocol + optional methods |
Ranger language + kernel dispatch |
| 4 | spawn local |
Child VC reuse | PROCESS_LIFECYCLE — reduces duplicate timers |
| 5 | @binds / observable fields |
KVO | UI sync without manual pageSetValue spam |
| 6 | performMessage / default onMessage |
performSelector: / forwarding |
Typed alternative to raw strings |
| 7 | Weak delegate/codegen | weak id<Delegate> |
Parent/page fields, stream handlers |
| 8 | Scheduled messages via VirtualClock | NSTimer / afterDelay |
Timer processes without host setInterval |
// Main thread only
[workoutVC.timer setPaused:YES]; // property → message
// or
[[NSNotificationCenter defaultCenter]
postNotificationName:@"WorkoutTimerPause" object:workoutVC];
// Timer VC holds weak delegate; table updates via delegate callback
[delegate workoutTimerDidPause:remaining];; Host (Swift/JS) after button tap:
kernel.sendMessage(timerProcessId, pageActionPauseMsg)
kernel.tick(timerProcessId)
; Inside kernel: processTimerMessages → toggleTimerPause
; → pageSetValue(..., "paused", "true")
; EventBus: ui.propChanged → host updates view
; Host still drives tick, but process is typed:
proc_send timerProc (PageActionMessage.pause())
; TimerProcess implements TimerDisplaying + handles message in onMessage
; @binds updates circularTimer.remainingSec when timeLeft changes
One story, three layers — ObjC collapsed runtime + UIKit conventions; Ranger is separating compile-time process objects, kernel queue, and host rendering.
| Question | Short answer |
|---|---|
Is @process like NSObject? |
Partially — instance lifecycle and hierarchy, not dynamic messaging everywhere. |
Is ProcessKernel like NSRunLoop + center? |
Closer — serial tick, queued messages, EventBus ≈ notifications. |
| What should Ranger adopt? | Queued messages, protocol-shaped APIs, weak delegates, observer cleanup, scheduled clock-driven messages, optional property→UI binding. |
| What should stay different? | Static typing, explicit proc_stop, no swizzling, multi-target codegen. |
Objective-C teaches how to structure responsibility (who receives what, when, and on which thread). Ranger’s compiler MVP teaches how to structure ownership (tree, registry, stop order). The product work is merging those lessons without importing the ObjC runtime — see the expanded gap analysis in PROCESS_STATUS.md.