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-exports —
ObjCClass,ObjCInstance,Block,ObjCBlock,autoreleasepool(),send_message(). Re-exported with rubicon-objc identity preserved so anything written againstrubicon.objckeeps working when imported fromjuno.objc.Juno helpers —
load_framework(),ns(),nsurl(),nsdata_to_bytes(),uiimage_to_png(),sel(),create_objc_class(),on_main_thread().Pre-bound Foundation/UIKit classes —
NSObject,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 aUIBezierPath, composing UIKit objects) withon_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 (
UIAlertActionhandlers, 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.objcis 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
.frameworksuffix (for example,"UIKit"or"CoreLocation").- Returns:
The loaded framework as a
ctypes.CDLLhandle.- Raises:
- juno.objc.ns(value)¶
Convert a Python value to its Foundation counterpart.
Conversions, applied in order so subclasses don’t shadow their bases:
None→NSNullsingleton.ObjCInstance→ returned unchanged (idempotent).bool→NSNumber(boxed BOOL). Checked beforeintsoTruedoes not silently collapse into a boxed integer.int→NSNumber(boxedNSInteger).float→NSNumber(boxeddouble).str→NSString.bytes/bytearray/memoryview→NSData(length-preserving — embedded NUL bytes are retained).datetime.date/datetime.datetime→NSDate.dict→NSDictionary. Both keys and values are passed throughns()recursively.list/tuple→NSArray. Elements are passed throughns()recursively.
- Parameters:
value – Python value to convert.
- Returns:
The Foundation object representing
value.- Raises:
TypeError – If
valuehas a type with no defined mapping.
- juno.objc.nsurl(s)¶
Return an
NSURLfor a URL or filesystem path.The argument is treated as a URL if it contains
"://"; otherwise it’s interpreted as a filesystem path.
- juno.objc.nsdata_to_bytes(data)¶
Copy the contents of an
NSDatainstance tobytes.Length-aware — embedded NUL bytes are preserved.
- juno.objc.uiimage_to_png(image)¶
Encode a
UIImageas PNG bytes.Calls UIKit’s
UIImagePNGRepresentationdirectly — 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
UIImageinstance (anObjCInstance).- Returns:
PNG-encoded image bytes.
- Raises:
TypeError – If
imageis not anObjCInstance.RuntimeError – If UIKit returns
nilfor the PNG conversion.
- Return type:
- juno.objc.sel(name)¶
Return a registered selector for an Objective-C method name.
- 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
methodsbecomes 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:definedef 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 raisesTypeError. Re-registering with a different superclass also raisesTypeError.- 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 anObjCClass, a class name string, or any valueObjCClasscan 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
ObjCProtocolinstances the class declares conformance to.class_addProtocolis 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
nameis 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 (
UIAlertActionhandlers, 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.dialogsfor alerts / sheets / pickers,juno.sharingfor the share sheet,juno.previewfor 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
KeyboardInterruptinstead 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
TrueraisesNotImplementedError.
- Returns:
A wrapper that, when called, dispatches the original function to the main thread.
- Raises:
NotImplementedError – If
async_=True.RuntimeError – If called from a non-main thread on a host that lacks the dispatch bridge.