• 8012阅读
  • 7回复

Jisong's Qt Internal - Signal Sailing <让信号也扬帆远航> [复制链接]

上一主题 下一主题
离线beajisong
 

只看楼主 倒序阅读 楼主  发表于: 2010-05-06
Jisong's Qt Internal - Signal Sailing <让信号也扬帆远航>
— 本帖被 XChinux 设置为精华(2010-05-06) —
最近离职交接期,我那位交接人还请了个假,闲着无聊,就和大家分享一下QtCore的一些经验吧。

Qt有个最个性的模式,信号与槽。很方便很方便,但是有个局限。他只提供了本地的信号连接到本地的槽(本地指进程内部)。我们写软件有时会写一些监视软件,监视软件顾名思义,就是要见识谁在干什么。但你怎么知道他在干什么呢?有很多人实现成了polling,其他人实现成了事件接收。
试想,如果Qt的信号可以远程传播,那么我们就可以把第二种方法进一步的拓展一下。如果服务器有什么事件发生,他就可以把他转换成信号,客户端想知道什么信号就连接什么信号。这样比事件是不是要简单一些?

那么,这个东西怎么做比较好呢?这个问题先不说,作为理想主义者,我们可以想想,如果这个小API做出来了,得给别人怎么用。
1. 获取信号,我需要个捕捉器,可捕捉任意一个或者任意一个QObject的信号,或者多个。
2. 传递信号是通过API内部完成,你不用管怎么传递。
3. 传到了客户端了怎么弄?我想我就给他弄个基类,你就继承,继承了以后你需要得知什么信号就给我声明出来个一样的,不需要就别声明,多余的我不告诉你。这个东西叫信号代理吧。
4. 运行时,客户端的信号代理链接到服务器的信号收集器就行,其他均为自动。

好,出于此,我们逐个解决问题。
首先,我们如何捕捉信号?
信号可以通过一个槽来捕捉,但问题来了,我需要捕捉任何信号,N种组合,我还想把正确的参数内容拿下,这个怎么弄?别急,以前我们做不到,4.6了我们就可以了。但是,其实这个也是个歪招,我们需要使用QStateMachine机制。这个东西本来是做状态机用的,但是他提供了一个东西叫QSignalTransition,这个东西实现了以后可以在信号发出时执行一些自动以动作。

bool GSignalSpy::eventTest(QEvent *event)
{
//    GFDebug() ;
    if (!QSignalTransition::eventTest(event)) {
        return false;
    }
    QStateMachine::SignalEvent *signalEvent
            = static_cast<QStateMachine::SignalEvent*>(event);

    GDebug(QString("SIGNAL[%1] with ARG(s)[")
           .arg(signalEvent->sender()->metaObject()->method(signalEvent->signalIndex()).signature())
           << signalEvent->arguments() << "] captured."
           << signalEvent->arguments().value(0)) ;

    GObjectSignalMethod method(this, SIGNAL(signalEmitted(QByteArray,QVariantList))) ;
    method.invoke(Qt::QueuedConnection,
                  Q_ARG(QByteArray, QByteArray(signalEvent->sender()->metaObject()->method(signalEvent->signalIndex()).signature())),
                  Q_ARG(QVariantList, signalEvent->arguments())) ;

    return true ;
}
看,这个东西可以从event事件对象里提出信号的signature与参数值列表。那我就通过这个再发射了一个自己的信号叫signalEmitted,正好包括了这两个参数。这样信号就被感知了,而且是任何形式的信号。
当然,为了启动这个过程我们还需要这样做。
    QStateMachine *m = new QStateMachine(this) ;
    QState *s = new QState(m) ;
    s->addTransition(spy);
    m->setInitialState(s);

    connect(spy, SIGNAL(signalEmitted(QByteArray,QVariantList)),
            this, SLOT(propagateSignal(QByteArray,QVariantList))) ;

    m->start();
这样捕捉就开始了。
还有个小问题,怎么捕捉某个对象的所有信号呢?
这个很简单,我们只需要获取每个信号的signature,然后用上面的方法就行。信号的sig就在metaObject里面。
int count =object->metaObject()->methodCount() ;
    int offset = GSignalSpyHelper().metaObject()->methodOffset() ;

    QState *state = new QState(&this->m_StateMachine);

    for(int i = offset ; i < count ; i++) {
        QMetaMethod method = object->metaObject()->method(i) ;
        if(method.methodType() != QMetaMethod::Signal) {
            continue ;
        }
    。。。。
这就OK了。
捕捉完了,我们需要将信号序列化成网络数据,并传播出去,这个也是有一定学问的。
首先,我们信号包含的sig是肯定可以序列化的,那除了sig,参数列表怎么办?我们得将他们序列化才行,而且不是每个都是内置的可序列化类型。这个只能靠你或者你代码的使用者向他们需要传递的参数类型都提供序列化API了。Qt文档中提到如下:
void qRegisterMetaTypeStreamOperators ( const char * typeName )

Registers the stream operators for the type T called typeName.

Afterward, the type can be streamed using QMetaType::load() and QMetaType::save(). These functions are used when streaming a QVariant.

qRegisterMetaTypeStreamOperators<MyClass>("MyClass");
The stream operators should have the following signatures:

QDataStream &operator<<(QDataStream &out, const MyClass &myObj);
QDataStream &operator>>(QDataStream &in, MyClass &myObj);
下面是序列化的代码,这个没什么特别。
    QByteArray data ;

    QDataStream stream(&data, QIODevice::WriteOnly) ;
    stream.setVersion(QDataStream::Qt_4_6);
    stream.setByteOrder(QDataStream::LittleEndian);

    stream << qint32(varList.count()) ;
    stream << signature ;
    foreach(QVariant v, varList) {
        stream << qint32(v.userType()) << v ;
    }
需要注意的是,你需要将类型码qint32(v.userType())也序列化到数据中,这样你在大洋彼岸也好有个参照,号给他反序列化了。这个在后面会很重要。好吧,这里估计也该提了,这些东西都是为了预防用户自定义类型的数据进行序列化及信号还原时出问题而设计的。
这里有点混乱,但你先凑合听着,慢慢也就能知道了(慢慢没能,别怪我哦)。在对于用户数据来说,我发现我们必须得给他由QVariant存储到一个QByteArray或者一个内存缓存中,然后才能给他用void*指针传给Qt核心实现来激发自定义参数类型的信号,我也没搞明白为什么,但是,实践证明必须这么做。那我就需要一套函数来达到qRegisterMetaTypeStreamOperators<MyClass>("MyClass");类似的效果,下面就是这些函数。
template<typename T>
inline QByteArray gStoreUserType(const T& t)
{
    return QByteArray((const char*)&t, sizeof(T)) ;
}


template<typename T>
QByteArray gStoreVariant(const QVariant& var)
{
    GFDebug() ;
    if(!var.canConvert<T>()) {
        return QByteArray() ;
    }

    GDebug("type" << var.userType() << "value" << var.value<T>()) ;
    T t = var.value<T>() ;
    return gStoreUserType(t) ;
}

typedef QByteArray (*GStoreVariantFunc) (const QVariant& var) ;


template<typename T>
bool gRegisterEventPropagatorMetaType(const char* name)
{
    Q_ASSERT(name != 0) ;

    if(QVariant::Invalid == QVariant::nameToType(name)) {
        qFatal("GEventPropagator: Type[%s] must registered to QMetaType System first!",
               name) ;
    }

    QVariant var ;
    var.setValue(T());

    return gRegisterEventPropagatorMetaTypeStoreOp(var.userType(), gStoreVariant<T>) ;
}

static QHash<int, GStoreVariantFunc> gEventPropagatorMetaTypeOpMap ;

bool gRegisterEventPropagatorMetaTypeStoreOp(int type, GStoreVariantFunc func)
{
    G_ASSERT(type != 0) ;
    G_ASSERT(func != 0) ;

    if(gEventPropagatorMetaTypeOpMap.contains(type)) {
        GWarning("GEventPropagator: Already registered MetaTypeOp before!") ;
        return false ;
    }

    gEventPropagatorMetaTypeOpMap.insert(type, func) ;

    return true ;
}

bool gIsRegisterEventPropagatorMetaTypeStoreOp(int type)
{
    return gEventPropagatorMetaTypeOpMap.contains(type) ;
}

GStoreVariantFunc gGetEventPropagatorMetaTypeStoreOp(int type)
{
    if(!gEventPropagatorMetaTypeOpMap.contains(type)) {
        GWarning(QString("GEventPropagator: There is no MetaTypeOp for type[%1]!")
                 .arg(type));
        return 0 ;
    } else {
        return gEventPropagatorMetaTypeOpMap.value(type) ;
    }

}
这些模板就是用来将自定义的QVariant存储到一片QByteArray中的。注释就不写了,大家自己看吧,很会意吧?哈哈

好了好了,最关键的问题,数据到手了,准备好了,怎么动态激发一个信号呢?
按我们最原先的设想,需要继承一个基类,声明所有你想看到的远方信号就行。好吧,那么得弄个基类了。这个基类应该能发现继承他的子类都有啥子信号嘛!
    int index = this->metaObject()->indexOfSignal(signature) ;
    if(-1 == index) {
        GWarning(QString("GEventPropagatorClient: There is no signal with signature[%1]!")
                 .arg(signature.data())) ;
        return ;
    }
OK,通过这个代码,信号的index你是搞到手了,准备激发吧?这个怎么弄?
你编译了一个QObject子类,并带有自定义信号或者槽后,你可以看看moc_{你的文件名}.moc这个文件,这个是moc编译器生成的源对象系统帮助代码。是不是有一些信息。这个不是特别重要。你再继续,看QMetaObject类的代码,看看是不是有个activate方法?虽然是public,而且你也能用,但Qt文档里面就没告诉你。为什么?以后他想改就改,不影响什么,所以你需要付出未来有可能不兼容的代价。但我估计,一段时间内,这个东西还得存在之。所以,用了吧!
看看函数需要什么?一个活物对象,一个metaObject,一个索引号,一个void**???
有点蒙,没关系,我也蒙来着,后来分析了好一阵,这现在你就不用蒙了,我来告诉你。他是激发一个活物的一个元方法(信号,槽,构造等等),并根据他的metaObject,这个索引就是指向这个元方法的索引号,void**当然就是参数列表指针数组喽~~索引可以通过methodIndex和methodOffset想减得来,参数列表略显麻烦。
首先定义:
void* vars[13] = {0} ;
为什么是13?返回值一个,参数最多带10个,后面两个00结尾喽。(00一说完全靠猜)
顺序和我猜测是一样的,所以第0个,你别动,就是保持0。
后面你就遍历远洋过来的参数值列表,如果非用户自定义就直接取QVariant的地址,像这样。
vars[i + 1] = reinterpret_cast<void*>(&varList) ;
反过来是用户自定义的话,就得麻烦些,先给var存到内存某处,在取首地址。我上面的几个模板已经搞定了这个麻烦事情啦~
bufferList.append(gGetEventPropagatorMetaTypeStoreOp(type)(varList));
vars[i + 1] = reinterpret_cast<void*>(bufferList[bufferList.count() - 1].data()) ;
注意哦,调用完成之前,你这个buffer别没了,那样就不安全了。
OK,最后调用它~
this->metaObject()->activate(this, this->metaObject(), index-offset, vars);

好了,就这么多。网络传输方面我没讲,原因是,我实现的也挺繁琐的,而且蛮简单的东西,没什么好讲,这个完全你自定义就OK了~还有,在最后,这个是我辛辛苦苦分析了2 3天的结果呢,所以大家如果想分享,这个我很支持,但请保留原来的题目和作者。
我是苍松,很多时也写英文Jisong,欢迎大家交流,我就在Qt的群里窝着。

各位,下次见吧,看看有啥值得分享的我会再写的。
离线xtfllbl

只看该作者 1楼 发表于: 2010-05-06
忍不住了,看到这么好的帖子出现,不支持一下是不行的,非常感谢楼主分享。
至于内容,以我的水平还需要慢慢消化,不是看一边就OK的。
上海欢迎您
离线banyibanyi

只看该作者 2楼 发表于: 2010-05-06
慢慢消化 慢慢看 支持一个
离线beajisong

只看该作者 3楼 发表于: 2010-05-11
自顶
离线xtfllbl

只看该作者 4楼 发表于: 2010-05-11
引用第3楼beajisong于2010-05-11 13:48发表的  :
自顶

在这里不用顶,好东西总会被人挖出来的。
上海欢迎您
离线luoyes

只看该作者 5楼 发表于: 2010-05-12


离线dongsir
只看该作者 6楼 发表于: 2010-11-17
这个必须支持
离线luoyes

只看该作者 7楼 发表于: 2010-11-22
标记一下
慢慢消化
快速回复
限100 字节
 
上一个 下一个