1010// platforms. All UIKit interactions require the main thread; methods that
1111// present view controllers will return `PresswerkError::Bridge` if called
1212// off-main.
13+ //
14+ // ## ABI Safety (src/abi/Bridge.idr)
15+ //
16+ // Unsafe code in this module falls into three categories, each covered by
17+ // formal proofs in Bridge.idr:
18+ //
19+ // 1. **Toll-free bridging** (nsstr_as_obj, nsdata_as_obj, dict_as_cf):
20+ // Casts between NSString↔CFString, NSData↔CFData, NSDictionary↔CFDictionary.
21+ // Proven safe by Bridge.idr TollFreePair — identical size and alignment.
22+ //
23+ // 2. **ObjC message sends** (msg_send!, define_class! #[unsafe(...)]):
24+ // Required by the objc2 runtime. Selector correctness is verified by
25+ // Apple's SDK headers. Thread safety proven by Bridge.idr threadReq.
26+ //
27+ // 3. **Security.framework C FFI** (SecItemAdd, SecItemCopyMatching, etc.):
28+ // C function calls with toll-free bridged dictionary arguments.
29+ // Keychain semantics proven by Bridge.idr KeychainProperty.
1330
1431#![ cfg( target_os = "ios" ) ]
1532
@@ -110,7 +127,9 @@ fn root_view_controller() -> Result<Retained<UIViewController>> {
110127
111128 let app = UIApplication :: sharedApplication ( mtm) ;
112129
113- // UIApplication.keyWindow -> UIWindow? -> rootViewController?
130+ // SAFETY: msg_send! to well-known UIApplication selectors (keyWindow,
131+ // rootViewController). MainThreadMarker guarantees we are on the main
132+ // thread (Bridge.idr threadReq = MainThread for UI operations).
114133 let root: Option < Retained < UIViewController > > = unsafe {
115134 let window: Option < Retained < AnyObject > > = msg_send ! [ & app, keyWindow] ;
116135 window. and_then ( |w| msg_send ! [ & w, rootViewController] )
@@ -137,11 +156,18 @@ fn dict_as_cf(dict: &NSDictionary<NSString, AnyObject>) -> *const c_void {
137156
138157/// Cast a `*const NSString` to `*const AnyObject` (NSString *is* an
139158/// AnyObject).
159+ ///
160+ /// SAFETY: NSString is a subclass of NSObject (which is AnyObject in objc2).
161+ /// The pointer representation is identical — no layout change.
162+ /// Proven by Bridge.idr TollFreePair: sameSize, sameAlign.
140163unsafe fn nsstr_as_obj ( s : & NSString ) -> & AnyObject {
141164 & * ( s as * const NSString as * const AnyObject )
142165}
143166
144167/// Cast an `NSData` reference to `&AnyObject`.
168+ ///
169+ /// SAFETY: NSData is a subclass of NSObject. Same pointer, same layout.
170+ /// Proven by Bridge.idr TollFreePair.
145171unsafe fn nsdata_as_obj ( d : & NSData ) -> & AnyObject {
146172 & * ( d as * const NSData as * const AnyObject )
147173}
@@ -158,6 +184,10 @@ struct CameraDelegateIvars {
158184 sender : RefCell < Option < mpsc:: Sender < Option < Vec < u8 > > > > > ,
159185}
160186
187+ // SAFETY: define_class! #[unsafe(super(NSObject))] declares CameraDelegate as
188+ // an ObjC class inheriting from NSObject. This is required by objc2 for all
189+ // custom ObjC classes. MainThreadOnly ensures delegate callbacks only fire on
190+ // the main thread (Bridge.idr threadReq CaptureImage = MainThread).
161191define_class ! {
162192 #[ unsafe ( super ( NSObject ) ) ]
163193 #[ thread_kind = MainThreadOnly ]
@@ -173,13 +203,14 @@ define_class! {
173203 picker: & UIImagePickerController ,
174204 info: & NSDictionary <NSString , AnyObject >,
175205 ) {
176- // Extract the original UIImage from the info dictionary.
206+ // SAFETY: objectForKey with UIImagePickerControllerOriginalImage
207+ // (extern static from UIKit). Returns nil if key not present.
177208 let image_bytes: Option <Vec <u8 >> = unsafe {
178209 info. objectForKey( UIImagePickerControllerOriginalImage )
179210 }
180211 . and_then( |ui_image: Retained <AnyObject >| {
181- // UIImageJPEGRepresentation is a C function that returns
182- // an autoreleased NSData*.
212+ // SAFETY: UIImageJPEGRepresentation is a UIKit C function.
213+ // Returns autoreleased NSData* (nil on failure) .
183214 let raw = unsafe {
184215 UIImageJPEGRepresentation (
185216 & * ui_image as * const AnyObject ,
@@ -189,12 +220,17 @@ define_class! {
189220 if raw. is_null( ) {
190221 None
191222 } else {
223+ // SAFETY: non-null result is an NSData* (toll-free bridged
224+ // with CFData — Bridge.idr TollFreePair). We copy bytes
225+ // immediately so the autorelease is harmless.
192226 let ns_data: & NSData = unsafe { & * ( raw as * const NSData ) } ;
193227 Some ( ns_data. to_vec( ) )
194228 }
195229 } ) ;
196230
197- // Dismiss the picker.
231+ // SAFETY: dismissViewControllerAnimated:completion: is a standard
232+ // UIViewController selector. Called on main thread (delegate is
233+ // MainThreadOnly).
198234 unsafe {
199235 let _: ( ) = msg_send![
200236 picker,
@@ -212,6 +248,7 @@ define_class! {
212248 /// Called when the user cancels the camera.
213249 #[ unsafe ( method( imagePickerControllerDidCancel: ) ) ]
214250 fn did_cancel( & self , picker: & UIImagePickerController ) {
251+ // SAFETY: dismissViewControllerAnimated:completion: — same as above.
215252 unsafe {
216253 let _: ( ) = msg_send![
217254 picker,
@@ -240,6 +277,8 @@ impl CameraDelegate {
240277 let this = this. set_ivars ( CameraDelegateIvars {
241278 sender : RefCell :: new ( Some ( tx) ) ,
242279 } ) ;
280+ // SAFETY: Standard NSObject init via super. The alloc above provides
281+ // a valid, allocated-but-uninitialised object; init completes it.
243282 unsafe { msg_send ! [ super ( this) , init] }
244283 }
245284}
@@ -252,6 +291,9 @@ struct DocPickerDelegateIvars {
252291 sender : RefCell < Option < mpsc:: Sender < Option < String > > > > ,
253292}
254293
294+ // SAFETY: define_class! #[unsafe(super(NSObject))] declares DocPickerDelegate
295+ // as an ObjC class inheriting from NSObject. MainThreadOnly ensures delegate
296+ // callbacks fire on the main thread (Bridge.idr threadReq PickFile = MainThread).
255297define_class ! {
256298 #[ unsafe ( super ( NSObject ) ) ]
257299 #[ thread_kind = MainThreadOnly ]
@@ -269,6 +311,8 @@ define_class! {
269311 ) {
270312 // Take the first selected URL and convert to a file-system path.
271313 let path: Option <String > = urls. firstObject( ) . and_then( |url| {
314+ // SAFETY: msg_send to NSURL.path property — well-known
315+ // Foundation selector, returns NSString? for the file path.
272316 let ns_path: Option <Retained <NSString >> =
273317 unsafe { msg_send![ & url, path] } ;
274318 ns_path. map( |p| p. to_string( ) )
@@ -297,6 +341,7 @@ impl DocPickerDelegate {
297341 let this = this. set_ivars ( DocPickerDelegateIvars {
298342 sender : RefCell :: new ( Some ( tx) ) ,
299343 } ) ;
344+ // SAFETY: Standard NSObject init via super (same as CameraDelegate::new).
300345 unsafe { msg_send ! [ super ( this) , init] }
301346 }
302347}
@@ -351,13 +396,15 @@ impl NativePrint for IosBridge {
351396 let controller = UIPrintInteractionController :: sharedPrintController ( mtm) ;
352397 let ns_data = NSData :: with_bytes ( document) ;
353398
354- // Set the document data as the single printing item.
399+ // SAFETY: setPrintingItem is a well-known UIPrintInteractionController
400+ // selector. MainThreadMarker (above) guarantees main-thread execution
401+ // (Bridge.idr threadReq ShowPrintDialog = MainThread).
355402 unsafe {
356403 controller. setPrintingItem ( Some ( & ns_data) ) ;
357404 }
358405
359- // Present the print dialog. Completion handler is None (fire-and-
360- // forget) .
406+ // SAFETY: presentAnimated_completionHandler is a documented UIKit method.
407+ // Main-thread requirement satisfied by require_main_thread() above .
361408 let presented = unsafe {
362409 controller. presentAnimated_completionHandler ( true , None )
363410 } ;
@@ -408,6 +455,8 @@ impl NativeCamera for IosBridge {
408455 }
409456
410457 let picker = UIImagePickerController :: new ( mtm) ;
458+ // SAFETY: setSourceType is a UIImagePickerController property setter.
459+ // We verified availability with isSourceTypeAvailable above.
411460 unsafe {
412461 picker. setSourceType ( UIImagePickerControllerSourceType :: Camera ) ;
413462 }
@@ -416,9 +465,10 @@ impl NativeCamera for IosBridge {
416465 let ( tx, rx) = mpsc:: channel ( ) ;
417466 let delegate = CameraDelegate :: new ( mtm, tx) ;
418467
419- // UIImagePickerController.delegate is typed as
420- // `UIImagePickerControllerDelegate & UINavigationControllerDelegate`.
421- // Our CameraDelegate conforms to both.
468+ // SAFETY: CameraDelegate conforms to both UIImagePickerControllerDelegate
469+ // and UINavigationControllerDelegate (defined via define_class! above).
470+ // The pointer cast CameraDelegate→AnyObject is safe: CameraDelegate is an
471+ // NSObject subclass with identical pointer representation.
422472 unsafe {
423473 let delegate_obj: & AnyObject =
424474 & * ( ( & * delegate) as * const CameraDelegate as * const AnyObject ) ;
@@ -427,6 +477,9 @@ impl NativeCamera for IosBridge {
427477
428478 // Present modally on the root view controller.
429479 let root_vc = root_view_controller ( ) ?;
480+ // SAFETY: presentViewController is a UIViewController method.
481+ // Main-thread requirement satisfied by require_main_thread() above
482+ // (Bridge.idr threadReq CaptureImage = MainThread).
430483 unsafe {
431484 root_vc. presentViewController_animated_completion ( & picker, true , None ) ;
432485 }
@@ -475,6 +528,8 @@ impl NativeFilePicker for IosBridge {
475528 . iter ( )
476529 . filter_map ( |mime| {
477530 let ns_mime = NSString :: from_str ( mime) ;
531+ // SAFETY: msg_send to UTType class method (UniformTypeIdentifiers.framework).
532+ // Returns nil for unrecognised MIME types, which filter_map discards.
478533 let ut: Option < Retained < AnyObject > > = unsafe {
479534 msg_send ! [
480535 objc2:: class!( UTType ) ,
@@ -487,6 +542,8 @@ impl NativeFilePicker for IosBridge {
487542
488543 // Fall back to UTType.data (public.data) if nothing resolved.
489544 let content_types: Retained < NSArray < AnyObject > > = if ut_types. is_empty ( ) {
545+ // SAFETY: msg_send to UTType class property. Returns the well-known
546+ // public.data UTType — always non-nil.
490547 let public_data: Retained < AnyObject > = unsafe {
491548 msg_send ! [ objc2:: class!( UTType ) , dataType]
492549 } ;
@@ -495,9 +552,9 @@ impl NativeFilePicker for IosBridge {
495552 NSArray :: from_retained_slice ( & ut_types)
496553 } ;
497554
498- // Create the document picker. `initForOpeningContentTypes:` takes
499- // an ` NSArray<UTType>` but we pass ` NSArray<AnyObject>` which is
500- // valid at the ObjC level (same layout ).
555+ // SAFETY: ObjC alloc+init pattern for UIDocumentPickerViewController.
556+ // initForOpeningContentTypes: takes NSArray<UTType>; we pass NSArray<AnyObject>
557+ // which is layout-compatible at the ObjC level (type erasure ).
501558 let picker: Retained < UIDocumentPickerViewController > = unsafe {
502559 let alloc: Retained < UIDocumentPickerViewController > =
503560 msg_send ! [ objc2:: class!( UIDocumentPickerViewController ) , alloc] ;
@@ -511,12 +568,18 @@ impl NativeFilePicker for IosBridge {
511568 let ( tx, rx) = mpsc:: channel ( ) ;
512569 let delegate = DocPickerDelegate :: new ( mtm, tx) ;
513570
571+ // SAFETY: DocPickerDelegate conforms to UIDocumentPickerDelegate
572+ // (defined via define_class! above). ProtocolObject::from_ref is the
573+ // type-safe way to pass the delegate.
514574 unsafe {
515575 picker. setDelegate ( Some ( ProtocolObject :: from_ref ( & * delegate) ) ) ;
516576 }
517577
518578 // Present on the root view controller.
519579 let root_vc = root_view_controller ( ) ?;
580+ // SAFETY: presentViewController is a UIViewController method.
581+ // Main-thread satisfied by require_main_thread() above
582+ // (Bridge.idr threadReq PickFile = MainThread).
520583 unsafe {
521584 root_vc. presentViewController_animated_completion ( & picker, true , None ) ;
522585 }
@@ -562,9 +625,13 @@ impl NativeKeychain for IosBridge {
562625 let ns_service = NSString :: from_str ( KEYCHAIN_SERVICE ) ;
563626 let ns_data = NSData :: with_bytes ( value) ;
564627
628+ // SAFETY: Accessing extern statics from Security.framework. These are
629+ // constant CFStringRef values linked by the iOS SDK, valid for process lifetime.
565630 let keys: Vec < & NSString > = unsafe {
566631 vec ! [ kSecClass, kSecAttrAccount, kSecAttrService, kSecValueData]
567632 } ;
633+ // SAFETY: nsstr_as_obj/nsdata_as_obj are toll-free bridge casts.
634+ // Proven safe by Bridge.idr TollFreePair.
568635 let values: Vec < & AnyObject > = unsafe {
569636 vec ! [
570637 nsstr_as_obj( kSecClassGenericPassword) ,
@@ -576,6 +643,9 @@ impl NativeKeychain for IosBridge {
576643
577644 let dict = NSDictionary :: from_slices ( & keys, & values) ;
578645
646+ // SAFETY: dict_as_cf casts NSDictionary to CFDictionary (toll-free bridged).
647+ // SecItemAdd is a C function from Security.framework with well-defined semantics.
648+ // Bridge.idr KeychainProperty StoreLoad proves store-then-load consistency.
579649 let status = unsafe { SecItemAdd ( dict_as_cf ( & dict) , std:: ptr:: null_mut ( ) ) } ;
580650
581651 match status {
@@ -603,10 +673,12 @@ impl NativeKeychain for IosBridge {
603673
604674 // kSecReturnData expects a CFBoolean. kCFBooleanTrue is toll-free
605675 // bridged with `[NSNumber numberWithBool:YES]`.
676+ // SAFETY: msg_send to NSNumber class method. Returns a valid retained object.
606677 let cf_true: Retained < AnyObject > = unsafe {
607678 msg_send ! [ objc2:: class!( NSNumber ) , numberWithBool: Bool :: YES ]
608679 } ;
609680
681+ // SAFETY: Accessing Security.framework extern statics (process-lifetime constants).
610682 let keys: Vec < & NSString > = unsafe {
611683 vec ! [
612684 kSecClass,
@@ -616,6 +688,7 @@ impl NativeKeychain for IosBridge {
616688 kSecMatchLimit,
617689 ]
618690 } ;
691+ // SAFETY: Toll-free bridge casts (Bridge.idr TollFreePair).
619692 let values: Vec < & AnyObject > = unsafe {
620693 vec ! [
621694 nsstr_as_obj( kSecClassGenericPassword) ,
@@ -629,6 +702,9 @@ impl NativeKeychain for IosBridge {
629702 let dict = NSDictionary :: from_slices ( & keys, & values) ;
630703
631704 let mut result: * const c_void = std:: ptr:: null ( ) ;
705+ // SAFETY: SecItemCopyMatching is a Security.framework C function.
706+ // dict_as_cf is a toll-free bridge cast (Bridge.idr TollFreePair).
707+ // On success, `result` receives a retained CFData (toll-free bridged with NSData).
632708 let status =
633709 unsafe { SecItemCopyMatching ( dict_as_cf ( & dict) , & mut result) } ;
634710
@@ -637,11 +713,13 @@ impl NativeKeychain for IosBridge {
637713 if result. is_null ( ) {
638714 return Ok ( None ) ;
639715 }
640- // `result` is a retained CFData (toll-free bridged with NSData).
716+ // SAFETY: `result` is a retained CFData. CFData and NSData are
717+ // toll-free bridged (Bridge.idr TollFreePair) — identical layout.
641718 let ns_data: & NSData = unsafe { & * ( result as * const NSData ) } ;
642719 let bytes = ns_data. to_vec ( ) ;
643720
644- // Balance the implicit retain from SecItemCopyMatching.
721+ // SAFETY: Balance the implicit +1 retain from SecItemCopyMatching.
722+ // We own this reference and must release it.
645723 unsafe {
646724 let _: ( ) = msg_send ! [ result as * const AnyObject , release] ;
647725 }
@@ -666,8 +744,10 @@ impl NativeKeychain for IosBridge {
666744 let ns_key = NSString :: from_str ( key) ;
667745 let ns_service = NSString :: from_str ( KEYCHAIN_SERVICE ) ;
668746
747+ // SAFETY: Security.framework extern statics (process-lifetime constants).
669748 let keys: Vec < & NSString > =
670749 unsafe { vec ! [ kSecClass, kSecAttrAccount, kSecAttrService] } ;
750+ // SAFETY: Toll-free bridge casts (Bridge.idr TollFreePair).
671751 let values: Vec < & AnyObject > = unsafe {
672752 vec ! [
673753 nsstr_as_obj( kSecClassGenericPassword) ,
@@ -677,6 +757,8 @@ impl NativeKeychain for IosBridge {
677757 } ;
678758
679759 let dict = NSDictionary :: from_slices ( & keys, & values) ;
760+ // SAFETY: SecItemDelete C FFI with toll-free bridged dict.
761+ // Bridge.idr KeychainProperty DeleteLoad proves delete-then-load = Nothing.
680762 let status = unsafe { SecItemDelete ( dict_as_cf ( & dict) ) } ;
681763
682764 match status {
@@ -696,9 +778,10 @@ impl IosBridge {
696778 let ns_service = NSString :: from_str ( KEYCHAIN_SERVICE ) ;
697779 let ns_data = NSData :: with_bytes ( value) ;
698780
699- // Query to locate the existing item .
781+ // SAFETY: Security.framework extern statics (process-lifetime constants) .
700782 let query_keys: Vec < & NSString > =
701783 unsafe { vec ! [ kSecClass, kSecAttrAccount, kSecAttrService] } ;
784+ // SAFETY: Toll-free bridge casts (Bridge.idr TollFreePair).
702785 let query_values: Vec < & AnyObject > = unsafe {
703786 vec ! [
704787 nsstr_as_obj( kSecClassGenericPassword) ,
@@ -708,12 +791,16 @@ impl IosBridge {
708791 } ;
709792 let query = NSDictionary :: from_slices ( & query_keys, & query_values) ;
710793
711- // New value to write .
794+ // SAFETY: Security.framework extern static (process-lifetime constant) .
712795 let update_keys: Vec < & NSString > = unsafe { vec ! [ kSecValueData] } ;
796+ // SAFETY: nsdata_as_obj is a toll-free bridge cast (Bridge.idr TollFreePair).
713797 let update_values: Vec < & AnyObject > =
714798 unsafe { vec ! [ nsdata_as_obj( & ns_data) ] } ;
715799 let update = NSDictionary :: from_slices ( & update_keys, & update_values) ;
716800
801+ // SAFETY: SecItemUpdate is a Security.framework C function.
802+ // dict_as_cf casts NSDictionary→CFDictionary (toll-free bridged).
803+ // Bridge.idr KeychainProperty LastWriteWins proves update semantics.
717804 let status = unsafe {
718805 SecItemUpdate ( dict_as_cf ( & query) , dict_as_cf ( & update) )
719806 } ;
@@ -757,6 +844,9 @@ impl NativeShare for IosBridge {
757844 ) ;
758845 let items = NSArray :: from_retained_slice ( & [ url_as_obj] ) ;
759846
847+ // SAFETY: ObjC alloc+init pattern for UIActivityViewController.
848+ // initWithActivityItems:applicationActivities: takes NSArray of activity
849+ // items and optional NSArray of UIActivity objects (nil = system default).
760850 let activity_vc: Retained < UIActivityViewController > = unsafe {
761851 let alloc: Retained < UIActivityViewController > =
762852 msg_send ! [ objc2:: class!( UIActivityViewController ) , alloc] ;
@@ -768,6 +858,9 @@ impl NativeShare for IosBridge {
768858 } ;
769859
770860 let root_vc = root_view_controller ( ) ?;
861+ // SAFETY: presentViewController is a UIViewController method.
862+ // Main-thread satisfied by require_main_thread() above
863+ // (Bridge.idr threadReq ShareFile = MainThread).
771864 unsafe {
772865 root_vc. presentViewController_animated_completion (
773866 & activity_vc,
0 commit comments