juno.objc

Bridge Python scripts to the Objective-C runtime.

This module gives Juno scripts and notebooks direct access to Cocoa Touch — UIKit, Foundation, AVFoundation, and anything else reachable through the Objective-C runtime — without having to wait for a curated juno.* wrapper.

juno.objc is a thin convenience layer on top of rubicon-objc 0.5.4, the upstream BeeWare Python ↔ Objective-C bridge. The rubicon package ships alongside Juno; everything documented in rubicon’s reference works unchanged when imported from juno.objc, and you can also use import rubicon.objc directly for the full upstream surface. The helpers documented on this page (ns, nsurl, create_objc_class, on_main_thread …) are Juno-specific sugar for the common Cocoa Touch idioms that the rubicon surface leaves to the caller.

For anything not covered below, the upstream rubicon reference is the source of truth:

Typical usage:

from juno.objc import ObjCClass, on_main_thread, ns

NSString = ObjCClass("NSString")
greeting = NSString.stringWithUTF8String_(b"Hello, world!")
print(str(greeting))

# Synchronous UIKit query from a worker-thread script cell.
@on_main_thread
def device_summary() -> str:
    UIDevice = ObjCClass("UIDevice")
    UIScreen = ObjCClass("UIScreen")
    device = UIDevice.currentDevice
    screen = UIScreen.mainScreen
    return (
        f"{str(device.name)} / iOS {str(device.systemVersion)} "
        f"/ {screen.bounds.size.width:.0f}x{screen.bounds.size.height:.0f}"
    )

print(device_summary())

Three groups of names are exposed:

  • rubicon re-exportsObjCClass, ObjCInstance, Block, ObjCBlock, autoreleasepool(), send_message(). Re-exported with rubicon-objc identity preserved so anything written against rubicon.objc keeps working when imported from juno.objc.

  • Juno helpersload_framework(), ns(), nsurl(), nsdata_to_bytes(), uiimage_to_png(), sel(), create_objc_class(), on_main_thread().

  • Pre-bound Foundation/UIKit classesNSObject, NSString, NSArray, NSDictionary, NSData, NSNumber, NSURL, NSDate, NSNull, NSError, and the mutable variants. Plus struct types: CGPoint, CGSize, CGRect, UIEdgeInsets, NSRange.

Three constraints are worth knowing:

UIKit calls must originate from the main thread.

Wrap any function that reads or mutates UIKit state (querying UIDevice, building a UIBezierPath, composing UIKit objects) with on_main_thread(). The decorator handles the worker → main-thread dispatch and propagates the return value or exception back. Stop is honoured at the next checkpoint after the wrapped call completes — sync dispatch can’t interrupt main-thread work mid-flight.

UIKit Block-based callbacks aren’t safe in this release.

Blocks fired by UIKit’s main runloop (UIAlertAction handlers, animation completion blocks, delegate methods) can’t reliably re-enter your script’s Python state. The result is a silent hang or memory corruption. For interactive UI, use the curated bridges: juno.dialogs, juno.sharing, juno.preview. juno.objc is for non-callback access.

Don’t register callbacks that outlive the script.

A delegate, block, or notification observer registered from a script holds Python state that is torn down when the script ends. Anything that fires after the script has ended (a queued notification, a dispatch_after, a long-lived completion handler) will reach a dead callback and crash. Stick to synchronous calls that complete inside the cell.

Async fire-and-forget on_main_thread (the async_=True flavour) is deliberately not exposed in this release — it would queue work that could outlive the script and isn’t safe yet.

juno.objc.load_framework(name)

Load and return a system framework as a ctypes.CDLL.

Resolves the framework via the standard system path (/System/Library/Frameworks/<name>.framework/<name>) so the iOS dyld shared cache resolves it cleanly. Calls are cached; requesting the same framework twice returns the same handle.

Parameters:

name (str) – Framework name without the .framework suffix (for example, "UIKit" or "CoreLocation").

Returns:

The loaded framework as a ctypes.CDLL handle.

Raises:
  • TypeError – If name is not a string.

  • OSError – If the framework cannot be loaded.

juno.objc.ns(value)

Convert a Python value to its Foundation counterpart.

Conversions, applied in order so subclasses don’t shadow their bases:

  • NoneNSNull singleton.

  • ObjCInstance → returned unchanged (idempotent).

  • boolNSNumber (boxed BOOL). Checked before int so True does not silently collapse into a boxed integer.

  • intNSNumber (boxed NSInteger).

  • floatNSNumber (boxed double).

  • strNSString.

  • bytes / bytearray / memoryviewNSData (length-preserving — embedded NUL bytes are retained).

  • datetime.date / datetime.datetimeNSDate.

  • dictNSDictionary. Both keys and values are passed through ns() recursively.

  • list / tupleNSArray. Elements are passed through ns() recursively.

Parameters:

value – Python value to convert.

Returns:

The Foundation object representing value.

Raises:

TypeError – If value has a type with no defined mapping.

juno.objc.nsurl(s)

Return an NSURL for a URL or filesystem path.

The argument is treated as a URL if it contains "://"; otherwise it’s interpreted as a filesystem path.

Parameters:

s (str) – URL string (for example, "https://example.com") or filesystem path (for example, "/tmp/foo.txt").

Returns:

An NSURL initialised from s.

Raises:

TypeError – If s is not a string.

juno.objc.nsdata_to_bytes(data)

Copy the contents of an NSData instance to bytes.

Length-aware — embedded NUL bytes are preserved.

Parameters:

data – An NSData (or NSMutableData) instance.

Returns:

The same byte payload as a Python bytes object.

Raises:

TypeError – If data is not an ObjCInstance.

Return type:

bytes

juno.objc.uiimage_to_png(image)

Encode a UIImage as PNG bytes.

Calls UIKit’s UIImagePNGRepresentation directly — it’s a free C function, not an Objective-C message, so plain attribute access on the UIImage instance wouldn’t work.

Parameters:

image – A UIImage instance (an ObjCInstance).

Returns:

PNG-encoded image bytes.

Raises:
  • TypeError – If image is not an ObjCInstance.

  • RuntimeError – If UIKit returns nil for the PNG conversion.

Return type:

bytes

juno.objc.sel(name)

Return a registered selector for an Objective-C method name.

Parameters:

name (str) – Selector name in standard Objective-C form (for example, "stringWithUTF8String:").

Returns:

The SEL value the runtime uses to dispatch the message.

Raises:

TypeError – If name is not a string.

Return type:

SEL

juno.objc.create_objc_class(name, superclass=None, methods=(), classmethods=(), protocols=())

Register or replace a custom Objective-C class.

Use this to define delegate classes, custom view subclasses, or target/action receivers from Python. Each Python callable in methods becomes an instance method on the resulting class; the selector is derived from the callable’s name by replacing every underscore with a colon (literal __name__.replace("_", ":")). To match a real Obj-C selector, name the Python function with exactly the right underscore placement — e.g. for -tableView:didSelectRowAtIndexPath: define def tableView_didSelectRowAtIndexPath_(self, table, indexPath); the trailing underscore stands in for the trailing colon on the last argument. There is no smart CamelCase-aware mapping. Type annotations on the callable are required so the Objective-C runtime can marshal arguments correctly.

The same class name is safe to re-register: subsequent calls replace the method implementations in place (via Objective-C’s class_replaceMethod), which makes the function safe to call from a script that may be re-executed. Method signatures must remain stable across calls — passing a callable whose type annotations differ from the existing registration raises TypeError. Re-registering with a different superclass also raises TypeError.

Parameters:
  • name (str) – Class name. Used as the Objective-C class name and as the lookup key for replacement on subsequent calls.

  • superclass – Parent class. Defaults to NSObject. May be an ObjCClass, a class name string, or any value ObjCClass can coerce.

  • methods – Iterable of Python callables to register as instance methods. Each callable’s __name__ is converted to a selector by replacing underscores with colons.

  • classmethods – Iterable of Python callables to register as class methods on the metaclass.

  • protocols – Iterable of ObjCProtocol instances the class declares conformance to. class_addProtocol is idempotent, so adding a protocol the class already conforms to is harmless.

Returns:

The registered ObjCClass. Re-registration returns the same runtime class as previous calls with the same name (Objective-C class identity is process-global).

Raises:

TypeError – If name is not a string, if re-registration is attempted with a different superclass, or if a method’s type signature differs from a previous registration.

juno.objc.on_main_thread(fn=None, *, async_=False)

Run the decorated function on the main thread.

UIKit calls must originate from the main thread. The decorator wraps a function so that invoking it from a worker thread (the notebook cell or script context) marshals the call onto the main thread, blocks until it returns, and propagates the return value or exception back to the caller.

Use this for synchronous, value-returning UIKit / Foundation queries the script drives directly: reading a UIDevice property, building a UIBezierPath, measuring text via UIFont, or composing UIKit objects that don’t need callbacks.

Do not use this with UIKit APIs that fire Block-based callbacks (UIAlertAction handlers, completion blocks invoked from the main runloop, delegate methods triggered by UIKit). Blocks fired from UIKit’s main runloop can’t reliably re-enter your script’s Python state in this release — the result is a silent hang or memory corruption. For interactive UI, use the curated bridges instead: juno.dialogs for alerts / sheets / pickers, juno.sharing for the share sheet, juno.preview for file / image preview.

Already-on-main-thread calls bypass the dispatch and execute inline.

Stop is not honoured synchronously while the main-thread callable is running — sync dispatch is not interruptible mid-flight (interrupting UIKit work would leave it half-finished). After the callable completes, the bridge checks for pending Stop before returning:

  • If a Stop signal has already arrived by then, the decorated call itself raises KeyboardInterrupt instead of returning the value.

  • Otherwise Stop surfaces at the next normal interrupt checkpoint on the worker side (typically the next bytecode dispatch).

If the callable raises, the exception always propagates as-is — a pending Stop never masks the callable’s own error.

Parameters:
  • fn – The function to wrap. May be passed via decorator syntax (@on_main_thread) without parentheses.

  • async – Fire-and-forget mode (queue the call on main and return immediately to the caller). Not supported in this release; passing True raises NotImplementedError.

Returns:

A wrapper that, when called, dispatches the original function to the main thread.

Raises: