defclass 定义里使用变量报错 Invalid slot type: secret, string, aria2-rpc-secret

我下载了 emacsmirror/aria2 镜像的代码,尝试运行,结果报错:

和问题有关代码如下,这个是为什么? defclass 里面不能计算变量么?

(defcustom aria2-rcp-secret (or (let ((uuidgen (executable-find "uuidgen")))
                                  (and uuidgen (string-trim (shell-command-to-string uuidgen))))
                                (sha1 (format "%s%s%s%s%s%s%s%s%s" (user-uid) (emacs-pid) (system-name)
                                              (user-full-name) (current-time) (emacs-uptime) (buffer-string)
                                              (random) (recent-keys))))
  "Secret value used for authentication with the aria2c process, for use with --rpc-secret= switch."
  :type '(integer :tag "Http port")
  :group 'aria2)

....

(defclass aria2-controller (eieio-persistent)
  ((request-id :initarg :request-id
               :initform 0
               :type integer
               :docstring "Value of id field in JSONRPC data, gets incremented for each request.")
   (rcp-url :initarg :rcp-url
            :initform (aria2--url)
            :type string
            :docstring "Url on which aria2c listens for JSON RPC requests.")
   (secret :initarg :secret
           :initform aria2-rcp-secret
           :type string
           :docstring "Secret value used for authentication with the aria2c process, for use with --rpc-secret= switch.")
   (pid :initarg :pid
        :initform -1
        :type integer
        :docstring "PID of the aria2c process, or -1 if process isn't running."))
  :docstring "This takes care of starting/stopping aria2c process and provides methods for each remote command.")

secret slot的type check失败了,你应该打开backtrace看看到底给secret slot存了个什么值

aria2-rcp-secret 的值是 "eb2e3289-0285-4e6b-bfc7-2b69daadd67f". 我 打开 toggle-debug-on-error后,再 C-M-x 运行 defclass . 报错信息是:

Debugger entered--Lisp error: (invalid-slot-type secret string aria2-rcp-secret)
  signal(invalid-slot-type (secret string aria2-rcp-secret))
  eieio--perform-slot-validation-for-default(#s(cl-slot-descriptor :name secret :initform aria2-rcp-secret :type string :props nil) nil)
  eieio--add-new-slot(#s(eieio--class :name aria2-controller :docstring nil :parents (#s(eieio--class :name eieio-persistent :docstring "This special class enables persistence through sav..." :parents nil :slots [#s(cl-slot-descriptor :name file :initform unbound :type string :props ((:documentation . "The save file for this persistent object.\nThis mus...")))] :index-table #<hash-table eq 1/65 0x155a11fa7855> :children (aria2-controller) :initarg-tuples ((:file . file)) :class-slots [#s(cl-slot-descriptor :name do-backups :initform t :type boolean :props ((:documentation . "Saving this object should make backup files.\nSetti..."))) #s(cl-slot-descriptor :name file-header-line :initform ";; EIEIO PERSISTENT OBJECT" :type string :props ((:documentation . "Header line for the save file.\nThis is used with t..."))) #s(cl-slot-descriptor :name extension :initform ".eieio" :type string :props ((:documentation . "Extension of files saved by this object.\nEnables a...")))] :class-allocation-values [t ";; EIEIO PERSISTENT OBJECT" ".eieio"] :default-object-cache #<eieio-persistent eieio-persistent-155a11ef7996> :options (:custom-groups nil :documentation "This special class enables persistence through sav..." :abstract t))) :slots (#s(cl-slot-descriptor :name rcp-url :initform (aria2--url) :type string :props nil) #s(cl-slot-descriptor :name request-id :initform 0 :type integer :props nil) #s(cl-slot-descriptor :name file :initform unbound :type string :props ((:documentation . "The save file for this persistent object.\nThis mus...")))) :index-table #<hash-table eq 5/65 0x155a182b9971> :children nil :initarg-tuples ((:rcp-url . rcp-url) (:request-id . request-id) (:file . file)) :class-slots (#s(cl-slot-descriptor :name extension :initform ".eieio" :type string :props ((:documentation . "Extension of files saved by this object.\nEnables a..."))) #s(cl-slot-descriptor :name file-header-line :initform ";; EIEIO PERSISTENT OBJECT" :type string :props ((:documentation . "Header line for the save file.\nThis is used with t..."))) #s(cl-slot-descriptor :name do-backups :initform t :type boolean :props ((:documentation . "Saving this object should make backup files.\nSetti...")))) :class-allocation-values [".eieio" ";; EIEIO PERSISTENT OBJECT" t] :default-object-cache #<aria2-controller aria2-controller-155a182b9bb6> :options (:custom-groups nil :docstring "This takes care of starting/stopping aria2c proces...")) #s(cl-slot-descriptor :name secret :initform aria2-rcp-secret :type string :props nil) :secret nil defaultoverride nil)
  #f(compiled-function (cname superclasses slots options) "Define CNAME as a new subclass of SUPERCLASSES.\nSLOTS are the slots residing in that class definition, and OPTIONS\nholds the class options.\nSee `defclass' for more information." #<bytecode 0x1cff8c5a5ebb3174>)(aria2-controller (eieio-persistent) ((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) (:docstring "This takes care of starting/stopping aria2c proces..."))
  apply(#f(compiled-function (cname superclasses slots options) "Define CNAME as a new subclass of SUPERCLASSES.\nSLOTS are the slots residing in that class definition, and OPTIONS\nholds the class options.\nSee `defclass' for more information." #<bytecode 0x1cff8c5a5ebb3174>) (aria2-controller (eieio-persistent) ((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) (:docstring "This takes care of starting/stopping aria2c proces...")))
  eieio-defclass-internal(aria2-controller (eieio-persistent) ((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) (:docstring "This takes care of starting/stopping aria2c proces..."))
  (progn (defalias 'aria2-controller-p (eieio-make-class-predicate 'aria2-controller)) (defalias 'aria2-controller--eieio-childp (eieio-make-child-predicate 'aria2-controller)) (defalias 'aria2-controller-child-p 'aria2-controller--eieio-childp) (make-obsolete 'aria2-controller-child-p "use (cl-typep ... \\='aria2-controller) instead" "25.1") (define-symbol-prop 'aria2-controller 'cl-deftype-satisfies #'aria2-controller--eieio-childp) (eieio-defclass-internal 'aria2-controller '(eieio-persistent) '((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) '(:docstring "This takes care of starting/stopping aria2c proces...")) (defun aria2-controller (&rest slots) "Create a new object of class type `aria2-controlle..." (declare (compiler-macro (lambda (whole) (if (not (stringp ...)) whole (macroexp--warn-and-return (format "Obsolete name arg %S to constructor %S" ... ...) `...))))) (apply #'make-instance 'aria2-controller slots)))
  (defclass aria2-controller (eieio-persistent) ((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) :docstring "This takes care of starting/stopping aria2c proces...")
  eval((defclass aria2-controller (eieio-persistent) ((request-id :initarg :request-id :initform 0 :type integer :docstring "Value of id field in JSONRPC data, gets incremente...") (rcp-url :initarg :rcp-url :initform (aria2--url) :type string :docstring "Url on which aria2c listens for JSON RPC requests.") (secret :initarg :secret :initform aria2-rcp-secret :type string :docstring "Secret value used for authentication with the aria...") (pid :initarg :pid :initform -1 :type integer :docstring "PID of the aria2c process, or -1 if process isn't ...")) :docstring "This takes care of starting/stopping aria2c proces...") nil)
  edebug-eval-defun(nil)
  apply(edebug-eval-defun nil)
  eval-defun(nil)
  eros-eval-defun(nil)
  funcall-interactively(eros-eval-defun nil)
  call-interactively(eros-eval-defun nil nil)
  command-execute(eros-eval-defun)

看上去是赋值了变量没错。我还特意 (type-of aria2-rcp-secret) 检查了下,确实是 string. 真是奇怪。

这会报同样的错 (invalid-slot-type name string user-full-name) Emacs 27.1

user-full-name
;; => "Xu Chunyang"

(defclass person ()
  ((name :initarg :name
         :initform user-full-name
         :type string)))

文档说 :initform 后接一个表达式,但变量也是表达式,不知为何不行,替换成下面任意一种都没问题:

"Xu Chunyang"
(identity user-full-name)
(symbol-value 'user-full-name)

所以你可以试试 :initform aria2-rcp-secret 改成 :initform (identity aria2-rcp-secret)

测试确认可以了。非常感谢! @xuchunyang 我之前搜索过GitHub代码,看到其他人也是这么用的,难道和Emacs版本或者说EIEIO版本有关系?

错误信息是由 eieio--perform-slot-validation-for-default 抛出的,看看它的定义:

(defun eieio--perform-slot-validation-for-default (slot skipnil)
  "For SLOT, signal if its type does not match its default value.
If SKIPNIL is non-nil, then if default value is nil return t instead."
  (let ((value (cl--slot-descriptor-initform slot))
        (spec (cl--slot-descriptor-type slot)))
    (if (not (or (eieio-eval-default-p value) ;FIXME: Why?
                 eieio-skip-typecheck
                 (and skipnil (null value))
                 (eieio--perform-slot-validation spec value)))
        (signal 'invalid-slot-type (list (cl--slot-descriptor-name slot) spec value)))))

注意那一行注释 :FIXME: Why?,说明早就有人发现问题并且感到很懵逼了。

函数 eieio-eval-default-p 其实很简单,但是为啥不修复,这么设计?

(defsubst eieio-eval-default-p (val)
  "Whether the default value VAL should be evaluated for use."
  (and (consp val) (symbolp (car val)) (fboundp (car val))))

eieio-eval-default-p 的实现来看,只要在 :initform 后面放一个列表,中间怎么乱来都没关系,最后有返回值就行了:

  • `,user-full-name
  • '(identity user-full-name)
  • '(symbol-value 'user-full-name)
  • '(bound-and-true-p user-full-name)
  • '(if user-full-name user-full-name)
  • '(let ((init user-full-name)) init)
  • ...
(defclass person ()
   ((name :initarg :name
          :initform (let ((init user-full-name)) init)
          :type string)))
;; => person
1赞

原来如此,非常感谢,还是要深入代码去看,虽然技术还没那么好,努力向这个方向进步。感谢哈!

谈不上啥技术,就是忍不住想考古一下.

这个问题是在 2013 年合并 CEDET 代码的的时候引进的:

https://emba.gnu.org/emacs/emacs/-/commit/890f78904a8eb787cf1559c6e9ddac395b49a09e#7f2fab9d92995cf3353d92a99ac4e052b6711d9c_0_830

那句 ;FIXME: Why? 是 2015 年一次合并的时候加上的:

https://emba.gnu.org/emacs/emacs/-/commit/50c117fe86d94719807cbe08353c032779b3b910#7f2fab9d92995cf3353d92a99ac4e052b6711d9c_557_542

这么多年都没人想去修复,其实大佬们也会「如果代码能工作,就别动它」:

这个限制很明显的,就是只希望 initform 的参数是字面常量,对于 eieio 这样用宏实现的,很可能原作者想直接避免宏展开导致的多次执行副作用的问题。

如果给一个宏传参 symbol 有问题,那么传 (symbol-value 'symbol) 应该也会有问题吧?

我猜测 eieio 本来是只允许字面常量的,eieio-eval-default-p 为了能用 form 后加的,然后 corner case 没做好。