《elisp 每日一练》——异步流式传输与 Process Filter

《elisp 每日一练》——异步流式传输与 Process Filter

Masimaro
2026-10-24 / 0 评论 / 1 阅读 / 正在检测是否收录...

在之前一篇文章中,我们了解了通过Emacs异步调用进程,防止耗时进程卡住主界面。但是细心的读者会发现之前介绍的方法有一个很大的问题:我们的输出都是从buffer中获取的,创建buffer会额外消耗资源,另外如果要利用外部进程输出的内容做一些事情,我们需要固定的从buffer中读取。但是我们怎么知道buffer中有内容或者和上次相比有新内容出现,这就需要我们自己主动的不断去访问buffer。或者等待进程结束之后一次性读取所有内容。这样似乎又变成了需要等待的同步事件。对于这种场景,最理想的方式就是注册一个异步处理方式,只有在有新内容的时候触发。要实现这个需求,Emacs 为我们提供了 Process Filter 机制

背景知识

所谓的 Process Filter 是 Emacs 异步进程架构中的信息拦截器。它的工作原理是,Emacs调用进程时,进程的标准输出会被操作系统管道推送到Emacs端。Emacs 捕获到这些增量数据后,会立刻中断并调用你注册的 Filter 函数,将刚吐出的文本片段(String 增量)传递给你。如果不指定,那么会调用默认的 Filter 函数,该函数只是机械的将新内容写回到指定的buffer中。

一个标准的 Filter 回调函数的签名通常如下:

(defun my-custom-filter (proc output-string)
  )

其中 proc 是当前的进程对象。而output-string 则是当前增量更新的程序输出。

这里请注意, output-string 并不保证数据是完整一行或者是完整的结构,它只是机械的将本次输出的内容传进来。有时候数据会出现残缺,所以我们在处理的时候需要关注数据的完整性,有时候需要自己处理恢复完整数据,这算是另一种形式的“粘包”

filter 函数的注册可以利用我们在前面介绍 make-process 的时候提到的 :filter 关键字

练习

我们对昨天的程序做一个简单的变更。cmake 编译会输出大量信息,一般来讲我们只关心当前编译的进度以及错误出现时的报错信息。如果cmake 大量输出,Emacs需要频繁往buffer中写入新内容并刷新显示可能会造成卡顿。因此我们利用filter实现按需分批次写入缓冲区的功能。

(defvar my-build-progress nil
  "当前构建进度百分比,nil 表示未在构建。")

(defun my-build-progress-filter (proc output)
  "节流并解析 OUTPUT 中的进度,同时把完整输出写入进程缓冲区。"
  (let ((buf (process-buffer proc)))
    (when (buffer-live-p buf)
      (with-current-buffer buf
        (goto-char (point-max))
        (insert output))))

  (let ((start 0)
        (last-pct nil))
    (while (string-match "\\([0-9]+\\)%" output start)
      (setq last-pct (string-to-number (match-string 1 output)))
      (setq start (match-end 0)))
    (when (and last-pct (not (equal last-pct my-build-progress))) ;; 只有进度更新时才打印信息
      (setq my-build-progress last-pct)
      (message "构建进度: %d%%" last-pct))))

(defun my-cmake-msvc-build (source-dir)
  "在 SOURCE-DIR 下执行 CMake 配置与构建。"
  (interactive "D选择项目根目录:")
  (let* ((default-directory (file-name-as-directory source-dir))
         (build-dir "build")
         (out-buf (get-buffer-create "*compile-build*"))
         (err-buf (get-buffer-create "*compile-error*"))
         (config-proc nil)
         (build-proc nil))

    ;; 错误缓冲区启用 compilation-minor-mode,支持跳转
    (with-current-buffer err-buf
      (erase-buffer)
      (compilation-minor-mode t))

    ;; 构建输出缓冲区也启用编译模式,兼容走 stdout 的链接错误
    (with-current-buffer out-buf
      (erase-buffer)
      (compilation-minor-mode t))

    (setq my-build-progress nil)
    (force-mode-line-update)
    (message "开始 CMake 配置...")

    (setq config-proc
          (make-process
           :name "cmake-config"
           :command (list "cmake" "." "-B" build-dir
                          "-G" "Visual Studio 18 2026"
                          "-T" "v143"
                          "-A" "x64"
                          "-DCMAKE_BUILD_TYPE=Debug"
                          "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL")
           :buffer out-buf
           :stderr err-buf
           :noquery t
           :sentinel
           (lambda (p event)
             (when (memq (process-status p) '(exit signal))
               (if (zerop (process-exit-status p))
                   (progn
                     (message "配置成功,开始编译...")
                     (setq build-proc
                           (make-process
                            :name "cmake-build"
                            :command (list "cmake" "--build" build-dir)
                            :buffer out-buf
                            :stderr err-buf
                            :filter #'my-build-progress-filter
                            :noquery t
                            :sentinel
                            (lambda (p1 event1)
                              (when (memq (process-status p1) '(exit signal))
                                (setq my-build-progress nil)
                                (if (zerop (process-exit-status p1))
                                    (message "构建成功")
                                  (message "构建失败,查看 *compile-error*")
                                  (display-buffer
                                   (if (> (with-current-buffer err-buf (buffer-size)) 0)
                                       err-buf
                                     out-buf))))))))
                 (message "CMake 配置失败,查看 *compile-error*")
                 (display-buffer err-buf)))))
    config-proc))

本次我们添加了一个名为 my-build-progress-filter 的过滤函数,它主要的作用是将新增的内容插入buffer、并且提取里面进度信息。虽然这里频繁的插入信息到buffer中,但是如果当前Emacs不显示buffer的话,它并不会立即重绘并刷新buffer,只有在调用 message 或者 force-mode-line-update 这种显式刷新的函数才会刷新当前buffer显示。这里只有在进度变化时才刷新,算是降低了刷新频率。

后面编译进程相关的代码就与前面的类似。只是通过 :filter 指定了函数

总结

本篇我们讨论了 make-process 中的 :filter 用法,绑定一个函数作为回调,每当进程有新的输出时被调用,该函数可以进行数据的解读或者控制数据插入buffer中的行为。函数需要接受两个参数:proc 是对应的进程对象、output 是本次调用新增的字符串

0

评论 (0)

取消