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

