3、GObject基础:GObject系统简介、类型系统、信号与槽、属性系统,为插件开发打基础

好,咱们进入正题。GObject 这套东西,说白了就是 GLib 给 C 语言穿上的一件「面向对象外衣」。你可能会问:C 语言本来没有 class、没有继承,GStreamer 为什么非要搞这么一套?

我刚开始接触 GStreamer 时也有这个疑惑。后来写了一个插件,发现要处理各种媒体格式、要动态创建元素、要响应状态变化……嗯,没有 GObject 这套基础设施,代码会乱成一锅粥。GStreamer 里几乎所有的东西——元素、垫(Pad)、总线(Bus)、缓冲区(Buffer)——都是 GObject 的子类。

3.1 GObject 系统简介

GObject 是 GLib 对象系统的核心。它提供了三个基本能力:

  • 类型系统:运行时类型识别、继承、接口
  • 信号与槽:对象间通信机制,类似 Qt 的信号槽
  • 属性系统:对象的可配置参数,支持 get/set、通知

说白了,GObject 就是用 C 语言模拟了面向对象语言的三大特性:封装、继承、多态。你想想看,C 语言里没有 class 关键字,GObject 通过宏和约定来实现这些。

核心概念:GObject 的每个对象实例都包含一个 GObjectClass 结构体,这个结构体里存放的是「虚函数表」。子类可以覆盖父类的方法,实现多态。

3.2 类型系统:一切从 GType 开始

GType 是 GObject 类型系统的基石。每个 GObject 子类都有一个唯一的 GType ID,运行时通过这个 ID 来识别对象类型。

我记得第一次看 GStreamer 源码时,被各种 G_TYPE_* 宏搞得头晕。后来才明白,这些宏就是类型注册的入口。

看一个最简单的例子:

#include <glib-object.h>

// 定义一个结构体
typedef struct {
    GObject parent;
    gint my_data;
} MyObject;

// 定义对应的类结构体
typedef struct {
    GObjectClass parent_class;
} MyObjectClass;

// 类型注册宏
#define MY_TYPE_OBJECT (my_object_get_type())

// 类型注册函数
GType my_object_get_type(void) {
    static GType type = 0;
    if (type == 0) {
        static const GTypeInfo info = {
            sizeof(MyObjectClass),
            NULL,   // base_init
            NULL,   // base_finalize
            NULL,   // class_init
            NULL,   // class_finalize
            NULL,   // instance_init
            sizeof(MyObject),
            0,      // n_preallocs
            NULL    // instance_init
        };
        type = g_type_register_static(G_TYPE_OBJECT, "MyObject", &info, 0);
    }
    return type;
}

这段代码做了三件事:

  1. 定义实例结构体 MyObject 和类结构体 MyObjectClass
  2. 实现 my_object_get_type() 函数,这是类型注册的入口
  3. 调用 g_type_register_static() 注册新类型

个人经验:我习惯把类型注册函数放在一个单独的头文件里,用宏 G_DECLARE_FINAL_TYPEG_DECLARE_DERIVABLE_TYPE 来简化声明。GStreamer 插件开发中,99% 的情况用 G_DECLARE_FINAL_TYPE 就够了,因为插件元素通常不需要被继承。

3.3 信号与槽:对象间的「悄悄话」

信号与槽是 GObject 最强大的特性之一。它让对象之间可以解耦通信——发送者不需要知道接收者是谁,接收者也不需要轮询。

在 GStreamer 里,信号无处不在。比如 pad-added 信号,当动态 Pad 被创建时触发;state-changed 信号,当元素状态变化时触发。

定义一个信号需要三步:

// 1. 在 class_init 中注册信号
enum {
    SIGNAL_MY_SIGNAL,
    LAST_SIGNAL
};

static guint my_object_signals[LAST_SIGNAL] = { 0 };

static void
my_object_class_init(MyObjectClass *klass) {
    my_object_signals[SIGNAL_MY_SIGNAL] =
        g_signal_new("my-signal",
                     MY_TYPE_OBJECT,
                     G_SIGNAL_RUN_LAST,
                     0,
                     NULL, NULL,
                     g_cclosure_marshal_VOID__INT,
                     G_TYPE_NONE,
                     1,
                     G_TYPE_INT);
}

// 2. 发射信号
void
my_object_emit_signal(MyObject *self, gint value) {
    g_signal_emit(self, my_object_signals[SIGNAL_MY_SIGNAL], 0, value);
}

// 3. 连接信号(在用户代码中)
g_signal_connect(my_object, "my-signal",
                 G_CALLBACK(on_my_signal_received), user_data);

这里有个坑,我曾经踩过:g_signal_new 的最后一个参数是参数类型列表,必须用 G_TYPE_* 宏指定。如果参数类型写错了,运行时不会报错,但回调函数收到的数据会是乱码。

避坑指南:我曾经在写一个音频处理插件时,把 G_TYPE_INT 写成了 G_TYPE_UINT,结果信号参数一直传不对。调试了整整一个下午才发现是类型不匹配。记住:信号参数类型必须和回调函数签名严格一致。

3.4 属性系统:对象的「可调旋钮」

属性系统让 GObject 对象有了可配置的参数。在 GStreamer 里,每个元素都有属性,比如 videotestsrcpattern 属性、capsfiltercaps 属性。

属性系统的核心是 GParamSpec,它描述了属性的元数据:类型、取值范围、默认值、读写权限等。

实现属性系统需要四个步骤:

// 1. 在 class_init 中安装属性
enum {
    PROP_0,
    PROP_MY_DATA,
    PROP_LAST
};

static GParamSpec *obj_properties[PROP_LAST] = { NULL };

static void
my_object_class_init(MyObjectClass *klass) {
    obj_properties[PROP_MY_DATA] =
        g_param_spec_int("my-data",
                         "My Data",
                         "The data value",
                         0,    // 最小值
                         100,  // 最大值
                         50,   // 默认值
                         G_PARAM_READWRITE);
    
    g_object_class_install_properties(
        G_OBJECT_CLASS(klass), PROP_LAST, obj_properties);
}

// 2. 实现 get_property
static void
my_object_get_property(GObject *object,
                       guint property_id,
                       GValue *value,
                       GParamSpec *pspec) {
    MyObject *self = MY_OBJECT(object);
    
    switch (property_id) {
        case PROP_MY_DATA:
            g_value_set_int(value, self->my_data);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

// 3. 实现 set_property
static void
my_object_set_property(GObject *object,
                       guint property_id,
                       const GValue *value,
                       GParamSpec *pspec) {
    MyObject *self = MY_OBJECT(object);
    
    switch (property_id) {
        case PROP_MY_DATA:
            self->my_data = g_value_get_int(value);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

// 4. 在 class_init 中覆盖 get/set 方法
static void
my_object_class_init(MyObjectClass *klass) {
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    gobject_class->get_property = my_object_get_property;
    gobject_class->set_property = my_object_set_property;
    // ... 安装属性代码 ...
}

属性系统还有一个很实用的特性:属性通知。当你通过 g_object_set() 修改属性时,GObject 会自动发射 notify::property-name 信号。这样其他代码可以监听属性变化,而不需要轮询。

我的习惯:在 GStreamer 插件开发中,我会把所有可配置的参数都做成属性。这样用户可以通过 gst-launch-1.0 命令行直接设置,也可以通过 g_object_set() 在代码中设置。而且属性系统自动提供了参数验证和通知机制,比自己手写 getter/setter 省事多了。

3.5 三者的协作关系

类型系统、信号与槽、属性系统,这三者不是孤立的。它们共同构成了 GObject 的完整生态:

组件 作用 在 GStreamer 中的体现
类型系统 运行时类型识别、继承 GstElement、GstPad 等类型的注册
信号与槽 对象间通信 pad-added、state-changed 等事件
属性系统 对象参数配置 元素的 caps、bitrate 等参数

举个例子:当你在 GStreamer 中创建一个 videotestsrc 元素时,GObject 类型系统会分配内存并初始化实例;当你设置 pattern 属性时,属性系统会验证参数并更新内部状态;当属性变化时,信号系统会通知监听者。

嗯,这里要注意:GObject 的引用计数机制也很重要。每个 GObject 对象都有一个引用计数,当计数归零时自动销毁。GStreamer 里大量使用 gst_object_ref()gst_object_unref() 来管理对象生命周期。如果你忘记 unref,就会造成内存泄漏——我早期写插件时就犯过这个错,一个晚上都在追查内存泄漏的源头。

好了,GObject 的基础就讲到这里。下一章我们会把这些知识用到实际的 GStreamer 插件开发中,到时候你会看到这些概念是如何落地的。