Wednesday, June 17, 2026

How To: Consume COM server in Common Lisp

How To: Consume COM server in Common Lisp


Goal: Simple example to consume COM server/object using Common Lisp
Version info:
- OS: Windows 11 23H2 (Microsoft Windows [Version 10.0.22631.7079])
- Emacs: 30.2
- SBCL : 2.6.4
- SLIME: 2.32

Pre-Requisites:
- Install CFFI

Save below say as shell-min.lisp
(defpackage #:shell-min
  (:use #:cl #:cffi)
  (:export #:demo
           #:create-shell
           #:get-current-directory))
(in-package #:shell-min)

;;; --------------------------------------------------------------------------
;;; DLLs
;;; --------------------------------------------------------------------------

(define-foreign-library ole32
  (t (:default "ole32")))

(define-foreign-library oleaut32
  (t (:default "oleaut32")))

(use-foreign-library ole32)
(use-foreign-library oleaut32)

;;; --------------------------------------------------------------------------
;;; Types
;;; --------------------------------------------------------------------------

(defctype hresult :long)
(defctype dword   :uint32)
(defctype word    :uint16)
(defctype uint    :uint32)
(defctype lcid    :uint32)
(defctype dispid  :long)
(defctype ulong   :uint32)

(defcstruct guid
  (data1 :uint32)
  (data2 :uint16)
  (data3 :uint16)
  (data4 (:array :uint8 8)))

(defcstruct dispparams
  (rgvarg :pointer)
  (rgdispidNamedArgs :pointer)
  (cArgs uint)
  (cNamedArgs uint))

;; Windows VARIANT = 16 bytes
(defcstruct variant
  (vt word)
  (w1 word)
  (w2 word)
  (w3 word)
  (data (:array :uint8 8)))

;;; --------------------------------------------------------------------------
;;; Constants
;;; --------------------------------------------------------------------------

(defparameter +coinit-apartmentthreaded+ #x2)
(defparameter +clsctx-server+ #x5)
(defparameter +dispatch-propertyget+ #x2)
(defparameter +vt-bstr+ 8)

(defparameter +iid-idispatch+
  "{00020400-0000-0000-C000-000000000046}")

;;; --------------------------------------------------------------------------
;;; COM APIs
;;; --------------------------------------------------------------------------

(defcfun ("CoInitializeEx" coinit) hresult
  (reserved :pointer)
  (coinit dword))

(defcfun ("CoUninitialize" couninit) :void)

(defcfun ("CLSIDFromProgID" clsid-from-progid) hresult
  (progid :pointer)
  (clsid :pointer))

(defcfun ("IIDFromString" iid-from-string) hresult
  (str :pointer)
  (iid :pointer))

(defcfun ("CoCreateInstance" cocreate) hresult
  (clsid :pointer)
  (outer :pointer)
  (ctx dword)
  (iid :pointer)
  (ppv :pointer))

(defcfun ("SysStringLen" syslen) :uint32
  (bstr :pointer))

;;; --------------------------------------------------------------------------
;;; Helpers
;;; --------------------------------------------------------------------------

(defun check (hr msg)
  (when (minusp hr)
    (error "~A failed: 0x~8,'0X" msg (ldb (byte 32 0) hr))))

(defun zero-memory (ptr size)
  (dotimes (i size)
    (setf (mem-aref ptr :uint8 i) 0))
  ptr)

(defun vt-fn (p index)
  (mem-aref (mem-ref p :pointer) :pointer index))

(defun release (p)
  (foreign-funcall-pointer (vt-fn p 2)
                           (:convention :stdcall)
                           :pointer p
                           ulong))

(defun create-shell ()
  "Create WScript.Shell and return IDispatch*."
  (with-foreign-object (clsid '(:struct guid))
    (with-foreign-object (iid '(:struct guid))
      (with-foreign-string (progid "WScript.Shell" :encoding :utf-16le)
        (check (clsid-from-progid progid clsid)
               "CLSIDFromProgID"))
      (with-foreign-string (iid-str +iid-idispatch+ :encoding :utf-16le)
        (check (iid-from-string iid-str iid)
               "IIDFromString(IID_IDispatch)"))
      (with-foreign-object (ppv :pointer)
        (setf (mem-ref ppv :pointer) (null-pointer))
        (check (cocreate clsid
                         (null-pointer)
                         +clsctx-server+
                         iid
                         ppv)
               "CoCreateInstance")
        (mem-ref ppv :pointer)))))

(defun get-dispid (obj name)
  "Resolve a member name to DISPID via IDispatch::GetIDsOfNames."
  (let ((fn (vt-fn obj 5))) ; IDispatch::GetIDsOfNames
    (with-foreign-object (iid-null '(:struct guid))
      (with-foreign-object (names :pointer 1)
        (with-foreign-object (id 'dispid)
          ;; riid must be IID_NULL (all zeros)
          (zero-memory iid-null (foreign-type-size '(:struct guid)))
          (with-foreign-string (n name :encoding :utf-16le)
            (setf (mem-aref names :pointer 0) n)
            (check
             (foreign-funcall-pointer fn
                                      (:convention :stdcall)
                                      :pointer obj
                                      :pointer iid-null
                                      :pointer names
                                      uint 1
                                      lcid 0
                                      :pointer id
                                      hresult)
             (format nil "GetIDsOfNames(~A)" name))
            (mem-ref id 'dispid)))))))

(defun bstr->string (bstr)
  "Convert BSTR to Lisp string."
  ;; SysStringLen returns UTF-16 code units, not bytes.
  (let ((chars (syslen bstr)))
    (foreign-string-to-lisp bstr
                            :count (* chars 2)
                            :encoding :utf-16le)))

(defun get-current-directory (obj)
  "Call WScript.Shell.CurrentDirectory via IDispatch::Invoke."
  (let ((fn (vt-fn obj 6)) ; IDispatch::Invoke
        (id (get-dispid obj "CurrentDirectory")))
    (with-foreign-object (iid-null '(:struct guid))
      (with-foreign-object (dp '(:struct dispparams))
        (with-foreign-object (res '(:struct variant))
          ;; riid must be IID_NULL
          (zero-memory iid-null (foreign-type-size '(:struct guid)))
          (zero-memory dp (foreign-type-size '(:struct dispparams)))
          (zero-memory res (foreign-type-size '(:struct variant)))

          ;; no arguments for property get
          (setf (foreign-slot-value dp '(:struct dispparams) 'rgvarg)
                (null-pointer))
          (setf (foreign-slot-value dp '(:struct dispparams) 'rgdispidNamedArgs)
                (null-pointer))
          (setf (foreign-slot-value dp '(:struct dispparams) 'cArgs) 0)
          (setf (foreign-slot-value dp '(:struct dispparams) 'cNamedArgs) 0)

          (check
           (foreign-funcall-pointer fn
                                    (:convention :stdcall)
                                    :pointer obj
                                    dispid id
                                    :pointer iid-null
                                    lcid 0
                                    word +dispatch-propertyget+
                                    :pointer dp
                                    :pointer res
                                    :pointer (null-pointer)
                                    :pointer (null-pointer)
                                    hresult)
           "Invoke(CurrentDirectory)")

          ;; VT_BSTR expected
          (unless (= (foreign-slot-value res '(:struct variant) 'vt) +vt-bstr+)
            (error "CurrentDirectory returned unexpected VT=~A"
                   (foreign-slot-value res '(:struct variant) 'vt)))

          (let ((bstr (mem-ref (inc-pointer res 8) :pointer)))
            (bstr->string bstr)))))))

(defun demo ()
  "Create WScript.Shell, read CurrentDirectory, print and return it."
  (check (coinit (null-pointer) +coinit-apartmentthreaded+)
         "CoInitializeEx")
  (let ((obj (null-pointer)))
    (unwind-protect
         (progn
           (setf obj (create-shell))
           (let ((dir (get-current-directory obj)))
             (format t "CurrentDirectory => ~A~%" dir)
             dir))
      (unless (null-pointer-p obj)
        (ignore-errors
          (release obj)))
      (ignore-errors
        (couninit)))))

You can run it like below
; SLIME 2.32
CL-USER> (load "~/quicklisp/setup.lisp")
T
CL-USER> (ql:quickload :cffi)
To load "cffi":
  Load 1 ASDF system:
    cffi
; Loading "cffi"
..............
(:CFFI)
CL-USER> (load "c:/lang/sbclwork/shell-min.lisp")
T
CL-USER> (shell-min:demo)
CurrentDirectory => c:\lang\emacs-30.2\bin
"c:\\lang\\emacs-30.2\\bin"
CL-USER> 

Sunday, June 14, 2026

Installing CFFI on SBCL on Windows 11

Installing CFFI on SBCL on Windows 11

Version info:
- OS: Windows 11 23H2 (Microsoft Windows [Version 10.0.22631.7079])
- Emacs: 30.2
- SBCL : 2.6.4
- SLIME: 2.32

Install Quicklisp library manager
cd C:\lang\sbclwork
curl -O https://beta.quicklisp.org/quicklisp.lisp
This will download quicklisp.lisp into C:\lang\sbclwork
Load Quicklisp into SBCL
sbcl --load quicklisp.lisp

Install it (only once) into SBCL system
* (quicklisp-quickstart:install)

Install CFFI
* (ql:quickload :cffi)

Verify CFFI is loaded and working
(ql:quickload :cffi)
(cffi:defcfun ("GetTickCount" get-tick-count) :uint32)
(get-tick-count)

This was all done from the terminal. Below shows how it looks like in SLIME
; SLIME 2.32
CL-USER> (load "~/quicklisp/setup.lisp")
T
CL-USER> (ql:quickload :cffi)
To load "cffi":
  Load 1 ASDF system:
    cffi
; Loading "cffi"
..............
(:CFFI)
CL-USER> (ql:quickload :cffi)
To load "cffi":
  Load 1 ASDF system:
    cffi
; Loading "cffi"

(:CFFI)
CL-USER> (cffi:defcfun ("GetTickCount" get-tick-count) :uint32)
GET-TICK-COUNT
CL-USER> (get-tick-count)
1556937609
CL-USER> 

Wednesday, June 03, 2026

SBCL Hello world

SBCL Hello, world!

Version info:
- OS: Windows 11 23H2 (Microsoft Windows [Version 10.0.22631.7079])
- Emacs: 30.2
- SBCL : 2.6.4
- SLIME: 2.32

Create a file called hello.lisp and update to look like below -
(write-line "Hello world!")
Below shows where SBCL is located.
c:\lang\emacs-30.2\bin>where sbcl

C:\Program Files\Steel Bank Common Lisp\sbcl.exe

Run the script like, so:
c:\lang\emacs-30.2\bin>sbcl --script c:\lang\sbclwork\hello.lisp
Hello, world!

Emacs with SLIME for interactive development with SBCL on Windows 11

Emacs with SLIME for interactive development with SBCL

Version info:
- OS: Windows 11 23H2 (Microsoft Windows [Version 10.0.22631.7079])
- Emacs: 30.2
- SBCL : 2.6.4
- SLIME: 2.32

Prep my work directories
C:\>md c:\emacs-profiles
C:\>md c:\emacs-profiles\work
C:\>copy con c:\emacs-profiles\work\init.el
^Z
        1 file(s) copied.

Run Emacs on my work directory, I have my bin files in c:\lang\emacs-30.2\bin
c:\>cd c:\lang\emacs-30.2\bin
c:\lang\emacs-30.2\bin>runemacs.exe --init-directory c:\emacs-profiles\work

Install SLIME
M-x package-refresh-contents
M-x package-install RET slime RET

Update init.el to use SBCL
Restart Emacs and load SLIME
M-x slime RET

Thursday, May 28, 2026

Using Emacs as editor for SBCL on Ubuntu 26.04

Using Emacs as editor for SBCL on Ubuntu 26.04

Version info:
  • OS: Ubuntu 26.04
  • Emacs: 30.2
  • SBCL: 2.6.0
Install Emacs and SBCL as needed:
$: sudo apt install emacs
$: sudo apt install sbcl


Configure Emacs with SLY (Sylvester the Cat's Common Lisp IDE for Emacs), fire up Emacs and do:
  1. Install Sly package in Emacs
  2. M-x package-refresh-contents
    M-x package-install RET sly RET
    
  3. Create or edit Emacs configuration file
  4. M-: (find-file user-init-file)
    
    My .emacs looks like below
  5. Restart Emacs
  6. Load SLY
  7. M-x sly RET
    


Below shows SLY with SBCL loaded.

Using Emacs as editor for SBCL on Windows 10

Using Emacs as editor for SBCL on Windows 10

Emacs is installed in C:\emacs, using version 30.2. I run it using C:\emacs\bin\runemacs.exe.

SBCL version 2.6.4 (sbcl-2.6.4-x86-64-windows-binary.msi) is installed in C:\Program Files\Steel Bank Common Lisp
  1. Install Sly package in Emacs
  2. M-x package-refresh-contents
    M-x package-install RET sly RET
    
  3. Create or edit ~/.emacs.d/init.el
  4. Edit or replace contents like below
  5. Restart Emacs
  6. Load Sly
  7. M-x sly RET
    
Note: Verify that you are not using ~/.emacs, otherwise it will not load configration from ~/.emacs.d/init.el

Below shows SLY with SBCL loaded.

I am having issues running code below on Windows 10, it seems the input is buffered. Pressing RET does not properly go to the next prompt.
(defun prompt-for-cd ()
  (make-cd
   (prompt-read "Title")
   (prompt-read "Artist")
   (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
   (y-or-n-p "Ripped [y/n]: ")))
There is a workaround for the above bug, see below:
https://github.com/joaotavora/sly/commit/5a05c033197693462e67004549c24e0676eed53b

Wednesday, May 27, 2026

Emacs common commands

Character Operations:
DELETE : Delete character to the left of point (Also marked ROBOUT)
C-d    : Delete character to the right of (or under) point


Word Operations:
M-d    : Delete one word to the right (C-y yanks it back at point)
Cursor movement:
C-p    : Move to the previous line
C-n    : Move to the next line
C-f    : Move forward one char
C-b    : Move backward one char
M-f    : Move forward one word
M-b    : Move backward one word
C-a    : Move cursor beggining of the line
C-e    : Move cursor end of the line
M->    : End of file
M-<    : Beginning of file



Line operations:
M-m       : Move point to the first non-space in the line

Copying Text:
M-w     : Runs command kill-ring-save. This is like copy in Windows editors



Deleting Text:
C-SPC select text C-w Delete selected text
M-d Kill the next word after the cursor


Searching for text:
C-s           : Incremental search forward
C-r           : Incremental search backward


Misc commands
M-: user-init-file Show the configuration file used by Emacs in the echo area
M-: (find-file user-init-file) Edit user configuratin file

Convenience functions:
C-[number][char]
        : Repeat [char] [number] of times. Example C-8-0 1, repeat 1 80 times.

Dictionary:
M-$     : Check and correct spelling for the word at point

Others:
count-lines-region      
        : Count number line and chars for the selected text
untabify
        : Change tabs to space for the currently selected region



Python mode specific shortcuts:
M-/     : Command abbreviation expand
C-M-i   : Complete partial symbol or Intellisense in VS speak 


Links:
Emacs Keyboard keys (link)

How To: Consume COM server in Common Lisp

How To: Consume COM server in Common Lisp Goal: Simple example to consume COM server/object using Common Lisp Version info: - OS: Win...