;;; plist library for objc (use objc cocoa) (define-objc-classes NSDictionary NSArray) ;; Traverse a plist object (or any object composited of NSDictionaries ;; and NSArrays) using keys sequentially from path-list. General ;; dictionary keys are NSObjects, but plist keys must be NSStrings--so ;; symbol and numeric dict keys in path-list are coerced to strings. ;; Array keys must be numeric and are coerced to integers. (define (plist-path obj path-list) (define (ensure-numeric x) (if (number? x) x (error 'path-list "numeric key required at path index" path-list))) (define (stringify x) (if (or (symbol? x) (number? x)) (->string x) x)) (cond ((null? path-list) obj) ((not (objc:instance? obj)) (error 'plist-path (conc "Received " obj ", expected NSDictionary or NSArray object at path index") path-list)) (else (plist-path (cond ((@ obj is-kind-of-class: NSDictionary) (@ obj object-for-key: (stringify (car path-list)))) ((@ obj is-kind-of-class: NSArray) (@ obj object-at-index: (ensure-numeric (car path-list)))) (else (error 'plist-path ;; Don't print object--it may be gigantic :( "NSDictionary or NSArray object required at path index" path-list))) (cdr path-list))))) ; (plist-path plist `(Playlists 0 Name)) => @"Library" ;; Integers and bools are returned as NSNumber types. We don't auto-convert ;; these, but you can retrieve the value using NSNumber methods e.g. ;; (@ (@ b object-for-key: "Master") boolValue) ;; => #\x1 ;; We can also pass an int or bool in using the NSNumber class explicitly -- ;; i.e. (@ b set-object: (@ NSNumber numberWithLong: 1234) for-key: "Playlist ID") ;; You could create e.g. (ns:array-ref a i) which would internally convert return ;; value, rather than always converting in objc:ref->instance. And, ns:dictionary-set! ;; could convert the argument value. Both could do auto-symbol -> string key conversion. ;; Perhaps plist-set! and plist-ref instead. ;; Can't test the following because objc:PTR pass not supported in bridge: ;; (@ NSValue valueWithBytes: (float->ref 3.14 (make-locative (make-byte-vector 4))) ;; gc? ;; objCType: objc:FLT) ;; The perl plist->hash function at http://www.macdevcenter.com/pub/a/mac/2005/08/02/plist.html?page=4 ;; takes the option whether to convert values or leave as objects; when conversion is enabled, ;; all values are converted to strings -- not the appropriate type (so an NSNumber 3.14 becomes ;; "3.14"). ;; Ruby/Python keep dictionaries and arrays as NSObjects but provide native methods on them to ;; treat like a native dict/array. Ruby does no explicit conversion and it appears can't ;; convert NSNumbers except via calling ObjC methods. Python leaves NSDecimalNumbers alone, ;; converts NSNumber longlong->OC_PythonLong, float/double->OC_PythonFloat, NSNumber ;; else->OC_PythonInt which are subclasses of real number types that keep a pointer to the ObjC ;; object. Passing to ObjC, it converts Py_Bool->NSNumber bool, Py_Int->NSNumber long, ;; Py_Float->NSNumber double. Because plist values can only be or , this does ;; not lose data. ;; Most types are handled via Python bridge conversion, and dictionaries (if originating from ;; Objective C) have python iterators defined upon them but remain ObjC. However, ;; PyObjCTools/Conversion.py provides two conversion functions which convert a collection fully to or ;; from Objective C. These do deep transforms to recursively convert into an actual python ;; array or dictionary, converting every value using the bridge if possible and special casing ;; NSDate, NSData, and NSDecimalNumber. Additionally a thunk is called for unconvertible ;; types. ;; Determining if a number is int or floating point without an objc type guideline is hard. ;; 2.0 is considered a scheme integer, but is inexact. 2^31 is an integer, but is inexact. ;; 2^46 is an integer, but doesn't fit in a machine int. ;; Chicken can check this since "integer" type is fixnum or flonum as long as it fits ;; within a machine int. ;; Python converts NSStrings to python unicode objects automatically (this entails a copy). ;; If you're not going to convert NSStrings to strings, it doesn't save much to automatically convert ;; NSNumbers to scheme numbers, although the reverse could still be done. (define-objc-classes NSNumber) (define (ns:number-value x) (if (ns:number? x) (case (string-ref (@ x objC-type) 0) ((#\c) (objc:char->char-or-bool (@ x char-value))) ((#\f #\d) (@ x double-value)) (else (@ x long-value))) ;; how about long long? (error 'ns:number-value "NSNumber expected" x))) (define ns:string-value objc:nsstring->string) (define (ns:string? x) (and (objc:instance? x) (is-nsstring (objc:instance->pointer x)))) (define (ns:number? x) (and (objc:instance? x) (@ x is-kind-of-class: NSNumber))) ;; slow -- should write in ObjC (define ns:date? (let ((NSDate (objc:string->class "NSDate"))) (lambda (x) (and (objc:instance? x) (@ x is-kind-of-class: NSDate))))) (define ns:make-string objc:nsstring) ;; Numeric conversion is a little tricky; our aim is that machine integers are translated into ;; integer NSNumbers, and flonums [even which pass INTEGER?] become double values. However, ;; any number that fits into a machine int outside of fixnum range becomes an integer, as there is ;; no distinction between int and flonum in that spectrum. This generally satisfies plist ;; applications, for which only two values--integer and real--are used. If a specific type ;; of NSNumber is required, use NSNumber methods to create one. (define (ns:make-number x) (cond ((boolean? x) (@ NSNumber number-with-bool: x)) ;; creates a char and coerces to YES or NO ((char? x) (@ NSNumber number-with-char: x)) ;; Creates an int: but should we convert #\x1 to bool? ((fixnum? x) (@ NSNumber number-with-int: x)) ((not (number? x)) (error 'ns:make-number "number expected" x)) ((and (##sys#fits-in-int? x) (not (##sys#flonum-in-fixnum-range? x))) (@ NSNumber number-with-long: x)) ((rational? x) (@ NSNumber number-with-double: x)) (else (error 'ns:make-number "cannot make ns:number from" x)))) ;; Are SCHEMIFY and OBJCIFY appropriate names? (define (schemify x) (if (objc:instance? x) (cond ((ns:string? x) (ns:string-value x)) ;; types will be checked twice :( ((ns:number? x) (ns:number-value x)) (else ;(error 'schemify "could not schemify objc:instance" x) x)) ;; should we error out? We would have to handle NSDate and NSData then. x)) ;; We don't recurse into lists or alists or hash tables--use list->ns:array or the like ;; in the middle of the collection instead. (define (objcify x) (if (objc:instance? x) x (cond ((string? x) x) ;; ns:make-string done by bridge ((symbol? x) (symbol->string x)) ;; just for convenience ((number? x) (ns:make-number x)) ((boolean? x) (ns:make-number x)) ((char? x) (ns:make-number x)) (else (error 'objcify "could not objcify objc:instance" x))))) ;;; Array creation (define-objc-classes NSMutableArray) ;; Use [NSArray array] to create an NSArray, not alloc/init. (define (list->ns:array x . conv) (let ((conv (:optional conv objcify)) (a [objc:send NSMutableArray array])) (for-each (lambda (o) (objc:send a add-object: (conv o))) x) a)) ;; Example of creating an ns:array with various types, including an embedded ns:array. ;; (list->ns:array (list 1 2 3.5 ;; (list->ns:array (list 6 #f)) ;; #t #\a)) ;; => # ;;; Dictionary creation (define-objc-classes NSMutableDictionary) ;; Special note: plist keys -must- be strings, so we allow the user to convert KEY and ;; VALUE separately using a two argument conversion function. Dictionary keys in ;; general may be any Objective C object. ;; Also note: these names are horrible. ;; Convert key and value appropriately for a generic dictionary. (define (objcify/kv k v) (cons (objcify k) (objcify v))) ;; Convert key and value appropriately for a plist (all keys must be strings). (define (plistify/kv k v) (cons (cond ((ns:string? k) k) (else (->string k))) ;; stringify objc:instance via description? (objcify v))) ;; CONV takes a KEY and VALUE and returns a converted (KEY, VALUE) pair. (define (alist->ns:dictionary alist . conv) (let* ((conv (:optional conv objcify/kv)) (d [objc:send NSMutableDictionary dictionary])) (for-each (lambda (o) (let ((o (conv (car o) (cdr o)))) (objc:send d set-object: (cdr o) for-key: (car o)))) alist) d)) ;; Example use: ;; (alist->ns:dictionary '((5 . 3) (6 . 4))) ;; creates NSNumber keys and values ;; (alist->ns:dictionary '((5 . 3) (6 . 4)) plistify/kv) ;; creates string keys and NSNumber values ;;; Enumeration ;; Finalization note: if the number of live finalizers is greater than the ;; size of ##sys#pending-finalizers, a major GC will be forced for every ;; new finalizer created. Use the -:fXXX option to increase this buffer size ;; and use ns:enumerator-fold (left fold) when performing conversions from ;; objc:instances to scheme objects. ;; Fold-left. (define (ns:enumerator-fold kons knil e) (let loop ((seed knil)) (let ((obj (objc:send e next-object))) (if (not obj) seed (loop (kons obj seed)))))) ;; Fold-right. Because this builds a chain of recursive calls to ;; KONS, a large number of pending objc:instance finalizers can build ;; up even when your KONS successfully converts OBJ to a scheme object, ;; An example is KONS := (lambda (x y) (cons (schemify x) y)) on an ;; NSArray of NSStrings. Use ns:enumerator-fold to avoid this case. (define (ns:enumerator-fold-right kons knil e) (let loop () (let ((obj (objc:send e next-object))) (if (not obj) knil (kons obj ;(schemify obj) (loop)))))) ;(define (cons-schemify x y) ; (cons (schemify x) y)) (define (ns:array->list array . conv) (let ((schemify (:optional conv schemify))) (reverse (ns:enumerator-fold (lambda (x y) (cons (schemify x) y)) '() (@ array object-enumerator))))) ;; We don't need a reverse here as keys are unordered. (define (ns:dictionary->alist dict . conv) (let ((schemify (:optional conv schemify))) (ns:enumerator-fold (lambda (x y) (cons (cons (schemify x) (schemify (objc:send dict object-for-key: x))) y)) '() (@ dict key-enumerator)))) ;;;; Dictionary->alist conversion examples #| (define track1 (@ (@ tracks object-enumerator) next-object)) ;; first key's value ;; example of default conversion (schemify, which is reversible): (ns:dictionary->alist track1) ;; show reversibility (modulo out of order keys) (define (alist-sort a) (sort a (lambda (x y) (stringalist track1)) (alist-sort (ns:dictionary->alist (alist->ns:dictionary (ns:dictionary->alist track1))))) ;; => #t ;; example of using optional dictionary item conversion: augment schemify by checking for ;; NSDate and return its NSString description. Naturally, this is not reversible. (ns:dictionary->alist track1 (lambda (x) (if (ns:date? x) (@ x description) (schemify x)))) |# ;; If you don't care about the keys in a dict, just the values (e.g. under the "Tracks" ;; dictionary we may ignore the numeric keys) you could grab the values with ;; (ns:array->list (@ tracks allValues)). This will be very slow until finalization is fixed, ;; even if you use a conversion function. Alternatively, since ns:array->list is implemented ;; using an object-enumerator, you can use (ns:array->list tracks) to obtain the same thing ;; without an intermediate NSArray. I guess this nomenclature is not ideal.