2021年1月31日 星期日

GObject信號機制

GObject參考手冊的“概念/信號”中最關鍵的一句話是:每一個信號在註冊的時候都要與某種數據類型(包括GObject庫中的內建型或GObject子類類型)存在關聯,這種數據類型的用戶需要實現信號與閉包的連接,這樣在信號被發射時,閉包會被調用。這句話,意味著我們要使用GObject信號機制,那麼就必須必須完成兩個步驟:第一個步驟是信號註冊,主要解決信號與數據類型的關聯問題;第二個步驟是信號連接,主要處理信號與閉包的連接問題。考察信號註冊的大致過程。

信號可以與GObject庫的類型管理機制中“可實例化”的數據類型進行關聯,但是GObject參考手冊建議我們最好是只在GObject子類類型中使用信號,因為信號跟類/對像在邏輯上比較相符。

有三個函數可以實現信號註冊,即g_signal_newv,g_signal_new_valist以及g_signal_new,其中g_signal_new_valist與g_signal_new實際上實現了g_signal_newv實際上,但是g_signal_new實際上是最平常的近人的名字。因此,我們可以從分析g_signal_new函數的參數來理解有關信號註冊的一些概念。

g_signal_new函數的聲明如下:

1
2
3
4
5
6
7
8
9
10
guint g_signal_new (const gchar        *signal_name,
                    GType               itype,
                    GSignalFlags        signal_flags,
                    guint               class_offset,
                    GSignalAccumulator  accumulator,
                    gpointer            accu_data,
                    GSignalCMarshaller  c_marshaller,
                    GType               return_type,
                    guint               n_params,
                    ...);

g_signal_new函數的參數設置,其中每個參數多少都有點深不可測的背景,所以直接理解是非常困難的。我們需要實例,從而獲得最直觀的理解。

首先,我們定義一個GObject的子類——SignalDemo類,其頭文件signal-demo.h內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef SIGNAL_DEMO_H
#define SIGNAL_DEMO_H
  
#include <glib-object.h>
  
#define SIGNAL_TYPE_DEMO (signal_demo_get_type ())
#define SIGNAL_DEMO(object) \
        G_TYPE_CHECK_INSTANCE_CAST ((object), SIGNAL_TYPE_DEMO, SignalDemo)
#define SIGNAL_IS_DEMO(object) \
        G_TYPE_CHECK_INSTANCE_TYPE ((object), SIGNAL_TYPE_DEMO))
#define SIGNAL_DEMO_CLASS(klass) \
        (G_TYPE_CHECK_CLASS_CAST ((klass), SIGNAL_TYPE_DEMO, SignalDemoClass))
#define SIGNAL_IS_DEMO_CLASS(klass) \
        (G_TYPE_CHECK_CLASS_TYPE ((klass), SIGNAL_TYPE_DEMO))
#define SIGNAL_DEMO_GET_CLASS(object) (\
                G_TYPE_INSTANCE_GET_CLASS ((object), SIGNAL_TYPE_DEMO, SignalDemoClass))
  
typedef struct _SignalDemo SignalDemo;
struct _SignalDemo {
        GObject parent;
};
  
typedef struct _SignalDemoClass SignalDemoClass;
struct _SignalDemoClass {
        GObjectClass parent_class;
        void (*default_handler) (gpointer instance, const gchar *buffer, gpointer userdata);
};
  
GType signal_demo_get_type (void);
 
#endif

SignalDemo類的源文件signal-demo.c內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "signal-demo.h"
 
G_DEFINE_TYPE (SignalDemo, signal_demo, G_TYPE_OBJECT);
 
static void
signal_demo_default_handler (gpointer instance, const gchar *buffer, gpointer userdata)
{
        g_printf ("Default handler said: %s\n", buffer);
}
 
void
signal_demo_init (SignalDemo *self)
{
}
 
void
signal_demo_class_init (SignalDemoClass *klass)
{
        klass->default_handler = signal_demo_default_handler;
}

基於基線所寫的GObject學習筆記系列,上述代碼不難理解,無非就是定義了一個SignalDemo類,其類結構體中包含了一個函數指針default_handler,並在類結構體初始化函數中使該指針指向函數signal_demo_default_handler 。

下面我們開始為SignalDemo類註冊一個“ hello”信號,只需修改一下SignalDemo類的類結構體初始化函數,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
signal_demo_class_init (SignalDemoClass *klass)
{
        klass->default_handler = signal_demo_default_handler;
 
        g_signal_new ("hello",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (SignalDemoClass, default_handler),
                      NULL,
                      NULL,
                      g_cclosure_marshal_VOID__STRING,
                      G_TYPE_NONE,
                      1,
                      G_TYPE_STRING);
}

此時,觀察一下g_signal_new函數的參數:

  • 第1個參數是字符串“ hello”,它表示信號。
  • 第2個參數是SignalDemo類的類型ID,可以使用G_TYPE_FROM_CLASS宏從SignalDemoClass結構體中獲取,也可以直接使用signal-demo.h中定義的宏SIGNAL_TYPE_DEMO。
  • 第3個參數可暫時略過。
  • 第4個參數比較關鍵,它是一個內存替換量,主要用於從SignalDemoClass結構體中找到default_handler指針的位置,可以使用G_STRUCT_OFFSET宏來獲取,也可以直接根據signal-demo.h中的SignalDemoClass結構體的定義,使用sizeof(GObjectClass)來獲得內存移位量,因為default_handler指針之前只有一個GObjectClass結構體成員。
  • 第5個和第6個參數暫時略過。
  • 第7個參數設定閉包的marshal。在文檔“函數指針,替代函數與GObject閉包”中,描述了GObject的閉包的概念與結構,我們可以將其視為功能+某些環境而構成另外,在那篇文檔中,我們也對marshal的概念進行了一些粗淺的解釋。實際上marshal主要是用來“翻譯”關閉的包的參數和返回值類型的,然後翻譯的結果傳遞給閉包。之所以不直接調用閉包,而是在其外加了一層marshal的包裝,主要是方便GObject庫與其他語言的綁定。例如,我們可以寫一個pyg_closure_marshal_VOID__STRING函數,其中可以調用python語言編寫的“閉包”將其計算結果傳遞給GValue容器,然後再從GValue容器中提取計算結果。
  • 第8個參數指定marshal函數的返回值類型。由於本例的。第7個參數所指定的marshal是g_cclosure_marshal_VOID__STRING函數的返回值是void,而void類型在GObject庫的類型管理系統是G_TYPE_NONE類型。
  • 第9個參數指定g_signal_new函數向marshal函數傳遞的參數個數,通過本例使用的marshal函數是g_cclosure_marshal_VOID__STRING函數,g_signal_new函數只向其傳遞1個參數。
  • 第10個參數是可變參數,其數量由第8個參數決定,用於指定g_signal_new函數向 marshal函數傳遞的參數類型。由於本例使用的marshal函數是g_cclosure_marshal_VOID__STRING函數,並且g_signal_new函數只向其傳遞一個參數,所以該參數的類型為G_TYPE_STRING(GObject庫類型管理系統中的變量類型)。

注意,在上述的g_signal_new函數的第7個參數的解釋中,我提到了閉包。事實上,g_signal_new函數並沒有閉包類型的參數,但是它在內部的確實是建造了一個閉包,而且是通過它的第4個參數實現的。因為g_signal_new函數在其內部調用了g_signal_type_cclosure_new函數,而是處理的工作就是從一個給定的類結構體通過內存分配地址獲得變量指針,然後構造閉包從g_signal_new函數的內部是需要閉包的,那麼它的第7〜10個參數自然都是為那個閉包做準備的。


需要注意,g_cclosure_marshal_VOID__STRING所約定的某些函數類型為:

1
void (*callback) (gpointer instance, const gchar *arg1, gpointer user_data)

這表明g_cclosure_marshal_VOID__STRING需要用戶向其自身的函數變量3個參數,其中前兩個參數是該變量的必要參數,而第3個參數,即userdata,是為用戶留的“後門”,使用者可以通過這個參數變量自己所需要的任意數據。由於GObject閉包約定了變量函數的第1個參數必須是對象本身,所以g_cclosure_marshal_VOID__STRING函數實際上要求用戶向其施加2個參數,但是在本例中g_signal_new只向其傳遞了1個類型為G_TYPE_STRING類型的參數,這有些蹊觸發器。


這是因為g_signal_new函數所隱藏的包只是讓信號所關聯的數據類型能夠有一次可以自我表現的機會,即可以在信號被觸發的時候,能夠自動調用該數據類型的某個方法,例如SignalDemo類結構實際上,SignalDemo類本身是沒有必要向閉包傳遞那個“ userdata”參數的,只是信號的使用者有這種需求。這就是g_signal_new的參數中只表明它向閉包傳遞了1個G_TYPE_STRING類型參數的緣故。


現在總結一下:g_signal_new函數內部所生成的閉包,它在被調用的時候,肯定是被替換了3個參數,它們被信號所關聯的閉包分成了以下層次:


第1個參數是信號的默認閉包(信號註冊階段出現)和信號使用者提供的閉包(信號連接階段出現)所必需的,但是這個參數是隱式存在的,由g_signal_new暗自向閉包傳遞。

第2個參數是顯式的,同時也是信號的封閉包和信號使用者提供的閉包所必須的,這個參數由信號的發射函數(例如g_signal_emit_by_name)向閉包傳遞。

第3個參數也是顯式的,並且僅被信號使用者提供的閉包所關注,這個參數由信號的連接函數(例如g_signal_connect)向閉包傳遞。

若要真正了解上述內容,我們必須去構建SignalDemo類的用戶,即main.c源文件,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "signal-demo.h"
 
static void
my_signal_handler (gpointer *instance, gchar *buffer, gpointer userdata)
{
        g_print ("my_signal_handler said: %s\n", buffer);
        g_print ("my_signal_handler said: %s\n", (gchar *)userdata);
}
 
int
main (void)
{
        g_type_init ();
 
        gchar *userdata = "This is userdata";
        SignalDemo *sd_obj = g_object_new (SIGNAL_TYPE_DEMO, NULL);
 
        /* 信号连接 */
        g_signal_connect (sd_obj, "hello",
                          G_CALLBACK (my_signal_handler),
                          userdata);
 
        /* 发射信号 */
        g_signal_emit_by_name (sd_obj,
                               "hello",
                               "This is the second param",
                               G_TYPE_NONE);
 
        return 0;
}

編譯signal-demo.c與main.c:

1
$ gcc signal-demo.c main.c -o test $(pkg-config --cflags --libs gobject-2.0)

程序運行結果如下:

1
2
3
4
$ ./test
Default handler said: This is the second param
my_signal_handler said: This is the second param
my_signal_handler said: This is userdata

結合程序的運行結果,再回顧一下第1個實例中的那些亂七八糟的內容,現在應該清晰了很多。


現在,我們再來看一下在第1個實例中被我們忽略的g_signal_new函數的第3個參數,我們將其設置為G_SIGNAL_RUN_FIRST。實際上,這個參數是枚舉類型,是信號閉合包的調用階段的標識,可以是下面7種形式中1種,也可以是多種組合。

1
2
3
4
5
6
7
8
9
10
typedef enum
{
  G_SIGNAL_RUN_FIRST = 1 << 0,
  G_SIGNAL_RUN_LAST = 1 << 1,
  G_SIGNAL_RUN_CLEANUP = 1 << 2,
  G_SIGNAL_NO_RECURSE = 1 << 3,
  G_SIGNAL_DETAILED = 1 << 4,
  G_SIGNAL_ACTION = 1 << 5,
  G_SIGNAL_NO_HOOKS = 1 << 6
} GSignalFlags;

這個參數被設為G_SIGNAL_RUN_FIRST,表示信號的替換閉包要先於信號使用者的閉包被調用,這個觀察一下上面的測試程序的輸出結果替換知悉。如果我們將這個參數設置為G_SIGNAL_RUN_LAST,則表示對於此參數的理解暫且到此為止,後面在稱為信號連接的時候再次再次變為它。


REF

http://garfileo.is-programmer.com/2011/3/25/gobject-signal-extra-1.25576.html

沒有留言:

張貼留言