;;;-*-Mode: LISP; Package: CCL -*- (in-package "CCL") #+darwin-target (unless (>= (parse-integer (software-version) :junk-allowed t) 10) (error "the Objective-C bridge needs at least Mac OS X 10.6")) (eval-when (:compile-toplevel :load-toplevel :execute) (require "BRIDGE")) (defun allocate-objc-object (class) (#/alloc class)) (defun conforms-to-protocol (thing protocol) (#/conformsToProtocol: thing (objc-protocol-address protocol))) #+(or apple-objc cocotron-objc) (defun iterate-over-objc-classes (fn) (let* ((n (#_objc_getClassList (%null-ptr) 0))) (declare (fixnum n)) (%stack-block ((buffer (the fixnum (ash n target::word-shift)))) (#_objc_getClassList buffer n) (do* ((i 0 (1+ i))) ((= i n) (values)) (declare (fixnum i)) (funcall fn (paref buffer (:* :id) i)))))) #+(or apple-objc cocotron-objc) (defun count-objc-classes () (#_objc_getClassList (%null-ptr) 0)) #+gnu-objc (defun iterate-over-objc-classes (fn) (rletZ ((enum-state :address)) (loop (let* ((class (#_objc_next_class enum-state))) (if (%null-ptr-p class) (return) (funcall fn class)))))) #+gnu-objc (defun count-objc-classes () (let* ((n 0)) (declare (fixnum n)) (rletZ ((enum-state :address)) (if (%null-ptr-p (#_objc_next_class enum-state)) (return n) (incf n))))) (defun %note-protocol (p) ;; In Cocotron (which is ultimately based on the GNU ObjC runtime), ;; it may be the case that some Protocol objects aren't fully initialized ;; when this code runs, hence the sleazy use of PREF here. (with-macptrs ((cname #+cocotron-objc (pref p #>Protocol.nameCString) #-cocotron-objc (objc-message-send p "name" :address))) (let* ((namelen (%cstrlen cname)) (name (make-string namelen))) (declare (dynamic-extent name)) (%str-from-ptr cname namelen name) (let* ((proto (or (gethash name *objc-protocols*) (progn (setq name (subseq name 0)) (setf (gethash name *objc-protocols*) (make-objc-protocol :name name)))))) (unless (objc-protocol-address proto) (setf (objc-protocol-address proto) (%inc-ptr p 0))) proto)))) (defun note-class-protocols (class) #-(or apple-objc-2.0) (do* ((protocols (pref class :objc_class.protocols) (pref protocols :objc_protocol_list.next))) ((%null-ptr-p protocols)) (let* ((count (pref protocols :objc_protocol_list.count))) (with-macptrs ((list (pref protocols :objc_protocol_list.list))) (dotimes (i count) (with-macptrs ((p (paref list (:* (:* (:struct :
rotocol))) i))) (%note-protocol p)))))) #+(or apple-objc-2.0) (rlet ((p-out-count :int 0)) (with-macptrs ((protocols (#_class_copyProtocolList class p-out-count))) (let* ((n (pref p-out-count :int))) (dotimes (i n) (with-macptrs ((p (paref protocols (:* (:* (:struct :
rotocol))) i)))
(%note-protocol p))))
(unless (%null-ptr-p protocols) (#_free protocols)))))
(defun map-objc-classes (&optional (lookup-in-database-p t))
(iterate-over-objc-classes
#'(lambda (class)
(note-class-protocols class)
(install-foreign-objc-class class lookup-in-database-p))))
(let* ((nclasses 0)
(lock (make-lock)))
(declare (fixnum nclasses))
(defun maybe-map-objc-classes (&optional use-db)
(with-lock-grabbed (lock)
(let* ((new (count-objc-classes)))
(declare (fixnum new))
(unless (= nclasses new)
(setq nclasses new)
(map-objc-classes use-db))
t)))
(defun reset-objc-class-count ()
(with-lock-grabbed (lock)
(setq nclasses 0))))
(register-objc-class-decls)
(maybe-map-objc-classes t)
(defvar *class-init-keywords* (make-hash-table :test #'eq))
(defun process-init-message (message-info)
(let* ((keys (objc-to-lisp-init (objc-message-info-message-name message-info))))
(when keys
(let* ((keyinfo (cons keys (objc-message-info-lisp-name message-info))))
(dolist (method (objc-message-info-methods message-info))
(when (and (eq :id (objc-method-info-result-type method))
(let* ((flags (objc-method-info-flags method)))
(not (or (memq :class flags)
(memq :protocol flags)))))
(let* ((class (canonicalize-registered-class
(find-objc-class (objc-method-info-class-name method)))))
(pushnew keyinfo (gethash class *class-init-keywords*)
:test #'equal))))))))
(register-objc-init-messages)
(register-objc-set-messages)
(defun all-init-keywords-for-class (c)
(let* ((keyinfo ()))
(dolist (class (class-precedence-list c))
(when (eq class ns:ns-object)
(return keyinfo))
(dolist (class-keys (gethash class *class-init-keywords*))
(pushnew class-keys keyinfo :test #'equal)))))
(defun send-init-message-for-class (class initargs)
(let* ((all-keywords-for-class (all-init-keywords-for-class class)))
(multiple-value-bind (initfunction args)
(if all-keywords-for-class
(let* ((candidate-functions ())
(candidate-arglists ())
(missing-keyword (cons nil nil)))
(declare (dynamic-extent missing-keyword))
(dolist (keys-and-function all-keywords-for-class)
(collect ((arglist))
(destructuring-bind (keys . function) keys-and-function
(dolist (key keys (progn (push function candidate-functions)
(push (arglist) candidate-arglists)))
(let* ((val (getf initargs key missing-keyword)))
(if (eq missing-keyword val)
(return)
(arglist val)))))))
(if candidate-functions
(if (null (cdr candidate-functions))
(values (car candidate-functions) (car candidate-arglists))
;; Pick the longest match, if that's unique. If there's
;; no unique longest match, complain.
(let* ((maxlen 0)
(maxfun ())
(maxargs ())
(duplicate-match nil))
(declare (fixnum maxlen))
(do* ((functions candidate-functions (cdr functions))
(arglists candidate-arglists (cdr arglists)))
((null functions)
(if duplicate-match
(values nil nil)
(values maxfun maxargs)))
(let* ((arglist (car arglists))
(n (length arglist)))
(declare (fixnum n))
(if (> n maxlen)
(setq maxlen n
duplicate-match nil
maxargs arglist
maxfun (car functions))
(if (= n maxlen)
(setq duplicate-match t)))))))
(values '#/init nil)))
(values '#/init nil))
(if initfunction
(let* ((instance (apply initfunction (#/alloc class) args)))
(ensure-lisp-slots instance class)
instance)
(error "Can't determine ObjC init function for class ~s and initargs ~s." class initargs)))))
#+gnu-objc
(defun iterate-over-class-methods (class method-function)
(do* ((mlist (pref class :objc_class.methods)
(pref mlist :objc_method_list.method_next)))
((%null-ptr-p mlist))
(do* ((n (pref mlist :objc_method_list.method_count))
(i 0 (1+ i))
(method (pref mlist :objc_method_list.method_list)
(%incf-ptr method (record-length :objc_method))))
((= i n))
(declare (fixnum i n))
(funcall method-function method class))))
#+gnu-objc
(progn
;; Er, um ... this needs lots-o-work.
(let* ((objc-class-count 0))
(defun reset-objc-class-count () (setq objc-class-count 0))
(defun note-all-library-methods (method-function)
(do* ((i objc-class-count (1+ i))
(class (id->objc-class i) (id->objc-class i)))
((eq class 0))
(iterate-over-class-methods class method-function)
(iterate-over-class-methods (id->objc-metaclass i) method-function))))
(def-ccl-pointers revive-objc-classes ()
(reset-objc-class-count)))
#+apple-objc-2.0
(progn
(defun setup-objc-exception-globals ()
(flet ((set-global (offset name)
(setf (%get-ptr (%int-to-ptr (+ (target-nil-value) (%kernel-global-offset offset))))
(foreign-symbol-address name))))
(set-global 'objc-2-personality "___objc_personality_v0")
(set-global 'objc-2-begin-catch "objc_begin_catch")
(set-global 'objc-2-end-catch "objc_end_catch")
(set-global 'unwind-resume "__Unwind_Resume")))
(def-ccl-pointers setup-objc-exception-handling ()
(setup-objc-exception-globals))
(setup-objc-exception-globals)
)
(defvar *condition-id-map* (make-id-map) "Map lisp conditions to small integers")
;;; Encapsulate an NSException in a lisp condition.
(define-condition ns-exception (error)
((ns-exception :initarg :ns-exception :accessor ns-exception))
(:report (lambda (c s)
(format s "Objective-C runtime exception: ~&~a"
(nsobject-description (ns-exception c))))))
(defun ensure-dealloc-method-for-class (class)
(let* ((direct-slots (class-direct-slots class))
(effective-slots (class-slots class)))
(when (and (dolist (d direct-slots)
(when (and (typep d 'standard-direct-slot-definition)
(eq :instance (slot-definition-allocation d)))
(return t)))
(dolist (e effective-slots t)
(when (and (typep e 'standard-effective-slot-definition)
(eq :instance (slot-definition-allocation e))
(not (find (slot-definition-name e)
direct-slots
:key #'slot-definition-name
:test #'eq)))
(return))))
(eval `(objc:defmethod (#/dealloc :void) ((self ,(class-name class)))
(objc:remove-lisp-slots self)
(call-next-method))))))
(eval-when (:compile-toplevel :execute)
(declaim (ftype (function (&rest t) t) objc-callback-error-return)))
(defclass ns-lisp-exception (ns::ns-exception)
((condition :initarg :condition :initform nil :reader ns-lisp-exception-condition))
(:metaclass ns::+ns-object))
(objc:defmethod #/init ((self ns-lisp-exception))
(#/initWithName:reason:userInfo: self #@"lisp exception" #@"lisp exception" +null-ptr+))
(defun recognize-objc-exception (x)
(if (typep x 'ns:ns-exception)
(ns-exception->lisp-condition x)))
(pushnew 'recognize-objc-exception *foreign-error-condition-recognizers*)
(defun objc:make-nsstring (string)
(with-encoded-cstrs :utf-8 ((s string))
(#/initWithUTF8String: (#/alloc ns:ns-string) s)))
(defun %make-nsstring (string)
(objc:make-nsstring string))
(defmacro with-autoreleased-nsstring ((nsstring lisp-string) &body body)
`(let* ((,nsstring (%make-nsstring ,lisp-string)))
(#/autorelease ,nsstring)
,@body))
(defmacro objc:with-autoreleased-nsstrings (speclist &body body)
(with-specs-aux 'with-autoreleased-nsstring speclist body))
(defun retain-objc-instance (instance)
(#/retain instance))
;;; May have to create/release autorelease pools before the bridge
;;; is fully reinitialized, so use low-level OBJC-MESSAGE-SEND
;;; and @class.
(defun create-autorelease-pool ()
(objc-message-send
(objc-message-send (@class "NSAutoreleasePool") "alloc") "init"))
(defun release-autorelease-pool (p)
(objc-message-send p "release" :void))
(defun lisp-string-from-nsstring (nsstring)
(with-autorelease-pool
;; It's not clear that it's even possible to lose information
;; when converting to UTF-8, but allow lossage to occur, just in
;; case.
(let* ((data (#/dataUsingEncoding:allowLossyConversion:
nsstring #$NSUTF8StringEncoding t))
(len (#/length data)))
(if (= len 0)
""
(let* ((bytes (#/bytes data))
(nchars (utf-8-length-of-memory-encoding bytes len 0))
(string (make-string nchars)))
(utf-8-memory-decode bytes len 0 string)
string)))))
(objc:defmethod #/reason ((self ns-lisp-exception))
(with-slots (condition) self
(if condition
(#/autorelease (%make-nsstring (format nil "~A" condition)))
(call-next-method))))
(objc:defmethod #/description ((self ns-lisp-exception))
(#/stringWithFormat: ns:ns-string #@"Lisp exception: %@" (#/reason self)))
(defun ns-exception->lisp-condition (nsexception)
(if (typep nsexception 'ns-lisp-exception)
(ns-lisp-exception-condition nsexception)
(make-condition 'ns-exception :ns-exception nsexception)))
(defmethod ns-exception ((c condition))
"Map a lisp condition object to an NSException. Note that instances
of the NS-EXCEPTION condition class implement this by accessing an
instance variable."
;;; Create an NSLispException with a lispid that encapsulates
;;; this condition.
;; (dbg (format nil "~a" c))
;;(#_NSLog #@"Lisp exception: %@" :id (%make-nsstring (format nil "~a" c)))
(make-instance 'ns-lisp-exception :condition c))
#+(or apple-objc cocotron-objc) ; not really
(progn
#+ppc-target
(defun objc-callback-error-return (condition return-value-pointer return-address-pointer)
;; On PPC, the "address" of an external entry point is always
;; aligned on a 32-bit word boundary. On PPC32, it can always
;; be represented as a fixnum; on PPC64, it might be a pointer
;; instead.
;; Note that this clobbers the actual (foreign) return address,
;; replacing it with the address of #__NSRaiseError. Note also
;; that storing the NSException object as the return value has
;; the desired effect of causing #__NSRaiseError to be called
;; with that NSException as its argument (because r3 is used both
;; as the canonical return value register and used to pass the
;; first argument on PPC.)
(process-debug-condition *current-process* condition (%get-frame-ptr))
(let* ((addr (%reference-external-entry-point (load-time-value (external "__NSRaiseError")))))
(if (typep addr 'fixnum)
(%set-object return-address-pointer 0 addr)
(setf (%get-ptr return-address-pointer 0) addr)))
(setf (%get-ptr return-value-pointer 0) (ns-exception condition))
nil)
#+x8664-target
(progn
(defloadvar *x8664-objc-callback-error-return-trampoline*
(let* ((code-bytes '(#x48 #x89 #xc7 ; movq %rax %rdi
#x66 #x48 #x0f #x7e #xc0 ; movd %xmm0,%rax
#x52 ; pushq %rdx
#xff #xe0)) ; jmp *rax
(nbytes (length code-bytes))
(ptr (%allocate-callback-pointer 16)))
(dotimes (i nbytes ptr)
(setf (%get-unsigned-byte ptr i) (pop code-bytes)))))
(defun objc-callback-error-return (condition return-value-pointer return-address-pointer)
;; The callback glue reserves space for %rax at return-value-pointer-8,
;; for %rdx at -16, for %xmm0 at -24. Store NS-EXCEPTION in the
;; %rax slot, the address of #_objc_exception_throw in the %rdx slot, the
;; original return address in the %xmm0 slot, and force a return to
;; the trampoline code above.
(process-debug-condition *current-process* condition (%get-frame-ptr))
(setf (%get-ptr return-value-pointer -8) (ns-exception condition)
(%get-ptr return-value-pointer -16) (%get-ptr return-address-pointer 0)
(%get-ptr return-address-pointer 0) *x8664-objc-callback-error-return-trampoline*)
;; A foreign entry point is always an integer on x8664.
(let* ((addr (%reference-external-entry-point (load-time-value (external "_objc_exception_throw")))))
(if (< addr 0) ;unlikely
(setf (%%get-signed-longlong return-value-pointer -24) addr)
(setf (%%get-unsigned-longlong return-value-pointer -24) addr)))
nil)
)
#+x8632-target
(progn
(defloadvar *x8632-objc-callback-error-return-trampoline*
(let* ((code-bytes '(#x83 #xec #x10 ; subl $16,%esp
#x89 #x04 #x24 ; movl %eax,(%esp)
#x52 ; pushl %edx
#xff #xe1)) ; jmp *ecx
(nbytes (length code-bytes))
(ptr (%allocate-callback-pointer 16)))
(dotimes (i nbytes ptr)
(setf (%get-unsigned-byte ptr i) (pop code-bytes)))))
(defun objc-callback-error-return (condition return-value-pointer return-address-pointer)
(process-debug-condition *current-process* condition (%get-frame-ptr))
(let* ((addr (%reference-external-entry-point (load-time-value (external #+cocotron-objc "_NSRaiseException" #-cocotron-objc "__NSRaiseError")))))
(setf (%get-unsigned-long return-value-pointer -12 ) addr))
(setf (%get-ptr return-value-pointer -8) (ns-exception condition)
(%get-ptr return-value-pointer -4) (%get-ptr return-address-pointer)
(%get-ptr return-address-pointer) *x8632-objc-callback-error-return-trampoline*)
nil)
)
)
(defun open-main-bundle ()
(#/mainBundle ns:ns-bundle))
;;; Create a new immutable dictionary just like src, replacing the
;;; value of each key in key-value-pairs with the corresponding value.
(defun copy-dictionary (src &rest key-value-pairs)
(declare (dynamic-extent key-value-pairs))
;(#_NSLog #@"src = %@" :id src)
(let* ((count (#/count src))
(enum (#/keyEnumerator src))
(keys (#/arrayWithCapacity: ns:ns-mutable-array count))
(values (#/arrayWithCapacity: ns:ns-mutable-array count)))
(loop
(let* ((nextkey (#/nextObject enum)))
(when (%null-ptr-p nextkey)
(return))
(do* ((kvps key-value-pairs (cddr kvps))
(newkey (car kvps) (car kvps))
(newval (cadr kvps) (cadr kvps)))
((null kvps)
;; Copy the key, value pair from the src dict
(#/addObject: keys nextkey)
(#/addObject: values (#/objectForKey: src nextkey)))
(when (#/isEqualToString: nextkey newkey)
(#/addObject: keys nextkey)
(#/addObject: values newval)
(return)))))
(make-instance 'ns:ns-dictionary
:with-objects values
:for-keys keys)))
(defparameter *objc-description-max-length* 1024 "Limit on the length of NSObject description strings if non-NIL.")
(defun %cf-instance-p (instance)
#-apple-objc (declare (ignore instance))
#+apple-objc
(> (objc-message-send instance "_cfTypeID" #>CFTypeID) 1))
(defun initialized-nsobject-p (nsobject)
(or (objc-class-p nsobject)
(objc-metaclass-p nsobject)
(has-lisp-slot-vector nsobject)
(let* ((cf-p (%cf-instance-p nsobject))
(isize (if cf-p (external-call "malloc_size" :address nsobject :size_t) (%objc-class-instance-size (#/class nsobject))))
(skip (if cf-p (+ (record-length :id) 4 #+64-bit-target 4) (record-length :id))))
(declare (fixnum isize skip))
(or (> skip isize)
(do* ((i skip (1+ i)))
((>= i isize))
(declare (fixnum i))
(unless (zerop (the (unsigned-byte 8) (%get-unsigned-byte nsobject i)))
(return t)))))))
(defun nsobject-description (nsobject)
"Returns a lisp string that describes nsobject. Note that some
NSObjects describe themselves in more detail than others."
(if (initialized-nsobject-p nsobject)
(with-autorelease-pool
(let* ((desc (#/description nsobject)))
(if (or (null *objc-description-max-length*)
(< (#/length desc) *objc-description-max-length*))
(lisp-string-from-nsstring desc)
(ns:with-ns-range (r 0 *objc-description-max-length*)
(format nil "~a[...]"(lisp-string-from-nsstring (#/substringWithRange: desc r)))))))
"[uninitialized]"))
(defun lisp-string-from-nsstring-substring (nsstring start length)
(let* ((substring (#/substringWithRange: nsstring (ns:make-ns-range start length))))
(lisp-string-from-nsstring substring)))
(def-standard-initial-binding *listener-autorelease-pool* nil)
(setq *listener-autorelease-pool* (create-autorelease-pool))
(define-toplevel-command :global rap () "Release and reestablish *LISTENER-AUTORELEASE-POOL*"
(when (eql *break-level* 0)
(without-interrupts
(when (boundp '*listener-autorelease-pool*)
(let* ((old *listener-autorelease-pool*))
(if old (release-autorelease-pool old))
(setq *listener-autorelease-pool* (create-autorelease-pool)))))))
#+apple-objc
(defun show-autorelease-pools ()
(objc-message-send (@class ns-autorelease-pool) "showPools" :void))
#+gnu-objc
(defun show-autorelease-pools ()
(do* ((current (objc-message-send (@class ns-autorelease-pool) "currentPool")
(objc-message-send current "_parentAutoreleasePool"))
(i 0 (1+ i)))
((%null-ptr-p current) (values))
(format t "~& ~d : ~a [~d]"
i
(nsobject-description current)
(pref current : ool._released_count))))
#+cocotron-objc
(defun show-autorelease-pools ()
(%string-to-stderr "No info about current thread's autorelease pools is available"))
(define-toplevel-command :global sap () "Log information about current thread's autorelease-pool(s) to C's standard error stream"
(show-autorelease-pools))
(define-toplevel-command :global kap () "Release (but don't reestablish) *LISTENER-AUTORELEASE-POOL*"
(when (eql *break-level* 0)
(without-interrupts
(when (boundp '*listener-autorelease-pool*)
(let* ((p *listener-autorelease-pool*))
(setq *listener-autorelease-pool* nil)
(release-autorelease-pool p))))))
;;; Use the interfaces for an add-on ObjC framework. We need to
;;; tell the bridge to reconsider what it knows about the type
;;; signatures of ObjC messages, since the new headers may define
;;; a method whose type signature differs from the message's existing
;;; methods. (This probably doesn't happen too often, but it's
;;; possible that some SENDs that have already been compiled would
;;; need to be recompiled with that augmented method type info, e.g.,
;;; because ambiguity was introduced.)
(defun augment-objc-interfaces (dirname)
(use-interface-dir dirname)
(register-objc-class-decls)
(update-objc-method-info))
;;; A list of "standard" locations which are known to contain
;;; framework bundles. We should look in ~/Library/Frameworks/" first,
;;; if it exists.
(defparameter *standard-framework-directories*
(list #p"/Library/Frameworks/"
#p"/System/Library/Frameworks/"))
;;; This has to run during application (re-)initializtion, so it
;;; uses lower-level bridge features.
(defun %reload-objc-framework (path)
(when (probe-file path)
(let* ((namestring (native-translated-namestring path)))
(with-cstrs ((cnamestring namestring))
(with-nsstr (nsnamestring cnamestring (length namestring))
(with-autorelease-pool
(let* ((bundle (objc-message-send (@class "NSBundle")
"bundleWithPath:"
:id nsnamestring :id)))
(unless (%null-ptr-p bundle)
(objc-message-send bundle "load" :