Elisp 有办法获取未经过 Advice 的原始的函数定义吗?

比如说,

(define-advice switch-to-buffer (:after (&rest _) recenter) (recenter))

如何调用未advice的原始的 switch-to-buffer 呢?


问题有歧义,我重新编辑一下:

现在已知是 switch-to-buffer 已经被 Advice 了,可能是 around,也可能是别的其他的,这些函数会在实际调用 switch-to-buffer 的时候被调用。

那么现在我突然想调用最原始的 switch-to-buffer ,就只有依次 advice-remove 所有的 advice,然后调用,然后再加回去。

这种方法在advice非常多的时候不是很好用,而且不是很干净。

我就想问问,在这种情况下有没有办法去调用原始的 switch-to-buffer

需要调用原函数(OLDFUN),你得用 :around。用 Advice 之前得要看明白 (info “(elisp) Advice combinators”)

你的描述存在歧义:

  1. 别人写了一个 advice,但是你想调原来的函数。
  2. 你正在写一个 advice,过程中需要调用原函数。

第 1 种情况,又可以细分成两种:

  • 1.1. 别人添加的 advice 有问题,解决之道是删除

    如何一次性删除所有 advice,看这个帖子:Unadvise a function (remove all advice from it) - Emacs Stack Exchange

  • 1.2. 别人添加的 advice 没有问题,而且相关的 package 需要,不能删除。但是你自己在某个场景下需要调用原始函数:

    (defun foo ()
    (message "foo"))
    
    (defun override@foo ()
    (message "override@foo"))
    
    (fset 'orignal-foo (symbol-function 'foo))
    (advice-add 'foo :override 'override@foo)
    
    (foo)         ;; => override@foo
    (orignal-foo) ;; => foo
    

如果是第 2 个意思,应该用 :around:

(defun around@switch-to-buffer (orign-fn &rest args
                                         ;; 如果需要用到具体参数,直接抄原函数的定义:
                                         ;;     buffer-or-name &optional norecord force-same-window)
                                         )
  ;; do something before orign-fn
  (apply orign-fn args)
  ;; do something after orign-fn
  )

(advice-add 'switch-to-buffer :around 'around@switch-to-buffer)

使用 :around 表示你必须调用原来的函数,以完成工作。否则,就变成了 :override 的效果。

:before/:after 可以看作是 :around 的特例,这两种情况下自动调用原来的函数。

@xuchunyang @twlz0ne

我的描述确实有歧义,两位理解的都不是我需要的。

(@twlz0ne 的第一种情况应该就是我需要的,不知道有没有办法解决~ )

现在已知是 switch-to-buffer 已经被 Advice 了,可能是 around,也可能是别的其他的,这些函数会在实际调用 switch-to-buffer 的时候被调用。

那么现在我突然想调用最原始的 switch-to-buffer,就只有依次 advice-remove 所有的 advice,然后调用,然后再加回去。

这种方法在advice非常多的时候不是很好用,而且不是很干净。

我就想问问,在这种情况下有没有办法去调用原始的 switch-to-buffer


(defun zx ()
  (print 89))

(define-advice zx (:before (&rest _) rece) (print "77"))

(defun around@zx (orign-fn &rest args)
  (apply orign-fn args)
  (print "around")
  )
                                                                                       
(advice-add 'zx :around 'around@zx)

(zx)

1 个赞

你再看我前面的回答,补充了各种分歧下的解决方案。

可是这个好像和 (symbol-function 'foo) 和 advice-add 的位置有关系吧?

如果是先调用 advice-add 就没办法了。。。

明白了你的问题,advice--cdr 貌似可以:

(defun foo () 123)
;; => foo

(define-advice foo (:around (_oldfun) bar)
  456)
;; => foo@bar

(foo)
;; => 456

(funcall (advice--cdr (symbol-function 'foo)))
;; => 123

注意我仅试过这一个例子。


但在我看来应该避免这样的问题,Advice 的使用需要节制,像 switch-to-buffer 这样的库函数不适合 Advice,优先重写一个新函数,如:switch-to-buffer-and-recenter

3 个赞

那就想办法更先,自己的配置自己做主。

1 个赞

参考 advice-function-mapc 的实现, 用 advice--cdr 应该可以处理多个 advice 的例子 .

(defun symbol-function-original (symbol)
  (let ((function-def (advice--symbol-function symbol)))
    (while (advice--p function-def)
      (setq function-def (advice--cdr function-def)))
    function-def))

(symbol-function-original 'foo)
;; (lambda nil
;;   (message "call foo"))


(defun foo ()
  (message "call foo"))

(defun foo-ad (orignal-fn &rest args)
  (message "call foo-ad")
  (apply orignal-fn args))

(advice-add 'foo :around 'foo-ad)

(foo)
;; call foo
;; call foo-ad

(funcall (symbol-function-original 'foo))
;; call foo

2 个赞

advice.el有ad-get-orig-definition一键获取原始定义

advice--cdr 之前还需要 advice--p 判断一下,否则可能得到错误的返回值:

(with-emacs
  (byte-compile
   (defun greet (name)
     (message "hello, %s" name)))
  (advice--cdr (symbol-function 'greet)))
;; => "hello, %s"

显然,上面代码返回的值并非预期的 nil。

问题就出在 byte code 上,如果不进行编译,函数就只是 list:

(with-emacs
  (defun greet (name)
    (message "hello, %s" name))
  (symbol-function 'greet))
;; => (lambda (name) (message "hello, %s" name))

这时 advice--cdr 必然出现异常,也就及时发现了错误。

比较隐蔽的错误是在函数编译之后,因为函数编译之后成了数组。 对数组进行 advice--cdr 或者 aref 操作,得到的是数组当中的元素,也就是函数内部的某一个语句(对于 advice--cdr 来说就是索引为 2 位置的值):

(ith-emacs
  (byte-compile
   (defun greet (name)
     (message "hello, %s" name)))
  (symbol-function 'greet))
;; => #[(name) "\303\201\303\202\"\302\207" [name message "hello, %s"] 3]

这里得到的是字符串,所以如果用 funcall 执行也会及时报错。但如果这个位置正好是一个函数的 symbol,可能就稀里糊涂地执行了下去

随便看看一些内置函数没有 advice 会返回什么:

(with-emacs
  (advice--cdr (symbol-function 'switch-to-buffer)))
;; window-normalize-buffer-to-switch-to

(with-emacs
  (advice--cdr (symbol-function 'lisp-indent-function)))
;; parse-partial-sexp

最后再用自定义函数完整地检验一遍:

(with-emacs
  (require 'cl)
  
  (byte-compile
   (defun greet (name)
     (message "hello, %s" name)))

  ;; 必须不是 advice,否则抛出错误
  (assert (not (advice--p (symbol-function 'greet))))

  (define-advice greet (:override (name) override)
    (message "hi, %s" name))

  ;; 返回原始函数
  (and (advice--p (symbol-function 'greet))
       (advice--cdr (symbol-function 'greet))))
;; => #[(name) "\303\201\303\202\"\302\207" [name message "hello, %s"] 3]
1 个赞

直接用ad-get-orig-definition吧,判断工作都给你做好了,就是引用so-called obsolete package(advice.el)显得不优雅而已

(defun ad-get-orig-definition (function) ;FIXME: Rename to "-unadvised-".
  (if (symbolp function)
      (setq function (if (fboundp function)
                         (advice--strip-macro (symbol-function function)))))
  (while (advice--p function) (setq function (advice--cdr function)))
  function)

advice--cdr就是个aref的包装,他懂个啥子函数

(defsubst advice--cdr   (f) (aref (aref f 2) 2))

ad-get-orig-definition 也不懂函数呀,它只不过是读取了对的位置。

另,这个函数的命名,恐怕通不过 MELPA 的审核,内置的包果然就可以为所欲为。

你说不懂也是对的,毕竟ad-get-orig-definition遇到非函数会原封不动的返回而不会报错