Python设置GObject派生类属性时Runtime Warning问题排查
背景
我开发了一款基于GLib和GObject的C库,用于心理学实验,目标是生成Python/JavaScript等语言绑定,方便心理学/语言学研究者使用。C语言的性能满足毫秒级精度呈现刺激的需求,GObject Introspection生成的绑定使用体验也不错。
问题现象
在Python中直接通过属性赋值(如circle.props.color = Psy.Color(r=0.5, g=0.5, b=0.5))设置GObject派生类的属性时,会触发Runtime Warning;但使用setter方法(instance.set_property_name(Psy.SomeNewObject()))或先将对象赋值给变量再设置属性则无异常。
测试Python代码
#!/usr/bin/env python import gi import tempfile # All object in Psy.* derive from GObject and not GObject.InitiallyUnowned. # Hence, they cannot have a floating reference. All objects/functions (should) use # transfer annotations, to determine object lifetime, as is currently advised. gi.require_version("Psy", "0.1") from gi.repository import Psy bg_color = Psy.Color(r=0.5, g=0.5, b=0.5) fg_color = Psy.Color(r=1.0, g=0.0, b=0.0) x, y = 0.0, 0.0 radius = 150.0 num_vertices = 100 canvas = Psy.GlCanvas.new(640, 480) circle = Psy.Circle.new_full(canvas, x, y, num_vertices, radius) canvas.set_background_color(bg_color) # 无异常 circle.props.color = fg_color # 无异常,Python持有引用 # 以下代码会触发警告,需要修复! circle.props.color = Psy.Color(r=0.5, g=0.5, b=0.5) circle.props.color = fg_color circle.play_for( canvas.get_time().add(Psy.Duration.new_ms(16)), Psy.Duration.new_ms(50) ) # 绘制圆形50毫秒 canvas.iterate() image = canvas.get_image() image.save_path(tempfile.gettempdir() + "/red-circle.png", "png")
触发的警告信息
sys:1: RuntimeWarning: Expecting to marshal a borrowed reference for
<Psy.Color object at 0x7f9e0ee26a80 (PsyColor at 0x5616651dba00)>, but
nothing in Python is holding a reference to this object.
C库相关实现代码
Circle类继承自PsyVisualStimulus抽象类,以下是PsyVisualStimulus的关键代码:
// header #define PSY_TYPE_VISUAL_STIMULUS psy_visual_stimulus_get_type() G_DECLARE_DERIVABLE_TYPE( PsyVisualStimulus, psy_visual_stimulus, PSY, VISUAL_STIMULUS, PsyStimulus) G_MODULE_EXPORT PsyColor * psy_visual_stimulus_get_color(PsyVisualStimulus *self); G_MODULE_EXPORT void psy_visual_stimulus_set_color(PsyVisualStimulus *self, PsyColor *color); // 实现部分关键代码 G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(PsyVisualStimulus, psy_visual_stimulus, PSY_TYPE_STIMULUS) static void psy_visual_stimulus_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PsyVisualStimulus *self = PSY_VISUAL_STIMULUS(object); switch ((VisualStimulusProperty) property_id) { // 省略其他属性处理 case PROP_COLOR: psy_visual_stimulus_set_color(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } } static void psy_visual_stimulus_class_init(PsyVisualStimulusClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->get_property = psy_visual_stimulus_get_property; object_class->set_property = psy_visual_stimulus_set_property; object_class->dispose = psy_visual_stimulus_dispose; PsyStimulusClass *stimulus_class = PSY_STIMULUS_CLASS(klass); stimulus_class->play = visual_stimulus_play; stimulus_class->set_duration = visual_stimulus_set_duration; klass->update = visual_stimulus_update; // 省略其他属性定义 /** * PsyVisualStimulus:color * * 用于填充对象的`PsyColor`颜色 */ visual_stimulus_properties[PROP_COLOR] = g_param_spec_object("color", "Color", "刺激物的填充颜色", PSY_TYPE_COLOR, G_PARAM_READWRITE); g_object_class_install_properties( object_class, NUM_PROPERTIES, visual_stimulus_properties); } /** * psy_visual_stimulus_get_color: * @self: `PsyVisualStimulus`实例 * * 获取刺激物的颜色 * * Returns:(transfer none): 刺激物的填充`PsyColor`对象 */ PsyColor * psy_visual_stimulus_get_color(PsyVisualStimulus *self) { PsyVisualStimulusPrivate *priv = psy_visual_stimulus_get_instance_private(self); g_return_val_if_fail(PSY_IS_VISUAL_STIMULUS(self), NULL); return priv->color; } /** * psy_visual_stimulus_set_color: * @self: `PsyVisualStimulus`实例 * @color:(transfer none): 用于填充刺激物形状的`PsyColor`实例 * * 设置刺激物的填充颜色 */ void psy_visual_stimulus_set_color(PsyVisualStimulus *self, PsyColor *color) { PsyVisualStimulusPrivate *priv = psy_visual_stimulus_get_instance_private(self); g_return_if_fail(PSY_IS_VISUAL_STIMULUS(self) && PSY_IS_COLOR(color)); g_clear_object(&priv->color); // psy_color_dup创建深拷贝,调用g_object_new而非增加引用计数,这可能存在问题 // PsyColor或许应该改为boxed类型,毕竟结构很简单,但这是另一个问题 priv->color = psy_color_dup(color); }
问题分析与修复方案
警告来自pygobject的pygi-object.c第111行,核心原因是属性的所有权注解与实际逻辑不匹配:
1. 根本原因
- 当前
g_param_spec_object创建的属性默认假设传入的对象是借用引用(borrowed reference),但psy_visual_stimulus_set_color并未持有传入对象的引用,而是创建了深拷贝。 - 当Python直接传递临时对象时,没有Python变量持有该对象的引用,GObject Introspection认为对象会被提前销毁,因此触发警告。
2. 修复方案
方案一:修正所有权注解与实现
- 将
psy_visual_stimulus_set_color的@color注解从(transfer none)改为(transfer full):即使内部做拷贝,也明确告诉绑定层,传入的对象所有权可以被接管,Python侧无需再持有引用。 - 或者,若要保留
(transfer none),需在set_property方法中临时持有传入对象的引用,避免Python侧提前回收:case PROP_COLOR: { PsyColor *color = g_value_get_object(value); g_object_ref(color); psy_visual_stimulus_set_color(self, color); g_object_unref(color); break; }
方案二:将PsyColor改为boxed类型(更优)
既然PsyColor是浅结构,改为boxed类型(类似GdkRGBA)可以从根本上避免引用计数问题:
- 使用
G_DEFINE_BOXED_TYPE定义PsyColor为boxed类型,实现psy_color_copy和psy_color_free方法。 - 将属性定义从
g_param_spec_object改为g_param_spec_boxed。 - 更新get/set方法的注解与实现,适配boxed类型的拷贝逻辑。
临时规避方案
若暂时不想修改C代码,可在Python中避免直接传递临时对象,先将对象赋值给变量再设置属性,或使用setter方法。但这只是规避问题,并非根本解决。
内容的提问来源于stack exchange,提问作者hetepeperfan

