信号和槽是如何在底层实现的?

24

这个问题已经在这个论坛中被问过,但我不理解这个概念。

我读了一些资料,似乎信号和槽使用函数指针来实现,也就是说信号是一个大的函数,在其中调用所有连接的槽(函数指针)。这是正确的吗?生成的 moc 文件在整个过程中扮演着什么角色?我不明白信号函数如何知道要调用哪些槽,即哪些槽与此信号相连。

感谢您的时间


好问题。另请参阅:http://stackoverflow.com/questions/1413777/how-boost-implements-signals-and-slots - elcuco
2个回答

16

Qt以类似解释型语言的方式实现这些功能。也就是说,它构建符号表来映射信号名称到函数指针,并在需要时维护并通过函数名查找函数指针。

每次发出信号(即写入

emit something();

实际上,你调用了something()函数,它是由元对象编译器自动生成并放置在一个*.moc文件中。在这个函数内部,检查了此信号当前连接到哪些槽,并通过符号表(如上所述)顺序调用适当的槽函数(这些函数是你在自己的源代码中实现的)。而像emit这样的Qt特定关键字,在生成*.moc文件之后被C++预处理器丢弃。实际上,在Qt头文件之一qobjectdefs.h中存在这样的代码行:

#define slots 
#define signals protected
#define emit

connect函数只是修改*.moc文件中维护的符号表,并且传递给它的参数(使用SIGNAL()SLOT宏)也经过预处理以匹配这些表。

这是总体思路。在他或她的另一个回答中,ジョージ向我们提供了链接,指向trolltech邮件列表和关于这个主题的另一个SO问题


4
基本上正确,你可以在发射处设置断点,然后逐步执行信号传递过程。虽然信号通常是直接传递的,但有时需要排队,特别是当你想连接两个不同线程中的对象时。 - Harald Scheirich

5
我认为我应该添加以下内容。
另一个相关的问题 - 以及一篇非常好的文章,可以被视为对它的回答进行相当详细的扩展; 这篇文章在这里, 代码语法高亮得到改进(虽然仍不完美)。
这是我简短的转述,可能会出现错误)
基本上,当我们在类定义中插入Q_OBJECT宏时,预处理器将其扩展为静态QMetaObject实例声明,这个声明将被同一类的所有实例共享:
class ClassName : public QObject // our class definition
{
    static const QMetaObject staticMetaObject; // <--= Q_OBJECT results to this

    // ... signal and slots definitions, other stuff ...

}

在初始化时,该实例将存储信号和插槽的签名("methodname(argtype1,argtype2)"),这将允许实现indexOfMethod()调用,它通过方法的签名字符串返回方法的索引:
struct Q_CORE_EXPORT QMetaObject
{    
    // ... skip ...
    int indexOfMethod(const char *method) const;
    // ... skip ...
    static void activate(QObject *sender, int signal_index, void **argv);
    // ... skip ...
    struct { // private data
        const QMetaObject *superdata; // links to the parent class, I guess
        const char *stringdata; // basically, "string1\0string2\0..." that contains signatures and other names 
        const uint *data; // the indices for the strings in stringdata and other stuff (e.g. flags)
        // skip
    } d;
};

现在,当moc为Qt类头文件headername.h创建moc_headername.cpp文件时,它会在那里放置签名字符串和其他数据以便正确初始化d结构,并使用此数据编写staticMetaObject单例的初始化代码。另一个重要的事情是生成对象的qt_metacall()方法的代码,该方法获取对象的方法ID和参数指针数组,并通过长的switch调用该方法。
int ClassName::qt_metacall(..., int _id, void **_args)
{
    // ... skip ...
    switch (_id) {
        case 0: signalOrSlotMethod1(_args[1], _args[2]); break; // for a method with two args
        case 1: signalOrSlotMethod2(_args[1]); break; // for a method with a single argument
        // ... etc ...
    }
    // ... skip ...
}

最后,对于每个信号,moc 会生成一个实现,其中包含一个 QMetaObject::activate() 调用:
void ClassName::signalName(argtype1 arg1, argtype2 arg2, /* ... */)
{
    void *_args[] = { 0, // this entry stands for the return value
                      &arg1, // actually, there's a (void*) type conversion
                      &arg2, // in the C++ style
                      // ...
                    };
    QMetaObject::activate( this, 
                           &staticMetaObject, 
                           0, /* this is the signal index in the qt_metacall() map, I suppose */ 
                           _args
                         );
}

最后,connect() 调用将字符串方法签名转换为它们的整数 id(由 qt_metacall() 使用的 id),并维护信号与槽连接的列表;当发出信号时,activate() 代码会遍历此列表,并通过其 qt_metacall() 方法调用适当的对象“槽”。

总之,静态的 QMetaObject 实例存储“元信息”(方法签名字符串等),生成的 qt_metacall() 方法提供了“方法表”,允许通过索引调用任何信号/槽,由 moc 生成的信号实现使用这些索引通过 activate(),最后 connect() 执行维护信号到槽索引映射列表的工作。

*注意:当我们想在不同线程之间传递信号时,这种方案会有一些复杂性(我怀疑必须查看 blocking_activate() 代码),但我希望总体思想保持不变)

这是我对链接文章的非常粗略的理解,可能是错误的,所以我建议直接去阅读它)

附注:由于我想提高自己对Qt实现的理解,请让我知道我叙述中任何不一致的地方!


由于我的另一个(早期的)答案被一些热心的编辑删除了,我将在此附上文本(我错过了一些细节,这些细节没有在Pavel Shved的帖子中包含,而我怀疑删除答案的人并不关心。)

@Pavel Shved:

我非常确定在Qt头文件中的某个地方存在一行:

#define emit

只是为了确认:通过Google Code Search在旧的Qt代码中找到了它。很可能它仍然存在); 找到的位置路径是:

ftp://ftp.slackware-brasil.com.br› slackware-7.1› contrib› kde-1.90› qt-2.1.1.tgz› usr› lib› qt-2.1.1› src› kernel› qobjectdefs.h


另一个补充链接:http://lists.trolltech.com/qt-interest/2007-05/thread00691-0.html -- 参见 Andreas Pakulat 的回答。


以下是答案的另一部分: Qt问题:信号和槽如何工作?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接