{"id":13803915,"url":"https://github.com/fiddlerwoaroof/objc-lisp-bridge","last_synced_at":"2025-07-14T05:38:16.183Z","repository":{"id":44963831,"uuid":"116481608","full_name":"fiddlerwoaroof/objc-lisp-bridge","owner":"fiddlerwoaroof","description":"A portable reader and bridge for interacting with Objective-C and Cocoa","archived":false,"fork":false,"pushed_at":"2023-05-23T06:02:56.000Z","size":115,"stargazers_count":45,"open_issues_count":1,"forks_count":3,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-07-14T05:17:02.387Z","etag":null,"topics":["cocoa","gui","lisp","objective-c"],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fiddlerwoaroof.png","metadata":{"files":{"readme":"README.org","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-01-06T12:36:16.000Z","updated_at":"2025-07-10T11:46:29.000Z","dependencies_parsed_at":"2024-01-08T22:11:57.811Z","dependency_job_id":null,"html_url":"https://github.com/fiddlerwoaroof/objc-lisp-bridge","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fiddlerwoaroof/objc-lisp-bridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fiddlerwoaroof%2Fobjc-lisp-bridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fiddlerwoaroof%2Fobjc-lisp-bridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fiddlerwoaroof%2Fobjc-lisp-bridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fiddlerwoaroof%2Fobjc-lisp-bridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fiddlerwoaroof","download_url":"https://codeload.github.com/fiddlerwoaroof/objc-lisp-bridge/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fiddlerwoaroof%2Fobjc-lisp-bridge/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265246229,"owners_count":23734111,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cocoa","gui","lisp","objective-c"],"created_at":"2024-08-04T01:00:39.091Z","updated_at":"2025-07-14T05:38:15.868Z","avatar_url":"https://github.com/fiddlerwoaroof.png","language":"Common Lisp","funding_links":[],"categories":["Objective-C ##"],"sub_categories":[],"readme":"* Intro\n\nCCL and LispWorks and other implementations have their own bridges to\nthe objective-c runtime.  This project is an attempt to create a\nbridge that only uses CFFI so that arbitrary lisp implementations can\nproduce native mac GUIs.  In the long run, I hope to use this as the\nbasis for a new mac-native backend for McClim: but we'll see if that\never happens.\n\nFor the time being, though, this only works on CCL and (sort-of) on\nLispWorks: it works like 95% on SBCL, but there's some weird issue\nthat's preventing the window from showing. I hae not tested the code\non any other implementations, but doing so will require changing a\ncouple places in objc-runtime.lisp to inform the code about the new\nlisp's ffi types.\n\n* Installing\n\n1. clone fwoar.lisputils from\n   https://github.com/fiddlerwoaroof/fwoar.lisputils and put it\n   somewhere quicklisp can find it (e.g. ~/quicklisp/local-projects)\n\n2. clone cffi from https://github.com/cffi/cffi and put it in the same\n   place (on Big Sur, at least, I need changes that haven't made it to\n   Quicklisp)\n\n3. Install rsvg-convert:\n    #+BEGIN_SRC sh :tangle no\n      brew install librsvg\n    #+END_SRC\n\n4. build + run the demo:\n   #+BEGIN_SRC sh :tangle no\n     make mkapp CL=/path/to/cl\n     open demo.app\n   #+END_SRC\n\n* Show me the code!\n\nFrom demo-app.lisp:\n\n#+BEGIN_SRC lisp :tangle no\n  (defun main ()\n    (trivial-main-thread:with-body-in-main-thread (:blocking t)\n      [#@NSAutoReleasePool @(new)]\n      [#@NSApplication @(sharedApplication)]\n      [objc-runtime::ns-app @(setActivationPolicy:) :int 0]\n\n      (objc-runtime::objc-register-class-pair\n       (demo-app::make-app-delegate-class '(\"actionButton\"\n                                  \"alertButton\"\n                                  \"profitButton\")))\n\n      (demo-app::load-nib \"MainMenu\")\n\n      (let ((app-delegate [objc-runtime::ns-app @(delegate)]))\n        (demo-app::make-button-delegate (value-for-key app-delegate \"actionButton\")\n                              (cffi:callback do-things-action))\n        (demo-app::make-button-delegate (value-for-key app-delegate \"alertButton\")\n                              (cffi:callback alert-action))\n        (demo-app::make-button-delegate (value-for-key app-delegate \"profitButton\")\n                              (cffi:callback profit-action)))\n\n      [objc-runtime::ns-app @(activateIgnoringOtherApps:) :boolean t]\n      [objc-runtime::ns-app @(run)]))\n\n#+END_SRC\n\n* In-depth example\n** Type-directed Objective-C extractors\n\n #+name: extractor-framework\n #+begin_src lisp :tangle no :results no :comments both\n   (defvar *objc-extractors* (list)\n     \"Functions called to extract specific data types\")\n\n   (defun extract-from-objc (obj)\n     (objc-typecase obj\n       (#@NSDate [[[[#@NSISO8601DateFormatter @(alloc)]\n                    @(init)]\n                   @(stringFromDate:) :pointer obj]\n                  @(UTF8String)]s)\n       (#@NSString [obj @(UTF8String)]s)\n       (#@NSNumber (parse-number:parse-number\n                    (objc-runtime::extract-nsstring\n                     [obj @(stringValue)])))\n       (#@NSArray (map-nsarray #'extract-from-objc obj))\n       (#@NSDictionary (fw.lu:alist-string-hash-table\n                        (pairlis (map-nsarray #'extract-from-objc [obj @(allKeys)])\n                                 (map-nsarray #'extract-from-objc [obj @(allValues)]))))\n       (t (or (funcall-some (cdr (objc-pick-by-type obj *objc-extractors*))\n                            obj)\n              obj))))\n\n   (defmacro define-extractor (class (o) \u0026body body)\n     `(serapeum:eval-always\n        (add-extractor ,class\n                       (lambda (,o)\n                         ,@body))\n        ,*objc-extractors*))\n\n   (defun clear-extractors ()\n     (setf *objc-extractors* ()))\n\n   (serapeum:eval-always\n     (defun add-extractor (class cb)\n       (unless (member class *objc-extractors* :test 'cffi:pointer-eq :key #'car)\n         (setf *objc-extractors*\n               (merge 'list *objc-extractors* (list (cons class cb))\n                      'objc-subclass-p\n                      :key 'car)))\n       ,*objc-extractors*))\n #+end_src\n\n** Reading List to Org-file converter\n\n   The entry-point is fairly unremarkable: it delegates most of the work to other functions and disables the debugger so\n   that this doesn't blow up when an error occurs in non-interactive mode.\n\n   #+name: r-l-r-main\n   #+begin_src lisp :tangle no :results no :noweb yes\n     (defun main ()\n       \u003c\u003cdisable-sbcl-debugger\u003e\u003e\n       (make-org-file *standard-output*\n                      (get-readinglist-info\n                       (translate-plist\n                        (get-bookmark-filename)))))\n   #+end_src\n\n   This pair of functions builds an org file from data extracted from the Safari bookmark file.\n\n   #+name: make-org-file\n   #+begin_src lisp :tangle no :results no\n     (defun make-org-file (s reading-list-info)\n       (format s \"~\u0026* Safari Reading List~%\")\n       (serapeum:mapply (serapeum:partial 'make-org-entry s)\n                        reading-list-info))\n\n     (defun make-org-entry (s date title url preview tag)\n       (format s \"~\u0026** ~a (~a) :~{~a:~}~% ~a~2% ~{~\u003c~% ~1,80:;~a~\u003e ~}~2%\"\n               title\n               (local-time:format-timestring nil date\n                                             :format local-time:+rfc3339-format/date-only+)\n               (alexandria:ensure-list tag)\n               url\n               (serapeum:tokens preview)))\n   #+end_src\n\n   Here we extract the data from Bookmarks.plist using our polymorphic objc data extractor framework\n\n   #+name: translate-plist\n   #+begin_src lisp :tangle no :results no\n     (defparameter *reading-list-location* \"Library/Safari/Bookmarks.plist\")\n     (defun get-bookmark-filename ()\n       (uiop:native-namestring\n        (merge-pathnames *reading-list-location*\n                         (truename \"~/\"))))\n\n     (defun translate-plist (fn)\n       (objc-runtime.data-extractors:extract-from-objc\n        (objc-runtime.data-extractors:get-plist fn)))\n   #+end_src\n\n   #+name: translate-data\n   #+begin_src lisp :tangle no :results no\n     (defun get-readinglist-info (bookmarks)\n       (sort (mapcar 'extract-link-info\n                     (gethash \"Children\"\n                              (car\n                               (select-child bookmarks\n                                             \"com.apple.ReadingList\"))))\n             'local-time:timestamp\u003e\n             :key 'car))\n\n     (defun extract-link-info (link)\n       (list (local-time:parse-rfc3339-timestring (or (fw.lu:pick '(\"ReadingList\" \"DateAdded\") link)\n                                                      (fw.lu:pick '(\"ReadingList\" \"DateLastViewed\") link)\n                                                      (fw.lu:pick '(\"ReadingListNonSync\" \"DateLastFetched\") link)\n                                                      (local-time:now)))\n             (fw.lu:pick '(\"URIDictionary\" \"title\") link)\n             (fw.lu:pick '(\"URLString\") link)\n             (plump:decode-entities (coerce (fw.lu:pick '(\"ReadingList\" \"PreviewText\") link) 'simple-string) t)\n             (fw.lu:may (slugify (fw.lu:pick '(\"ReadingListNonSync\" \"siteName\") link)))))\n   #+end_src\n\n** Appendices\n\n*** objc-data-extractor.lisp\n\n    #+begin_src lisp :tangle objc-data-extractors.lisp :noweb yes :comments both\n      (defpackage :objc-runtime.data-extractors\n        (:use :cl )\n        (:export\n         #:extract-from-objc\n         #:define-extractor\n         #:clear-extractors\n         #:add-extractor\n         #:get-plist))\n\n      (in-package :objc-runtime.data-extractors)\n      (named-readtables:in-readtable :objc-readtable)\n\n      (defun get-plist (file)\n        [#@NSDictionary @(dictionaryWithContentsOfFile:)\n                        :pointer (objc-runtime::make-nsstring file)])\n\n      (defun objc-subclass-p (sub super)\n        (unless (or (cffi:null-pointer-p sub)\n                    (cffi:null-pointer-p super))\n          (or (eql sub super)\n              (= [sub @(isSubclassOfClass:) :pointer [super @(class)]]#\n                 1))))\n\n      (defun order-objc-classes (classes \u0026rest r \u0026key key)\n        (declare (ignore key))\n        (apply 'stable-sort\n               (copy-seq classes)\n               'objc-subclass-p\n               r))\n\n      (defun objc-isa (obj class)\n        (unless (or (cffi:null-pointer-p obj)\n                    (cffi:null-pointer-p class))\n          (= [obj @(isKindOfClass:) :pointer class]#\n             1)))\n\n      (defun objc-pick-by-type (obj pairs)\n        (assoc obj\n               (order-objc-classes pairs :key 'car)\n               :test 'objc-isa))\n\n      (serapeum:eval-always\n        (defun make-cases (cases obj)\n          (mapcar (serapeum:op\n                    `(if (objc-isa ,obj ,(car _1))\n                         (progn ,@(cdr _1))))\n                         cases)))\n\n      (defmacro objc-typecase (form \u0026body ((case-type \u0026body case-handler) \u0026rest cases))\n        (alexandria:once-only (form)\n          (let* ((initial-cases `((,case-type ,@case-handler) ,@(butlast cases)))\n                 (cases (fw.lu:rollup-list (make-cases initial-cases form)\n                                           (if (eql t (caar (last cases)))\n                                               `((progn ,@(cdar (last cases))))\n                                               (make-cases (last cases) form)))))\n            cases)))\n\n      (defun map-nsarray (fn arr)\n        (unless (and (cffi:pointerp arr)\n                     (objc-isa arr #@NSArray))\n          (error \"must provide a NSArray pointer\"))\n        (loop for x below [arr @(count)]#\n           collect (funcall fn [arr @(objectAtIndex:) :int x])))\n\n      (defun nsarray-contents (arr)\n        (unless (and (cffi:pointerp arr)\n                     (objc-isa arr #@NSArray))\n          (error \"must provide a NSArray pointer\"))\n        (dotimes (n [arr @(count)]#)\n          (let ((obj [arr @(objectAtIndex:) :int n ]))\n            (objc-typecase obj\n              (#@NSString (format t \"~\u0026string~%\"))\n              (#@NSArray (format t \"~\u0026array~%\"))\n              (#@NSDictionary (format t \"~\u0026dictionary~%\"))\n              (t (format t \"~\u0026other... ~s~%\" (objc-runtime::objc-class-get-name\n                                              (objc-runtime::object-get-class obj))))))))\n\n      (defmacro funcall-some (fun \u0026rest args)\n        (alexandria:once-only (fun)\n          `(if ,fun\n               (funcall ,fun ,@args))))\n\n      \u003c\u003cextractor-framework\u003e\u003e\n    #+end_src\n\n*** build-reading-list-reader.sh\n\n    #+begin_src sh :tangle build-reading-list-reader.sh\n      #!/usr/bin/env bash\n      set -eu -x -o pipefail\n\n      cd \"$(dirname $0)\"\n      mkdir -p dist\n\n      pushd dist\n      rm -rf fwoar.lisputils\n      git clone https://github.com/fiddlerwoaroof/fwoar.lisputils.git\n      popd\n\n      export CL_SOURCE_REGISTRY=\"$PWD/dist//\"\n      sbcl --no-userinit \\\n           --load ~/quicklisp/setup.lisp \\\n           --load build.lisp\n    #+end_src\n\n*** build.lisp\n\n    #+begin_src lisp :mkdirp yes :results no :noweb yes :tangle build.lisp\n      (eval-when (:compile-toplevel :load-toplevel :execute)\n        (setf *default-pathname-defaults* (truename \"~/git_repos/objc-lisp-bridge/\"))\n        (load (compile-file \"objc-runtime.asd\")))\n\n      (eval-when (:compile-toplevel :load-toplevel :execute)\n        (ql:quickload '(:objc-runtime :yason :plump :cl-ppcre)))\n\n      (load \"reading-list-reader.lisp\")\n\n      (eval-when (:compile-toplevel :load-toplevel :execute)\n        (sb-ext:save-lisp-and-die \"reading-list2org\"\n                                  :toplevel (intern \"MAIN\"\n                                                    \"READING-LIST-READER\")\n                                  :executable t))\n    #+end_src\n\n*** reading-list-reader.lisp\n\n    #+begin_src lisp :mkdirp yes :results no :noweb yes :tangle reading-list-reader.lisp\n      (defpackage :reading-list-reader\n        (:use :cl )\n        (:export ))\n      (in-package :reading-list-reader)\n\n      (serapeum:eval-always\n        (named-readtables:in-readtable :objc-readtable))\n\n      (defun slugify (s)\n        (cl-ppcre:regex-replace-all \"\\\\s+\"\n                                    (string-downcase s)\n                                    \"_\"))\n\n      (defun select-child (d title)\n        (flet ((get-title (h)\n                 (equal (gethash \"Title\" h)\n                        title)))\n          (fw.lu:let-each (:be *)\n            (gethash \"Children\" d)\n            (remove-if-not #'get-title *))))\n\n      \u003c\u003ctranslate-plist\u003e\u003e\n\n      \u003c\u003cmake-org-file\u003e\u003e\n\n      \u003c\u003ctranslate-data\u003e\u003e\n\n      \u003c\u003cr-l-r-main\u003e\u003e\n    #+end_src\n\n    #+name: disable-sbcl-debugger\n    #+begin_src lisp :tangle no\n      ,#+(and build sbcl)\n      (progn (sb-ext:disable-debugger)\n             (sb-alien:alien-funcall\n              (sb-alien:extern-alien \"disable_lossage_handler\"\n                                     (function sb-alien:void))))\n    #+end_src\n\n\n# Local Variables:\n# fill-column: 120 :\n# End:\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffiddlerwoaroof%2Fobjc-lisp-bridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffiddlerwoaroof%2Fobjc-lisp-bridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffiddlerwoaroof%2Fobjc-lisp-bridge/lists"}