Qt Webengine回调函数内赋值引发Segment Fault的灵异事件

今晚想在Qt程序中调用一个JS库来做表单生产,结果是核心功能二十分钟就调通了,反而是被一句无关紧要的代码坑了两个小时。

事情是这样的,程序中嵌入了一个QWebEngineView,其中加载了已经调试好了的网页以及相应的JS,仅需要执行一句输出结果用的JS:

JSON.stringify(this.getValue(), null, 4);

即可返回我想要的一个以'Json'格式写成的配置文件。

看起来很简单,仅有的技术点在于如何把JS世界生成的东西传递到C++这边来,官方文档连例子都写好了。于是随手就写了:

void ConfigEditorMainWindow::on_btn_StartComputing_clicked()
{
    const QString js_BuildResultingJson("JSON.stringify($(\"#form_field\").alpaca(\"get\").getValue(), null, 4)");
    QString jsr_BuildResultingJson;
    auto cb = [&jsr_BuildResultingJson](const QVariant &v) {
        if (v.canConvert(QVariant::String)) {
            jsr_BuildResultingJson = v.toString();
        }
    };
    ui->webEngineView->page()->runJavaScript(js_BuildResultingJson, cb);
    ui->label_LeftUppper->setText(QString::fromStdString(jsr_BuildResultingJson));
}

jsr_BuildResultingJson用来暂存结果,ui->label_LeftUppper是临时放上去的控件,方便看。最后还是打算把数据从jsr_BuildResultingJson里面把数据传递出去。点击运行,点击StartComputing按钮——程序马上Segment Fault崩溃退出了,开调试器都看不到什么有用信息。

其实开调试器时候,我已经在lambda函数内`jsr_BuildResultingJson = v.toString();`看到了第一个错误征兆:`jsr_BuildResultingJson`变量显示为“无法访问”。只道当时是寻常。

随随便便一个字符串赋值为啥会引起段错误?猜测了很多原因,包括v.toString()赋值出来的QString是否是与v共享内存啦,用std::string会不会就好啦,是不是不应该先检查v.canConvert()啦,等等,全部验证为不对。

过程中也用了其他的测试手段,比如在lambda函数内直接qDebug()<<输出,在lambda函数内直接设置label上的字等等。结果很奇怪,这些输出手段都是好的,程序没有崩,输出也正确。最后甚至得出了这样的结论:只要不动jsr_BuildResultingJson变量,啥都好使。

这时候我已经隐约猜到是局部变量的问题,感觉到设置个类成员变量在存放结果肯定没有问题——但是没有实际测,因为找不到不能用局部变量的原因肯定是睡不着觉的,还不如一根筋到底。

所有猜测用完之后,又去仔细看了看一遍文档,发现有一段警告:

Warning: We guarantee that the callback (resultCallback) is always called, but it might be done during page destruction. When QWebEnginePage is deleted, the callback is triggered with an invalid value and it is not safe to use the corresponding QWebEnginePage or QWebEngineView instance inside it.

警告本身与这个错误没有关系,但它提醒了我一点:这个lambda函数是一个回调函数,其调用时间是不固定的!不是随着代码同步执行的!

也就是说,执行到ui->webEngineView->page()->runJavaScript(js_BuildResultingJson, cb);这一句的时候,程序并不是等着js_BuildResultingJson中的JS命令执行完了再去调用cb,而是先继续往下走,等到Webengine跑完了JS,再回过头来调用cb——然而,这时候整个on_btn_StartComputing_clicked()函数都已经跑到头了,lambda函数cb所捕获的jsr_BuildResultingJson变量都已经被销毁掉了,哪里还有地方给你赋值用?不崩溃才有鬼了。

这同时也解释了调试过程中看到的几个当时不觉得很重要的灵异现象:

  • 上面说过的,开调试器看不到jsr_BuildResultingJson的内容——不是空,是无法访问
  • 点击按钮时,有小概率程序不崩溃,只是ui->label_LeftUppper的内容变空了——如果某一次Webengine线程跑得快,可能调用cbjsr_BuildResultingJson还没被析构掉,但执行“设置标签内容”这一句的时候肯定还没跑完,毕竟是C++跟JS的对比
  • 直接输出或者直接向ui控件赋值都没问题——因为这些在执行cb函数时都是确定存在的,没什么幺蛾子

最后,新建了一个成员变量,用来代替jsr_BuildResultingJson,问题完美解决。

结论:lambda函数好用,但如果你是拿来当回调函数,千万不要在其中捕获、使用局部变量。

  • 最后更改: 2019/07/15 16:04