git.fiddlerwoaroof.com
objc-runtime.lisp
48df527e
 (in-package :objc-runtime)
da75eb42
 (serapeum:eval-always
  (named-readtables:in-readtable :objc-readtable))
48df527e
 
 (serapeum:eval-always
   (cffi:define-foreign-library cocoa
     (:darwin (:framework "Cocoa")))
ae1d3bf8
   (cffi:define-foreign-library foundation
48df527e
     (:darwin (:framework "Foundation")))
ae1d3bf8
   (cffi:define-foreign-library appkit
     (:darwin (:framework "AppKit"))))
48df527e
 
 (use-foreign-library foundation)
 (use-foreign-library cocoa)
 (use-foreign-library appkit)
 
 (defctype o-class :pointer)
 (defctype o-selector :pointer)
 
 (defcfun (objc-look-up-class "objc_lookUpClass" :library foundation)
     o-class
   (name :string))
 
 (defcfun (objc-allocate-class-pair "objc_allocateClassPair" :library foundation)
     :pointer
   (superclass :pointer)
   (name :string)
   (extra-bytes :int))
 
5e082198
 (defcfun (objc-register-class-pair "objc_registerClassPair" :library foundation)
     :void
   (superclass :pointer))
 
48df527e
 (defcfun (objc-get-protocol "objc_getProtocol" :library foundation)
     :pointer
   (name :string))
 
 (defcfun (class-add-protocol "class_addProtocol" :library foundation)
     :boolean
   (class :pointer)
   (protocol :pointer))
 
5e082198
 (serapeum:eval-always
   (defctype sizet
       :ulong
382ab041
     #+32-bit-target :uint))
5e082198
 
 (defcfun (class-add-ivar "class_addIvar" :library foundation)
     :boolean
   (class :pointer)
   (name :string)
   (size :ulong)
   (alignment :uint8)
   (types :string))
 
 (defun add-pointer-ivar (class name)
   (class-add-ivar class name
                   (foreign-type-size :pointer)
                   (floor (log (foreign-type-size :pointer)
                               2))
                   "@"))
 
382ab041
 
 #+nil
 (defun make-app-delegate-class (outlets)
   (let ((app-delegate-class (objc-runtime::objc-allocate-class-pair
                              #@NSObject "AppDelegate" 0)))
     (objc-runtime:add-pointer-ivar app-delegate-class "window")
     (objc-runtime:add-pointer-ivar app-delegate-class "delegate")
 
     (loop for outlet in outlets do
074f64b0
       (objc-runtime:add-pointer-ivar app-delegate-class outlet))
382ab041
 
     app-delegate-class))
 
3d652c2e
 #+(or)
382ab041
 (defun %setup-objc-class (name base ivars)
   (let ((class-pair (objc-allocate-class-pair base name 0)))
     (loop for ivar in ivars
074f64b0
           )))
382ab041
 
48df527e
 (defcfun (objc-class-get-name "class_getName" :library foundation)
     :string
   (cls o-class))
 
 (defcfun (objc-class-get-superclass "class_getSuperclass" :library foundation)
     :pointer
   (cls o-class))
 
 (defcfun (objc-get-class-list "objc_getClassList" :library foundation)
     :int
   (cls-buffer o-class)
   (buffer-count :int))
 
 (defcfun (sel-register-name "sel_registerName" :library foundation)
     o-selector
   (name :string))
 
9a92915d
 (defmacro safe-objc-msg-send (result-type thing selector &rest args)
   (alexandria:once-only (thing selector)
     `(if [,thing @(respondsToSelector:) :pointer ,selector]b
          ,(ccase result-type
60bee14d
             (:string `[,thing ,selector ,@args]s)
             (:nsstring `[,thing ,selector ,@args]@)
             (:pointer `[,thing ,selector ,@args])
             (:int `[,thing ,selector ,@args]#)
             (:bool `[,thing ,selector ,@args]b))
9a92915d
          (error "invalid selector"))))
 
d5a8a26b
 ;;; This is a macro, because objc-msg-send is a macro.... which makes "apply" impossible
 ;;; \o/
 (defmacro objc-msg-send-nsstring (thing selector &rest args)
   `[[,thing ,selector ,@args] @(UTF8String)]s)
279ed7b0
 
9a92915d
 (defmacro objc-msg-send-bool (thing selector &rest args)
   `(= 1 [,thing ,selector ,@args]#))
 
48df527e
 (defcfun (class-copy-method-list "class_copyMethodList" :library foundation)
     :pointer
   (cls o-class)
   (numMethods (:pointer :int)))
 
 (defcfun (method-get-name "method_getName")
a17f757e
     :string
48df527e
   (method :pointer))
 
10e90099
 (defcfun (method-get-type-encoding "method_getTypeEncoding")
     :string
   (method :pointer))
 
48df527e
 (defcfun (sel-get-name "sel_getName")
     :string
   (sel o-selector))
 
4abbc169
 (defcfun (class-get-instance-variable "class_getInstanceVariable" :library foundation)
     :pointer
   (cls o-class)
   (name :string))
 
6b08248f
 (defcfun (class-add-method "class_addMethod" :library foundation)
     :boolean
   (class :pointer)
   (selector :pointer)
   (cb :pointer)
4abbc169
   (type :string))
 
6b08248f
 
4abbc169
 (defcfun (object-get-class "object_getClass" :library foundation)
     :pointer
   (object :pointer))
 
 (defcfun (object-get-ivar "object_getIvar" :library foundation)
     :pointer
   (object :pointer)
   (ivar :pointer))
 
5e082198
 (defcfun (object-get-instance-variable "object_getInstanceVariable" :library foundation)
     :pointer
   (object :pointer)
   (name :string)
   (out :pointer))
 
4abbc169
 (defcfun (class-get-property "class_getProperty" :library foundation)
     :pointer
   (cls o-class)
   (name :string))
 
eaa1c811
 (defcstruct objc-property-attribute-t
   (name :string)
   (value :string))
 
 (defcfun (class-add-property "class_addProperty" :library foundation)
     :pointer
   (cls o-class)
   (name :string)
   (attributes (:pointer (:struct objc-property-attribute-t)))
   (attribute-count :unsigned-int))
 
4abbc169
 (defcfun (property-copy-attribute-value "property_copyAttributeValue" :library foundation)
     :string
   (prop :pointer)
   (name :string))
 
 
 (defcfun (property-get-attributes "property_getAttributes" :library foundation)
     :string
   (prop :pointer))
 
279ed7b0
 (defun get-classes ()
d5a8a26b
   (let ((num-classes (objc-get-class-list (null-pointer) 0))
         (result (list)))
279ed7b0
     (with-foreign-object (classes :pointer num-classes)
d5a8a26b
       (dotimes (n (objc-get-class-list classes num-classes) (nreverse result))
85382f54
         (push (mem-aref classes :pointer n)
d5a8a26b
               result)))))
279ed7b0
 
48df527e
 (defgeneric get-methods (class)
   (:method ((class string))
     (get-methods (objc-look-up-class class)))
49baa990
 
   #+ccl
   (:method ((class ccl:macptr))
     (with-foreign-object (num-methods :int)
       (let ((methods (class-copy-method-list class num-methods)))
         (let ((result (list)))
           (dotimes (n (mem-aref num-methods :int) (nreverse result))
             (push (mem-aref methods :pointer n)
                   result))))))
 
48df527e
   #+sbcl
   (:method ((class sb-sys:system-area-pointer))
     (with-foreign-object (num-methods :int)
       (let ((methods (class-copy-method-list class num-methods)))
         (let ((result (list)))
           (dotimes (n (mem-aref num-methods :int) (nreverse result))
             (push (mem-aref methods :pointer n)
                   result)))))))
 
382ab041
 (defmethod get-methods (f)
   (list))
 
116081f3
 
6b08248f
 (defun make-nsstring (str)
5fbe87c1
   [[#@NSString @(alloc)] @(initWithCString:encoding:) :string str :uint 4])
6b08248f
 
6f42890d
 (defun extract-nsstring (ns-str)
   [ns-str @(UTF8String)]s)
6b08248f
 
d5a8a26b
 (defun get-method-name (method)
   (sel-get-name (method-get-name method)))
 
48df527e
 (defun get-method-names (thing)
   (mapcar (alexandria:compose #'sel-get-name
                               #'method-get-name)
           (get-methods thing)))
 
 (defgeneric graph->dot (graph stream)
   (:method :around (graph stream)
6b08248f
      (declare (ignore graph))
48df527e
 	   (format stream "~&digraph {~%~4trankdir=LR;~%")
 	   (call-next-method)
 	   (format stream "~&}"))
   (:method ((graph hash-table) stream)
     (loop for class being the hash-keys of graph using (hash-value superclass)
        do (format stream "~&~4t\"~a\" -> \"~a\"~%" class superclass))))
 
 (defparameter *selector-cache* (make-hash-table :test 'equal))
382ab041
 (defparameter *class-cache* (make-hash-table :test 'equal))
48df527e
 
49baa990
 (serapeum:eval-always
   (defun normalize-selector-name (sel-name)
     (substitute #\: #\? sel-name)))
 
382ab041
 (defun ensure-class (name)
   (let ((objc-class (objc-look-up-class name)))
     (when (and objc-class (not (null-pointer-p objc-class)))
eaa1c811
       (alexandria:ensure-gethash name *class-cache* objc-class))))
382ab041
 
48df527e
 (defun ensure-selector (name)
49baa990
   (alexandria:ensure-gethash name
                              *selector-cache*
48df527e
                              (sel-register-name name)))
 
 (defmacro with-objc-classes ((&rest class-defs) &body body)
   `(let (,@(mapcar (fw.lu:destructuring-lambda ((lisp-name foreign-name))
                      `(,lisp-name (objc-look-up-class ,foreign-name)))
                    class-defs))
      ,@body))
 
 
116081f3
 
 (cffi:defcvar (ns-app "NSApp" :library appkit) :pointer)
 
 (defclass objc-class ()
   ((%objc-class-name :initarg :name :reader name)
    (%class-pointer :initarg :pointer :reader class-pointer)
    (%cache :initform (make-hash-table :test 'equal) :allocation :class :reader objc-class-cache)))
 
 (defclass objc-selector ()
   ((%objc-selector-name :initarg :name :reader name)
    (%selector-pointer :initarg :pointer :reader selector-pointer)
    (%args :initarg :args :reader args)
    (%result-type :initarg :result-type :reader result-type)
    (%cache :initform (make-hash-table :test 'equal) :allocation :class :reader objc-selector-cache))
   (:metaclass closer-mop:funcallable-standard-class))
 
 (defun make-message-lambda-form (args rettype)
   (alexandria:with-gensyms ((target :target))
     (fw.lu:with (arg-syms (mapcar (serapeum:op _ (gensym "arg")) args))
       `(lambda (selector)
          (lambda (,target ,@arg-syms)
            (cffi:foreign-funcall
             "objc_msgSend"
             :pointer ,target
             :pointer selector
             ,@(mapcan #'list args arg-syms)
             ,rettype))))))
 
 (defmethod initialize-instance :after ((sel objc-selector) &key &allow-other-keys)
   (with-accessors ((pointer selector-pointer)
                    (args args)
                    (rettype result-type))
       sel
     (closer-mop:set-funcallable-instance-function
      sel
      (funcall (compile nil (make-message-lambda-form args rettype))
               pointer))))
 
 (defgeneric reset-class-cache (class)
   (:method ((class symbol))
     (reset-class-cache (find-class class)))
   (:method ((class class))
     (setf (slot-value (closer-mop:class-prototype class) '%cache)
           (make-hash-table :test 'equal))))
 
 
 (define-condition no-such-objc-class (serious-condition)
   ((%wanted-name :initarg :wanted-name :reader wanted-name))
   (:report (lambda (c s)
              (format s "No such Objective-C class: ~a" (wanted-name c)))))
 
 (defun %ensure-wrapped-objc-class (name)
   (let* ((class-cache (objc-class-cache (closer-mop:class-prototype (find-class 'objc-class))))
          (cached (gethash name class-cache)))
     (if cached
ae1d3bf8
         cached
116081f3
         (let ((objc-class (objc-look-up-class name)))
           (if (null-pointer-p objc-class)
               (error 'no-such-objc-class :wanted-name name)
               (setf (gethash name class-cache)
                     (make-instance 'objc-class
                                    :name name
                                    :pointer objc-class)))))))
 
382ab041
 ;; TODO: should this error if there is no corresponding selector?
 ;;         Or should we let that fall through to message sending?
116081f3
 (defun %ensure-wrapped-objc-selector (name target-class result-type args)
   (assert (= (count #\: name)
ae1d3bf8
             (length args))
116081f3
           (name args)
           "Invalid number of arg types for selector ~s" name)
 
   (let* ((class-cache (objc-selector-cache (closer-mop:class-prototype (find-class 'objc-selector))))
          (cached (gethash (list name target-class)
                           class-cache)))
     (if cached
ae1d3bf8
         cached
116081f3
         (let ((objc-selector (ensure-selector name)))
           (setf (gethash (list name target-class) class-cache)
                 (make-instance 'objc-selector
                                :name name
                                :pointer objc-selector
                                :result-type result-type
                                :args args))))))
 
48df527e
 (defgeneric make-objc-instance (class &rest args)
   (:method ((class string) &rest args)
     (apply #'make-objc-instance (objc-look-up-class class) args))
   #+ccl
   (:method ((class ccl:macptr) &rest args)
     (declare (ignore args))
     (with-selectors (alloc init)
       [[class alloc] init]))
   #+sbcl
   (:method ((class sb-sys:system-area-pointer) &rest args)
     (declare (ignore args))
     (with-selectors (alloc init)
       [[class alloc] init])))
 
116081f3
 (defun ensure-wrapped-objc-class (name)
   (tagbody
    retry (restart-case (return-from ensure-wrapped-objc-class
                          (%ensure-wrapped-objc-class name))
            (use-value (new)
              :interactive (lambda ()
                             (format t "New Objective-C class name: ")
                             (multiple-value-list (read)))
              :report "Retry with new class name"
              (setf name new)
              (go retry)))))
48df527e
 
116081f3
 (defmacro with-selectors ((&rest selector-specs) &body body)
   `(let (,@(mapcar (fw.lu:destructuring-lambda ((sym foreign-selector))
                      `(,sym (ensure-selector ,foreign-selector)))
122bb166
                    (mapcar (fwoar.anonymous-gf:glambda (spec)
116081f3
                              (:method ((spec symbol))
                                (list spec (normalize-selector-name
                                            (string-downcase spec))))
                              (:method ((spec cons))
                                (list (car spec) (cadr spec))))
                            selector-specs)))
      ,@body))
48df527e
 
 
116081f3
 (defmacro with-typed-selectors ((&rest defs) &body body)
   (let ((expanded-defs (loop for ((name objc-name) args ret-type) in defs
ae1d3bf8
                              collect
                              `((,name (&rest r) (apply ,name r))
                                (,name (%ensure-wrapped-objc-selector ,objc-name ',ret-type ',args))))))
116081f3
     `(let (,@(mapcar #'second expanded-defs))
        (flet (,@(mapcar #'first expanded-defs))
          ,@body))))
10e90099
 
 (defun description (nsobject)
   [nsobject @(description)]@)
 
 (defun future-ns-date (seconds)
   [[#@NSDate @(alloc)]
    @(initWithTimeIntervalSinceNow:)
    :double (coerce seconds 'double-float)])
 
 (defun tick-ns-runloop (run-loop &optional (time 0.5d0))
   (let ((date (future-ns-date time)))
     (unwind-protect [run-loop @(runUntilDate:)
                               :pointer date]
       [date @(release)])))