SBCL sb-alien对.dll文件的调用

想通过ole32.dll调用cad的com组件。

这是python的做法

ole32 = ctypes.windll.ole32
# 定义 CLSIDFromString 函数原型
CLSIDFromString = ole32.CLSIDFromString
CLSIDFromString.argtypes = [ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_ulong)] 
# 定义 progid
progid = "ZWCAD.Application"

# 初始化一个 ULONG 类型的变量
clsid = ctypes.c_ulong()

# 调用 CLSIDFromString 函数
result = CLSIDFromString(progid, ctypes.byref(clsid))

然后这是sb-alien的

;;; 加载外部函数库
(sb-alien:load-shared-object "ole32.dll")

;;; 定义guid结构
(define-alien-type GUID
  (struct guid  (data1 (sb-alien:unsigned 32))
                (data2 (sb-alien:unsigned 16))
                (data3 (sb-alien:unsigned 16))
                (data4 (array (sb-alien:unsigned 8) 8))))

;; 函数 CLSIDFromString
(sb-alien:define-alien-routine("CLSIDFromString" CLSIDFromString)
  sb-alien:int
  (LPCOLESTR  (c-string))
  (LPCLSID    (* GUID))
)

(defvar lpsz (make-alien (sb-alien:c-string)))
(setf lpsz "ZWCAD.Application")

;; 传参
(defvar pclsid (make-alien (sb-alien:struct guid)))

(defvar *result* (make-alien sb-alien:int))

;;调用函数
(setf (sb-alien:deref *result*) (CLSIDFromString lpsz pclsid))
(format t "CLSIDFromProgID result: ~A~%" (sb-alien:deref *result*))

结果python能正常获取CLSID,sbcl失败了

CLSIDFromProgID result: -2147221005

函数的调用结果是progid未注册(应该),直接抓瞎。

怀疑是string在传参的过程中被修改了,未能传递正确字符。但不知道从何入手了。

以下是用cffi调用的代码,发生了同样的错误。

(ql:quickload :cffi)
(cffi:load-foreign-library "ole32.dll")

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

(cffi:defcfun ("CLSIDFromString" clsid-from-string) :int
  (str :string)
  (clsid :pointer (:struct guid)))

(cffi:with-foreign-object (clsid '(:struct guid))
    (let ((result (clsid-from-string "Shell.Application" clsid)))
      (print result)))

CLSIDFromString 的第一个参数,[in] LPCOLESTR lpsz 的类型,可能不是 c-string,或者说不是 ascii 编码的 char,而是 wchar_t。来源:https://stackoverflow.com/a/1607840/18964608

至于 c-string,manual 里说:c-string: an array of char in the structure.

有可能是因为这个

(defvar lpsz (make-alien (sb-alien:c-string)))
(print lpsz)
(setf lpsz "Shell.Application")
(print lpsz)

赋值之后修改了类型

#<SB-ALIEN-INTERNALS:ALIEN-VALUE :SAP #X00E41980 :TYPE (* SB-ALIEN:C-STRING)>
"Shell.Application" 

还在排查。

找到解决办法了。 字符转换宏

(defmacro with-ole-str ((var str) &body body)
  "wchar_t string which ends of null"
  (let ((len (gensym)))
    `(let* ((,len (length ,str)))
       (cffi:with-foreign-object (,var :unsigned-short (1+ ,len))
         (loop for i from 0 below ,len
            do (setf (cffi:mem-aref ,var :unsigned-short i)
                     (char-code (char ,str i)))
            finally (setf (cffi:mem-aref ,var :unsigned-short ,len) 0))
         ,@body))))
(cffi:with-foreign-objects ((clsid 'guid))
  (with-ole-str (s "Shell.Application")
    (print (cffi:foreign-funcall "CLSIDFromString" :pointer  s :pointer clsid :int))))

:money_mouth_face: 昨天自己写了一个,创建的是8位数组,结果是要创建16为数组。哭死。

最好不要使用 char-code,它返回的是 32 位数字,可能会整数溢出,比如:

(char-code #\😳)
=> 128563

使用函数 sb-ext:string-to-octet 可以将字符串转换成其他编码的 8 bit vector,参见:SBCL 2.4.5 User Manual

举例,将字符串 "hello world" 转换为 utf-16 小端序的 8 bit byte vector:

(sb-ext:string-to-octet "Hello world" :external-format '(:utf-16be))

不过 sbcl 有直接把字符串转换为外部字符串的方式,使用函数 sb-ext:make-alien-string,参见:SBCL 2.4.5 User Manual 注意,用这个函数创建的字符串要用 free-alien 来释放,就像 C 里用 malloc 分配的内存一样。

举例:创建 Windows 能识别的 utf-16 字符串:

(setq str (sb-alien:make-alien-string "Hello world" :external-format '(utf-16be) :null-terminate t))
;; do something
(sb-alien:free-alien str)

这里给出一个让使用变得简单,省得还要 free-alien 的宏:

(defmacro with-external-format-string ((var string format) &body body)
  `(let ((,var (sb-alien:make-alien-string
                 ,string
                 :external-format ,format
                 :null-terminate t)))
     (unwind-protect
       (progn . ,body)
       (sb-alien:free-alien ,var))))

用法:

(with-external-format-string (foo "Hello world" '(:utf-16le))
  (call-with-string foo)) ; 假设 `call-with-string' 是一个需要 utf-16 的字符串

是chatgpt吗?

我手写的啊(

:sweat: 没跑起来,排错之后····

sb-ext:string-to-octet函数名应该是sb-ext:string-to-octets

make-alien-string 中,应该传入:utf-16be

结果竟然得到的是一个8位数组,结果dll接口要的是16为数组

所以有种gpt的感觉。:sob:

(defun LPCOLESTR (txt)
  (let* ( (size (length txt))
          (wchar (sb-alien:make-alien short (1+ size))))
    (loop for i from 0 below size do
      (setf (sb-alien:deref wchar i) (char-code (char txt i)))
      finally
      (setf (sb-alien:deref wchar size) 0))
    (return-from LPCOLESTR wchar)
  )
)

最后抄了cffi的方法,创建的字符创需要手动释放内存。