You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Common Lisp技术问题:如何在WITH-EVENT-LOOP宏中使用自定义宏?

问题描述

我正在使用cl-sdl2编写一款简易游戏应用,cl-sdl2包含名为WITH-EVENT-LOOP的宏,可用于启动带有事件处理器的SDL应用。目前我按如下方式使用该宏:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)

  ;; Input handlers below
  (:keydown (:keysym keysym)
            (funcall input-func :keydown :keysym keysym))
  (:keyup (:keysym keysym)
          (funcall input-func :keyup :keysym keysym)))

由于省略了至少6个遵循相同模式的鼠标、游戏控制器输入事件处理器,实际代码冗长且高度重复。于是我编写了一个用于生成此类模板代码的宏:

(defmacro forward-evt-to (target evt &rest evt-args)
  `(,evt ,evt-args
         (funcall ,target ,evt ,@evt-args)))

该宏的展开结果符合WITH-EVENT-LOOP的要求:

> (macroexpand-1 '(forward-evt-to input-func :keydown :keysym keysym))
(:KEYDOWN (:KEYSYM KEYSYM) (FUNCALL INPUT-FUNC :KEYDOWN :KEYSYM KEYSYM))
T

但直接在WITH-EVENT-LOOP中使用该宏时会出现编译错误,示例代码如下:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)

  (forward-evt-to input-func :keydown :keysym keysym)
  (forward-evt-to input-func :keyup :keysym keysym))

错误信息为:

; The value
;      INPUT-FUNC
;    is not of type
;      LIST

请问在不直接修改WITH-EVENT-LOOP宏的前提下,是否有办法在其中使用自定义的forward-evt-to宏?

解决方案

问题核心在于宏展开时机不匹配:WITH-EVENT-LOOP会直接解析传入的子形式,不会先展开内部的宏。当它看到(forward-evt-to ...)时,会误把这个列表当成事件处理器定义,既不识别forward-evt-to为合法事件类型,还会尝试将input-func解析成事件参数列表,最终触发类型错误。

以下是三种无需修改WITH-EVENT-LOOP的可行方案:

1. 用外层宏生成完整的WITH-EVENT-LOOP调用

编写一个包裹WITH-EVENT-LOOP的宏,提前生成所有事件处理器再注入进去:

(defmacro with-forwarded-events ((target &rest events) &body body)
  `(sdl2:with-event-loop (:method :poll)
     ,@body
     ,@(loop for (evt . args) in events
             collect `(,evt ,args (funcall ,target ,evt ,@args)))))

使用示例:

(with-forwarded-events (input-func
                        (:keydown :keysym keysym)
                        (:keyup :keysym keysym)
                        (:mousebuttonup :button button :x x :y y))
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t))

这个方案简洁易维护,适合批量生成事件处理器。

2. 强制提前展开宏(内嵌写法)

通过eval-when在编译阶段手动展开forward-evt-to,再将结果插入WITH-EVENT-LOOP:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)

  ,(eval-when (:compile-toplevel :load-toplevel :execute)
     (macroexpand-1 '(forward-evt-to input-func :keydown :keysym keysym)))
  ,(eval-when (:compile-toplevel :load-toplevel :execute)
     (macroexpand-1 '(forward-evt-to input-func :keyup :keysym keysym))))

这里的逗号是Lisp反引号语法,用于将宏展开后的结果直接插入参数列表。缺点是写法繁琐,不适用于大量事件。

3. 改进宏生成事件处理器片段

修改forward-evt-to为批量生成处理器的宏,再通过反引号展开注入:

(defmacro forward-evts (target &rest evt-specs)
  `(progn
     ,@(loop for (evt . args) in evt-specs
             collect `(,evt ,args (funcall ,target ,evt ,@args)))))

使用示例:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)
  ,@(macroexpand-1 '(forward-evts input-func
                                  (:keydown :keysym keysym)
                                  (:keyup :keysym keysym))))

这种方式灵活性更高,可单独生成事件处理器片段再组合使用。

内容的提问来源于stack exchange,提问作者Dan Passaro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 01:06:09