如何使用SBCL sb-alien创建Gtk4窗口并实现有效回调?
问题
- 无法理解SBCL的Foreign Function Interface(FFI)文档,不知道如何在代码中实现GObject的Callback类型
- 无法将现有Gtk3示例适配到Gtk4,自行编写的代码出现编译错误,急需可运行的Gtk4示例
我的尝试代码
;;; test1 (cl:in-package "CL-USER") ; 已导入SB-ALIEN包 (load-shared-object "/usr/lib/x86_64-linux-gnu/libgtk-4.so.1.600.6") (define-alien-routine gtk_application_new (* t) (app (* t)) (flags (* T))) (define-alien-routine g_object_unref void (win (* t))) (define-alien-routine gtk_application_new (* t) (txt c-string) (flags int)) (define-alien-routine g_application_run int (app (* t)) (argc int) (argv (* t))) (define-alien-routine g_signal_connect long (instance (* t)) (sig c-string) ;(cback (function void (* t) (* t))) ; 不知道正确的回调类型 (alien-funcall (cback (function void))) (data (* t))) (define-alien-callable app_activate void (with-alien ((void)) (format t "application is activated"))) (with-alien ((app (* t)) (status int)) (setf app (gtk_application_new "test1.app.gtk" 0)) (g_signal_connect app "activate" (alien-callable-function 'app_activate) nil) (setf status (g_application_run app 0 nil)) (g_object_unref app) ;; 返回状态 status)
报错信息
> ; 在计算#P"/home/jacek/Programming/Lisp/lispy-experiments/aliengtk4/test1.lisp" > 第24行第0列开始的表单时捕获到1个错误: > > 调试器在主线程#<THREAD "main thread" RUNNING {10010A0003}>中触发SB-INT:COMPILED-PROGRAM-ERROR: > 执行存在编译错误的表单。表单: > (SB-ALIEN::ALIEN-LAMBDA VOID > (WITH-ALIEN ((VOID)) > (FORMAT T "application is activated"))) > 编译期错误: > 在宏展开(SB-ALIEN::ALIEN-LAMBDA VOID > (WITH-ALIEN # > #))期间出错。可使用*BREAK-ON-SIGNALS*拦截。 > > 值 WITH-ALIEN 不是 LIST 类型
Gtk3工作示例
;;; test2 (load-shared-object "/usr/lib/x86_64-linux-gnu/libgtk-3.so.0.2404.29") ;; 排除GTK库可能触发的:divide-by-zero陷阱,默认陷阱为(:OVERFLOW :INVALID :DIVIDE-BY-ZERO) ;; 参考: (sb-int:get-floating-point-modes) (sb-int:set-floating-point-modes :traps '(:overflow :invalid)) (define-alien-routine gtk_application_window_new (* t) (app (* t))) (define-alien-routine gtk_application_new (* t) (txt c-string) (flags int)) (define-alien-routine g_application_run int (app (* t)) (argc int) (argv (* t))) (define-alien-routine g_signal_connect_data long (instance (* t)) (sig c-string) (cback (function void (* t) (* t))) (data (* t)) (unusedptr (* t)) (unusedint int)) (define-alien-routine gtk_window_set_title void (win (* t)) (ttl (c-string))) (define-alien-routine gtk_window_set_default_size void (win (* t)) (x int) (y int)) (define-alien-routine gtk_widget_show_all void (win (* t))) (sb-alien::define-alien-callable mycallback void ((app (* t)) (u (* t))) (with-alien ((win (* t))) (setf win (gtk_application_window_new app)) (gtk_window_set_title win "This is window") (gtk_window_set_default_size win 100 100) (gtk_widget_show_all win))) (with-alien ((app (* t)) (status int)) (setf app (gtk_application_new "org.gtk.example" 0)) (g_signal_connect_data app "activate" (alien-callable-function 'mycallback) nil nil 0) (g_application_run app 0 nil)) (format t "App finished successfully~%")
SBCL版本
sbcl --version SBCL 2.2.11.160-65fe8bb77
解决方案与可运行Gtk4示例
错误原因分析
define-alien-callable语法错误:错误地在回调函数定义中使用with-alien ((void)),该宏用于声明alien变量,回调的参数列表需直接指定类型,无需包裹with-alien。g_signal_connect定义错误:GObject的activate信号回调签名应为void (*)(GApplication*, gpointer),对应SBCL FFI的(function void (* t) (* t)),之前的写法完全不符合规范。- 重复定义函数:重复定义了
gtk_application_new,第一个定义参数类型错误,应删除。 - Gtk4 API变更:Gtk4不再使用
gtk_widget_show_all,需用gtk_window_present显示窗口。
可运行的Gtk4示例代码
(cl:in-package "CL-USER") ;; 加载Gtk4库,路径根据你的系统调整 (load-shared-object "/usr/lib/x86_64-linux-gnu/libgtk-4.so.1") ;; 关闭可能由GTK触发的浮点除零陷阱 (sb-int:set-floating-point-modes :traps '(:overflow :invalid)) ;; 定义Gtk/GObject核心函数 (define-alien-routine gtk_application_new (* t) (application-id c-string) (flags int)) (define-alien-routine gtk_application_window_new (* t) (application (* t))) (define-alien-routine g_application_run int (application (* t)) (argc int) (argv (* t))) (define-alien-routine g_object_unref void (object (* t))) (define-alien-routine g_signal_connect long (instance (* t)) (signal c-string) (callback (function void (* t) (* t))) (data (* t))) (define-alien-routine gtk_window_set_title void (window (* t)) (title c-string)) (define-alien-routine gtk_window_set_default_size void (window (* t)) (width int) (height int)) (define-alien-routine gtk_window_present void (window (* t))) ;; 定义activate信号的回调函数 (define-alien-callable app-activate void ((app (* t)) (user-data (* t))) (declare (ignore user-data)) (with-alien ((window (* t))) (setf window (gtk_application_window_new app)) (gtk_window_set_title window "Gtk4 + SBCL FFI") (gtk_window_set_default_size window 400 300) (gtk_window_present window))) ;; 主程序逻辑 (with-alien ((app (* t)) (status int)) (setf app (gtk_application_new "org.example.gtk4" 0)) (g_signal_connect app "activate" (alien-callable-function 'app-activate) nil) (setf status (g_application_run app 0 nil)) (g_object_unref app) status) (format t "应用程序正常退出~%")
代码说明
- 回调函数
app-activate严格遵循GObject信号的参数要求:第一个参数是信号发射实例(GApplication),第二个是用户数据,用declare (ignore user-data)忽略未使用的参数。 - 使用
gtk_window_present替代Gtk3的gtk_widget_show_all,符合Gtk4的显示逻辑。 - 所有函数定义的参数类型严格对应C API的签名,避免类型不匹配导致的错误。
内容的提问来源于stack exchange,提问作者ruby_object
相关产品推荐
相关产品推荐

