这份文档用于约束 AI 在本仓库中生成 Qt 绑定代码的行为。
如果 AI 需要继续为本项目生成代码,应优先遵守本文件,而不是临时发明新的模式。如有特殊情况,必须及时向用户报告。
本文档的优先级高于 AGENTS.md
-
严格尊重 Qt 类层次。
例如QPushButton -> QAbstractButton -> QWidget -> QObject,QLabel -> QFrame -> QWidget -> QObject。
如果 Qt 中存在中间基类,而项目中还没有补齐,应优先补齐中间层,而不是直接把方法堆到叶子类上。
例外是,如果某个类存在派生类,但 MoonBit 侧尚未添加任何该类的派生类,则无需 As trait。而在后续补上派生类后,应该添加 As trait 并按原则 2 进行一定重构。 -
公共方法应尽量放在正确的 trait 层级。
例如按钮共有方法应放到AsAbstractButton,布局共有方法应放到AsLayout/AsBoxLayout,而不是只挂在QPushButton或QHBoxLayout上。 -
优先复用现有抽象,不要回退到 ad-hoc 实现。
已有Signal[A]、SignalAdapter、qt<T>、QSTATIC、QMETHOD、SIGNAL_DEF、COVARIANT等机制时,应优先使用。 -
命名和语义尽量贴近 Qt。
如果 Qt 的方法叫setCheckable、isChecked、animateClick,MoonBit 侧尽量保持同名,而不是自行发明别名。 -
Moonbit 方法顺序和导出顺序应该按首字母顺序。
尤其是对于 As 系列 trait。它内部应该分为三个区域:- as_ 函数
- 公共方法和插槽(按首字母顺序)
- 信号(按首字母顺序) 加入新方法时也应当插入到适当的位置。
-
Moonbit 导出顺序也应该按首字母顺序。
对于每个对应单个 Qt 类的 C++ stub,它内部应该分为四个区域:- 构造函数
- 公共方法和插槽(按首字母顺序)
- 信号(按首字母顺序)
- 协变函数 加入新方法时也应当插入到适当的位置。
-
专注于代码。无需刻意进行手动调整代码格式等有自动化流程辅助的操作,也无需调用代码格式化工具,相关工作应该交给人类。
-
不知道做什么时,可以参考现有实现,也可以询问用户或让用户接管。
-
目前 API 尚不稳定,无需运行
moon info生成 .mbti 文件。
本项目有两个明确层次:
-
src/
这一层是 public wrapper。负责:- 定义抽象类型和私有的 FFI 部分,并放在源码最上部
- 定义 public struct/newtype
- 定义 trait
- 在 trait 默认实现中调用 FFI
- 组织 public API
-
stub/
这一层是 C++ 胶水。负责:- 调用真实 Qt API
- 做字符串、信号、协变等跨 FFI 适配
- 为 MoonBit 的 FFI 提供导出符号
- MoonBit 侧
extern "C"函数绝对不能导出。 - public 层不要直接暴露 stub 层的实现细节。
public wrapper 通常写成:
pub type QWidget该类型直接对应 stub 侧的 qt<QWidget>,其含义为可空不悬垂指针。
不应在 MoonBit 侧使用 Option[QWidget],QWidget 本身就是可空语义。
FFI 名称遵循:
- MoonBit 外部函数:
_QWidget_show - C++ 导出函数:
QWidget_show
也就是说,public 接口只调用 _QWidget_show,真正的 C++ 符号名是 QWidget_show。
#borrow(this)
extern "C" fn _QWidget_show(this : QWidget) = "QWidget_show"本项目中的 Qt wrapper 类型(如 QObject、QWidget 等)本身具有可空语义。此外,由于 Qt 对象可能在对象树或其他 Qt 生命周期机制下被销毁,原本可用的句柄也可能在后续调用时失效并变为空。因此,应遵循以下规则:
- 对应 C++ stub 侧成员函数的方法需要空指针检查
- 空指针检查应在 MoonBit 侧 public 接口中,而不是在
extern "C"函数或 stub 中 as_系列函数和AsObject::is_null无需检查- 参数不用检查
对 trait 方法,空指针检查如下:
pub(open) trait AsWidget: AsObject {
as_widget(Self) -> QWidget
show(Self) -> Unit raise NullptrError = _
// ...
}
impl AsWidget with show(self) {
ensure_not_nullptr(self)
_QWidget_show(self.as_widget())
}对普通方法,空指针检查如下:
pub fn QComboBox::clear(self : Self) -> Unit raise NullptrError {
ensure_not_nullptr(self)
_QComboBox_clear(self)
}- As trait 用于表达 Qt 的继承/子类型关系。
- As trait 应为
pub(open),允许用户为自定义组件实现。 - As trait 默认实现用于承接“该层所有子类共有的方法”。
- 叶子类只保留:
- 构造函数
- 该类特有的方法
- 该类特有的信号
- 向上转型所需的
as_xxx
例如:
AsWidget承接show、setWindowTitle、setLayoutAsLayout承接addWidgetAsBoxLayout承接addLayoutAsAbstractButton承接按钮共有方法和信号
pub(open) trait AsWidget: AsObject {
as_widget(Self) -> QWidget
show(Self) -> Unit raise NullptrError = _
// ...
}
impl AsWidget with show(self) {
ensure_not_nullptr(self)
_QWidget_show(self.as_widget())
}- MoonBit 侧统一使用
Signal[A] - 连接统一使用
QObject::connect - 信号 getter 名字应尽量和 Qt 原始信号一致,例如:
clicked() -> Signal[Bool]pressed() -> Signal[Unit]textChanged() -> Signal[String]
访问 https://doc.qt.io/qt-6/ 以获取在线文档。例如 QAbstractButton 的文档位于 https://doc.qt.io/qt-6/qabstractbutton.html,请注意类名在这里应当转为全小写。
stub 层优先使用这些宏:
QSTATIC(Self, Method, ...)QMETHOD(Self, Method, ...)SIGNAL_DEF(Sender, Signal, Arg, Converter)COVARIANT(Derived, Base)
这些宏已经代表当前项目认可的模式。除非当前模式无法表达,否则不要回退到手写重复样板。
SIGNAL_DEF 宏可以直接把 Converter 作为 lambda 内联在宏中。但如果有函数能直接符合要求,则无需额外包一层 lambda。
qt<T> 的目标是保留 Qt 风格协变与防止悬垂。其本身具有可空语义,直接对应 MoonBit 侧的抽象类型。
尤其是:
qt<QPushButton> -> qt<QAbstractButton> -> qt<QWidget> -> qt<QObject>qt<QLabel> -> qt<QFrame> -> qt<QWidget>
这类上转型必须通过现有协变机制来做,不要重新发明别的桥接表示。
统一使用:
str::mbt_to_qtstr::qt_to_mbt
不要在各个 .cpp 文件里重复手写 QString / String 转换逻辑。
- 如果 Qt API 使用的是具名枚举类型,而该枚举在 MoonBit 侧值得直接表达语义,则应在 MoonBit public 层定义对应的常量枚举,而不是把 public API 退化成
Int。 - 这类 MoonBit 常量枚举应尽量保持 Qt 原始枚举项的语义和数值,例如
Qt::CheckState对应Unchecked = 0、PartiallyChecked = 1、Checked = 2。 - MoonBit internal/public 的
extern "C"声明可以直接使用这种常量枚举作为参数和返回值。按照 MoonBit FFI 约定,常量枚举会按整数传递,无需额外桥接层。 - stub 层可以继续把这类参数和返回值写成
Int,并在需要时显式转换到 Qt 枚举,例如static_cast<Qt::CheckState>(state)。 - 信号也遵循同样规则:如果 Qt 信号参数本质上是这类枚举,则 MoonBit 侧信号类型应写成对应枚举类型,而不是
Signal[Int]。
- 如果 Qt 方法带有默认参数,应优先在 MoonBit public 层表达默认参数,而不是为了省事把该参数直接删掉,或额外发明多个 ad-hoc 重载。
- 这种情况下,stub 层和 MoonBit
extern "C"声明应保留完整签名,显式写出所有参数;默认值只放在 public wrapper 或 trait 默认实现这一层。 - 也就是说:
- C++ stub:绑定真实 Qt 方法的完整参数列表
- MoonBit
extern "C":与 stub 保持一一对应的完整参数列表 - MoonBit public API:使用
arg? : T = default_value承接 Qt 默认参数
- 例如
QBoxLayout::addLayout(QLayout *layout, int stretch = 0),stub 和extern "C"都应保留stretch : Int,而 MoonBit public 层再写成stretch? : Int = 0。 - 只有当 Qt 默认参数对应的能力明确不打算暴露,且这种裁剪是用户确认过的设计决定时,才可以不映射该参数。
在 As trait 中的默认参数写法:
pub(open) trait AsBoxLayout: AsLayout {
as_box_layout(Self) -> QBoxLayout
addLayout(Self, layout : &AsLayout, stretch? : Int) -> Unit raise NullptrError = _
// ...
}
impl AsBoxLayout with addLayout(self, layout, stretch? = 0) {
ensure_not_nullptr(self)
_QBoxLayout_addLayout(self.as_box_layout(), layout.as_layout(), stretch)
}普通方法的默认参数写法:
pub fn QCheckBox::setTristate(self : Self, tristate? : Bool = true) -> Unit raise NullptrError {
ensure_not_nullptr(self)
_QCheckBox_setTristate(self, tristate)
}- 从 C++
bool到 MoonBitBool:使用Bool::make(...) - 从 MoonBit
Bool到 C++bool:使用现有隐式/显式转换能力,不要再新增无必要 helper
所有 Qt 信号应优先走 SignalAdapter + SIGNAL_DEF。
不要再手写这种旧式代码:
class Clicked : public Signal<Bool> { ... };除非该信号无法用当前设施表达。
可以在 stub 中用 Array<T> 和 FixedArray<T> 接受 MoonBit 侧对应的数组。但目前无法在 stub 侧构造这两种数组,凡返回线性容器的 MoonBit API 应暂不实现。
stub 的导出符号必须对齐 internal FFI 声明。 也就是说:
- MoonBit internal 有
extern "C" fn _QAbstractButton_setText(...) = "QAbstractButton_setText" - C++ stub 必须有
QMETHOD(QAbstractButton, setText, ...)
不要在 C++ 层随意更名。
无论做何种变更,都应先查询 Qt 的文档。
AI 在新增一个 Qt 类时,应按下面顺序工作:
- 先查清 Qt 文档中的真实继承链。
- 如果中间基类还没绑定,先补中间基类。
- 在
src/...中增加抽象类型type T和 extern 声明。 - 在
src/...中增加 public wrapper。 - 把方法放到“最高但仍正确”的 trait 层级,而不是直接塞给叶子类。
- 在
stub/中实现对应的 C++ 导出函数。 - 如有信号,优先使用
SIGNAL_DEF。 - 如有协变,优先使用
COVARIANT。 - 如该新增能力值得展示,补一个 example。
AI 在给现有类补方法时,应先判断:
-
这个方法是不是属于已有基类?
如果是,应优先加到对应 trait,而不是叶子类。 -
这个方法会不会引入新类型?
如果不会,优先实现这类“低成本、可直接落地”的方法。 -
如果会引入新类型,请暂时不做。
AI 不应做以下事情:
- 跳过 Qt 中间基类,直接把方法堆到叶子类
- 在 public 包里写
extern "C" - 为已存在的统一模式重新发明一套新模式
- 新增 ad-hoc 风格 API,而不是沿用 Qt 命名和现有抽象
- 为了省事把多个层次的方法都绑在单个类上
- 在 stub 中重复手写已有宏能表达的样板
- 为简单 bool/string 等类型转换引入额外 helper 或局部风格
- 在未被用户要求的情况下使用非只读的 Git 功能
完成代码生成后,至少执行:
moon check --target native- 在
examples/下再执行一次moon check --target native
一个好的绑定改动,应同时满足:
- Qt 类层次更完整,而不是更混乱
- public API 更像 Qt,而不是更像 FFI
- 重复样板更少,但抽象没有过度
- 后续 AI 更容易沿着同一模式继续写
如果一个改动只是“局部能跑”,但破坏了以上任何一点,就不应被视为好改动。
如果存在任何问题,一定要询问用户的意见,用户拥有最终决定权。