git.fiddlerwoaroof.com
README.org
4abbc169
 * Intro
ae1d3bf8
 
4abbc169
 CCL and LispWorks and other implementations have their own bridges to
 the objective-c runtime.  This project is an attempt to create a
 bridge that only uses CFFI so that arbitrary lisp implementations can
 produce native mac GUIs.  In the long run, I hope to use this as the
 basis for a new mac-native backend for McClim: but we'll see if that
 ever happens.
 
4b639d94
 For the time being, though, this only works on CCL and (sort-of) on
 LispWorks: it works like 95% on SBCL, but there's some weird issue
 that's preventing the window from showing. I hae not tested the code
 on any other implementations, but doing so will require changing a
 couple places in objc-runtime.lisp to inform the code about the new
 lisp's ffi types.
 
4abbc169
 * Installing
 
 1. clone fwoar.lisputils from
    https://github.com/fiddlerwoaroof/fwoar.lisputils and put it
    somewhere quicklisp can find it (e.g. ~/quicklisp/local-projects)
294c8e2c
 
ae1d3bf8
 2. clone cffi from https://github.com/cffi/cffi and put it in the same
    place (on Big Sur, at least, I need changes that haven't made it to
    Quicklisp)
 
 3. Install rsvg-convert:
aa2a5b88
     #+BEGIN_SRC sh :tangle no
ae1d3bf8
       brew install librsvg
abd994b1
     #+END_SRC
ae1d3bf8
 
 4. build + run the demo:
aa2a5b88
    #+BEGIN_SRC sh :tangle no
ae1d3bf8
      make mkapp CL=/path/to/cl
      open demo.app
4abbc169
    #+END_SRC
00bf024e
 
 * Show me the code!
ae1d3bf8
 
00bf024e
 From demo-app.lisp:
 
aa2a5b88
 #+BEGIN_SRC lisp :tangle no
00bf024e
   (defun main ()
     (trivial-main-thread:with-body-in-main-thread (:blocking t)
       [#@NSAutoReleasePool @(new)]
       [#@NSApplication @(sharedApplication)]
       [objc-runtime::ns-app @(setActivationPolicy:) :int 0]
 
b6be5a92
       (objc-runtime::objc-register-class-pair
        (demo-app::make-app-delegate-class '("actionButton"
                                   "alertButton"
                                   "profitButton")))
 
       (demo-app::load-nib "MainMenu")
ae1d3bf8
 
b6be5a92
       (let ((app-delegate [objc-runtime::ns-app @(delegate)]))
         (demo-app::make-button-delegate (value-for-key app-delegate "actionButton")
                               (cffi:callback do-things-action))
         (demo-app::make-button-delegate (value-for-key app-delegate "alertButton")
                               (cffi:callback alert-action))
         (demo-app::make-button-delegate (value-for-key app-delegate "profitButton")
                               (cffi:callback profit-action)))
ae1d3bf8
 
00bf024e
       [objc-runtime::ns-app @(activateIgnoringOtherApps:) :boolean t]
       [objc-runtime::ns-app @(run)]))
b6be5a92
 
00bf024e
 #+END_SRC
b3dc2dd1
 
 * In-depth example
 ** Type-directed Objective-C extractors
 
  #+name: extractor-framework
d5a8a26b
  #+begin_src lisp :tangle no :results no :comments both
b3dc2dd1
    (defvar *objc-extractors* (list)
      "Functions called to extract specific data types")
 
    (defun extract-from-objc (obj)
      (objc-typecase obj
        (#@NSDate [[[[#@NSISO8601DateFormatter @(alloc)]
                     @(init)]
                    @(stringFromDate:) :pointer obj]
                   @(UTF8String)]s)
        (#@NSString [obj @(UTF8String)]s)
        (#@NSNumber (parse-number:parse-number
                     (objc-runtime::extract-nsstring
                      [obj @(stringValue)])))
        (#@NSArray (map-nsarray #'extract-from-objc obj))
        (#@NSDictionary (fw.lu:alist-string-hash-table
                         (pairlis (map-nsarray #'extract-from-objc [obj @(allKeys)])
                                  (map-nsarray #'extract-from-objc [obj @(allValues)]))))
        (t (or (funcall-some (cdr (objc-pick-by-type obj *objc-extractors*))
                             obj)
               obj))))
 
    (defmacro define-extractor (class (o) &body body)
      `(serapeum:eval-always
         (add-extractor ,class
                        (lambda (,o)
                          ,@body))
         ,*objc-extractors*))
 
    (defun clear-extractors ()
      (setf *objc-extractors* ()))
 
    (serapeum:eval-always
      (defun add-extractor (class cb)
        (unless (member class *objc-extractors* :test 'cffi:pointer-eq :key #'car)
          (setf *objc-extractors*
                (merge 'list *objc-extractors* (list (cons class cb))
                       'objc-subclass-p
                       :key 'car)))
        ,*objc-extractors*))
  #+end_src
 
 ** Reading List to Org-file converter
 
06cb6baa
    The entry-point is fairly unremarkable: it delegates most of the work to other functions and disables the debugger so
    that this doesn't blow up when an error occurs in non-interactive mode.
 
    #+name: r-l-r-main
    #+begin_src lisp :tangle no :results no :noweb yes
      (defun main ()
        <<disable-sbcl-debugger>>
        (make-org-file *standard-output*
ae1d3bf8
                       (get-readinglist-info
                        (translate-plist
d5a8a26b
                         (get-bookmark-filename)))))
06cb6baa
    #+end_src
ae1d3bf8
 
    This pair of functions builds an org file from data extracted from the Safari bookmark file.
06cb6baa
 
    #+name: make-org-file
    #+begin_src lisp :tangle no :results no
d5a8a26b
      (defun make-org-file (s reading-list-info)
06cb6baa
        (format s "~&* Safari Reading List~%")
        (serapeum:mapply (serapeum:partial 'make-org-entry s)
                         reading-list-info))
 
d5a8a26b
      (defun make-org-entry (s date title url preview tag)
        (format s "~&** ~a (~a) :~{~a:~}~% ~a~2% ~{~<~% ~1,80:;~a~> ~}~2%"
                title
                (local-time:format-timestring nil date
                                              :format local-time:+rfc3339-format/date-only+)
                (alexandria:ensure-list tag)
06cb6baa
                url
                (serapeum:tokens preview)))
    #+end_src
ae1d3bf8
 
d5a8a26b
    Here we extract the data from Bookmarks.plist using our polymorphic objc data extractor framework
06cb6baa
 
    #+name: translate-plist
    #+begin_src lisp :tangle no :results no
      (defparameter *reading-list-location* "Library/Safari/Bookmarks.plist")
      (defun get-bookmark-filename ()
d5a8a26b
        (uiop:native-namestring
         (merge-pathnames *reading-list-location*
                          (truename "~/"))))
06cb6baa
 
      (defun translate-plist (fn)
        (objc-runtime.data-extractors:extract-from-objc
         (objc-runtime.data-extractors:get-plist fn)))
    #+end_src
 
    #+name: translate-data
    #+begin_src lisp :tangle no :results no
      (defun get-readinglist-info (bookmarks)
d5a8a26b
        (sort (mapcar 'extract-link-info
                      (gethash "Children"
                               (car
                                (select-child bookmarks
                                              "com.apple.ReadingList"))))
              'local-time:timestamp>
              :key 'car))
06cb6baa
 
      (defun extract-link-info (link)
d5a8a26b
        (list (local-time:parse-rfc3339-timestring (or (fw.lu:pick '("ReadingList" "DateAdded") link)
                                                       (fw.lu:pick '("ReadingList" "DateLastViewed") link)
                                                       (fw.lu:pick '("ReadingListNonSync" "DateLastFetched") link)
                                                       (local-time:now)))
              (fw.lu:pick '("URIDictionary" "title") link)
06cb6baa
              (fw.lu:pick '("URLString") link)
              (plump:decode-entities (coerce (fw.lu:pick '("ReadingList" "PreviewText") link) 'simple-string) t)
              (fw.lu:may (slugify (fw.lu:pick '("ReadingListNonSync" "siteName") link)))))
    #+end_src
b3dc2dd1
 
 ** Appendices
ae1d3bf8
 
b3dc2dd1
 *** objc-data-extractor.lisp
 
d5a8a26b
     #+begin_src lisp :tangle objc-data-extractors.lisp :noweb yes :comments both
06cb6baa
       (defpackage :objc-runtime.data-extractors
         (:use :cl )
         (:export
          #:extract-from-objc
          #:define-extractor
          #:clear-extractors
          #:add-extractor
          #:get-plist))
 
       (in-package :objc-runtime.data-extractors)
       (named-readtables:in-readtable :objc-readtable)
 
       (defun get-plist (file)
         [#@NSDictionary @(dictionaryWithContentsOfFile:)
                         :pointer (objc-runtime::make-nsstring file)])
 
       (defun objc-subclass-p (sub super)
         (unless (or (cffi:null-pointer-p sub)
                     (cffi:null-pointer-p super))
           (or (eql sub super)
               (= [sub @(isSubclassOfClass:) :pointer [super @(class)]]#
                  1))))
 
       (defun order-objc-classes (classes &rest r &key key)
         (declare (ignore key))
         (apply 'stable-sort
                (copy-seq classes)
                'objc-subclass-p
                r))
 
       (defun objc-isa (obj class)
         (unless (or (cffi:null-pointer-p obj)
                     (cffi:null-pointer-p class))
           (= [obj @(isKindOfClass:) :pointer class]#
              1)))
 
       (defun objc-pick-by-type (obj pairs)
         (assoc obj
                (order-objc-classes pairs :key 'car)
                :test 'objc-isa))
 
       (serapeum:eval-always
         (defun make-cases (cases obj)
           (mapcar (serapeum:op
                     `(if (objc-isa ,obj ,(car _1))
                          (progn ,@(cdr _1))))
                          cases)))
 
       (defmacro objc-typecase (form &body ((case-type &body case-handler) &rest cases))
         (alexandria:once-only (form)
           (let* ((initial-cases `((,case-type ,@case-handler) ,@(butlast cases)))
                  (cases (fw.lu:rollup-list (make-cases initial-cases form)
                                            (if (eql t (caar (last cases)))
                                                `((progn ,@(cdar (last cases))))
                                                (make-cases (last cases) form)))))
             cases)))
 
       (defun map-nsarray (fn arr)
         (unless (and (cffi:pointerp arr)
                      (objc-isa arr #@NSArray))
           (error "must provide a NSArray pointer"))
         (loop for x below [arr @(count)]#
            collect (funcall fn [arr @(objectAtIndex:) :int x])))
 
       (defun nsarray-contents (arr)
         (unless (and (cffi:pointerp arr)
                      (objc-isa arr #@NSArray))
           (error "must provide a NSArray pointer"))
         (dotimes (n [arr @(count)]#)
           (let ((obj [arr @(objectAtIndex:) :int n ]))
             (objc-typecase obj
               (#@NSString (format t "~&string~%"))
               (#@NSArray (format t "~&array~%"))
               (#@NSDictionary (format t "~&dictionary~%"))
               (t (format t "~&other... ~s~%" (objc-runtime::objc-class-get-name
                                               (objc-runtime::object-get-class obj))))))))
 
       (defmacro funcall-some (fun &rest args)
         (alexandria:once-only (fun)
           `(if ,fun
                (funcall ,fun ,@args))))
 
       <<extractor-framework>>
     #+end_src
b3dc2dd1
 
 *** build-reading-list-reader.sh
ae1d3bf8
 
06cb6baa
     #+begin_src sh :tangle build-reading-list-reader.sh
       #!/usr/bin/env bash
       set -eu -x -o pipefail
b3dc2dd1
 
06cb6baa
       cd "$(dirname $0)"
       mkdir -p dist
b3dc2dd1
 
06cb6baa
       pushd dist
       rm -rf fwoar.lisputils
       git clone https://github.com/fiddlerwoaroof/fwoar.lisputils.git
       popd
b3dc2dd1
 
06cb6baa
       export CL_SOURCE_REGISTRY="$PWD/dist//"
       sbcl --no-userinit \
            --load ~/quicklisp/setup.lisp \
            --load build.lisp
     #+end_src
b3dc2dd1
 
06cb6baa
 *** build.lisp
b3dc2dd1
 
06cb6baa
     #+begin_src lisp :mkdirp yes :results no :noweb yes :tangle build.lisp
       (eval-when (:compile-toplevel :load-toplevel :execute)
         (setf *default-pathname-defaults* (truename "~/git_repos/objc-lisp-bridge/"))
         (load (compile-file "objc-runtime.asd")))
b3dc2dd1
 
06cb6baa
       (eval-when (:compile-toplevel :load-toplevel :execute)
         (ql:quickload '(:objc-runtime :yason :plump :cl-ppcre)))
b3dc2dd1
 
06cb6baa
       (load "reading-list-reader.lisp")
b3dc2dd1
 
06cb6baa
       (eval-when (:compile-toplevel :load-toplevel :execute)
         (sb-ext:save-lisp-and-die "reading-list2org"
                                   :toplevel (intern "MAIN"
                                                     "READING-LIST-READER")
                                   :executable t))
     #+end_src
b3dc2dd1
 
06cb6baa
 *** reading-list-reader.lisp
b3dc2dd1
 
ae1d3bf8
     #+begin_src lisp :mkdirp yes :results no :noweb yes :tangle reading-list-reader.lisp
06cb6baa
       (defpackage :reading-list-reader
         (:use :cl )
         (:export ))
       (in-package :reading-list-reader)
 
       (serapeum:eval-always
         (named-readtables:in-readtable :objc-readtable))
 
d5a8a26b
       (defun slugify (s)
         (cl-ppcre:regex-replace-all "\\s+"
                                     (string-downcase s)
                                     "_"))
 
       (defun select-child (d title)
         (flet ((get-title (h)
                  (equal (gethash "Title" h)
                         title)))
           (fw.lu:let-each (:be *)
             (gethash "Children" d)
             (remove-if-not #'get-title *))))
 
       <<translate-plist>>
 
06cb6baa
       <<make-org-file>>
d5a8a26b
 
06cb6baa
       <<translate-data>>
d5a8a26b
 
       <<r-l-r-main>>
06cb6baa
     #+end_src
 
     #+name: disable-sbcl-debugger
     #+begin_src lisp :tangle no
       ,#+(and build sbcl)
       (progn (sb-ext:disable-debugger)
              (sb-alien:alien-funcall
               (sb-alien:extern-alien "disable_lossage_handler"
                                      (function sb-alien:void))))
     #+end_src
ae1d3bf8
 
06cb6baa
 
 # Local Variables:
 # fill-column: 120 :
 # End: