Revision control
Copy as Markdown
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/QtCore>
#include <QtTest/QtTest>
#include "global.h"
#ifdef QT_NAMESPACE
#define STRINGIFY_HELPER(s) #s
#define STRINGIFY(s) STRINGIFY_HELPER(s)
QString ns = STRINGIFY(QT_NAMESPACE) + QString("::");
#else
QString ns;
#endif
class tst_Symbols: public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void globalObjects();
#ifndef QT_CROSS_COMPILED
void prefix();
#endif
private:
QString qtModuleDir;
QString qtLibDir;
QHash<QString, QString> modules;
QStringList keys;
};
void tst_Symbols::initTestCase()
{
qtModuleDir = QString::fromLocal8Bit(qgetenv("QT_MODULE_TO_TEST"));
if (qtModuleDir.isEmpty()) {
QSKIP("$QT_MODULE_TO_TEST is unset - nothing to test. Set QT_MODULE_TO_TEST to the path "
"of a Qt module to test.");
}
QString configFile = qtModuleDir + "/tests/global/global.cfg";
if (!QFile(configFile).exists()) {
QSKIP(
qPrintable(QString(
"%1 does not exist. Create it if you want to run this test."
).arg(configFile))
);
}
QString workDir = qtModuleDir + QStringLiteral("/tests/global");
modules = qt_tests_shared_global_get_modules(workDir, configFile);
if (!modules.size())
QSKIP("No modules found.");
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qtLibDir = QLibraryInfo::path( QLibraryInfo::LibrariesPath );
#else
qtLibDir = QLibraryInfo::location( QLibraryInfo::LibrariesPath );
#endif
QFileInfo qtLibDirInfo(qtLibDir);
QVERIFY2(qtLibDirInfo.isDir(), qPrintable(
QString("QLibraryInfo::LibrariesPath `%1' %2\nIs your build complete and installed?")
.arg(qtLibDir)
.arg(!qtLibDirInfo.exists() ? "doesn't exist" : "isn't a directory")
));
keys = modules.keys();
QList<QString>::iterator i;
for (i = keys.begin(); i != keys.end(); ++i)
*i = "lib" + *i + ".so";
qDebug() << qtLibDir << keys;
}
/* Computes the line number from a symbol */
static QString symbolToLine(const QString &symbol, const QString &lib)
{
// nm outputs the symbol name, the type, the address and the size
QRegularExpression re("global constructors keyed to ([a-zA-Z_0-9.]*) (.) ([0-f]*) ([0-f]*)");
QRegularExpressionMatch match = re.match(symbol);
if (!match.hasMatch())
return QString();
// address and symbolSize are in hex. Convert to integers
bool ok;
int symbolAddress = match.captured(3).toInt(&ok, 16);
if (!ok)
return QString();
int symbolSize = match.captured(4).toInt(&ok, 16);
if (!ok)
return QString();
// now, find the start address, which is the address - size
QString startAddress = QString::number(symbolAddress - symbolSize, 16);
QProcess proc;
proc.start("addr2line", QStringList() << "-e" << lib << startAddress);
if (!proc.waitForFinished())
return QString();
QString result = QString::fromLocal8Bit(proc.readLine());
result.chop(1); // chop tailing newline
return result;
}
/* This test searches through all Qt libraries and searches for symbols
starting with "global constructors keyed to "
These indicate static global objects, which should not be used in shared
libraries - use Q_GLOBAL_STATIC instead.
*/
void tst_Symbols::globalObjects()
{
// these are regexps for global objects that are allowed in Qt
const QRegularExpression whitelist[] = {
// ignore qInitResources - they are safe to use
QRegularExpression("^_Z[0-9]*qInitResources_"),
QRegularExpression("qrc_.*\\.cpp"),
// ignore qRegisterGuiVariant - it's a safe fallback to register GUI Variants
QRegularExpression("qRegisterGuiVariant"),
};
bool isFailed = false;
QDir dir(qtLibDir, "*.so");
const QStringList files = dir.entryList();
QVERIFY(!files.isEmpty());
for (const QString &lib : files) {
if (!keys.contains(lib))
continue;
if (lib == "libQtCLucene.so") {
// skip this library, it's 3rd-party C++
continue;
}
qDebug() << lib
<< ", " << dir.absolutePath();
QProcess proc;
proc.start("nm",
QStringList() << "-C" << "--format=posix" << "--dynamic"
<< dir.absolutePath() + "/" + lib);
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitCode(), 0);
QCOMPARE(QString::fromLocal8Bit(proc.readAllStandardError()), QString());
const QStringList symbols = QString::fromLocal8Bit(proc.readAll()).split("\n");
QVERIFY(!symbols.isEmpty());
for (const QString &symbol : symbols) {
if (symbol.isEmpty())
continue;
if (!symbol.startsWith("global constructors keyed to "))
continue;
QRegularExpression re("global constructors keyed to ([a-zA-Z_0-9.]*)");
QRegularExpressionMatch match = re.match(symbol);
QVERIFY(match.hasMatch());
QString cap = match.captured(1);
bool whitelisted = false;
for (const QRegularExpression &white : whitelist) {
if (cap.indexOf(white) != -1) {
whitelisted = true;
break;
}
}
if (whitelisted)
continue;
QString line = symbolToLine(symbol, dir.absolutePath() + "/" + lib);
if (cap.contains('.'))
qWarning() << "Static global object(s) found in" << lib << "in file" << cap << "(" << line << ")";
else
qWarning() << "Static global object found in" << lib << "near symbol" << cap << "(" << line << ")";
isFailed = true;
}
}
if (isFailed) {
QVERIFY2(!isFailed, "Libraries contain static global objects. See Debug output above.");
}
}
// This test needs a compiler on the target, which is unlikely if cross-compiled.
#ifndef QT_CROSS_COMPILED
void tst_Symbols::prefix()
{
const QString qtTypes[] = {
"QString", "QChar", "QWidget", "QObject", "QVariant", "QList",
"QMap", "QHash", "QVector", "QRect", "QSize", "QPoint",
"QTextFormat", "QTextLength", "QPen", "QFont", "QIcon",
"QPixmap", "QImage", "QRegion", "QPolygon",
};
QStringList qAlgorithmFunctions;
qAlgorithmFunctions << "qAbs" << "qMin" << "qMax" << "qBound" << "qSwap"
<< "qHash" << "qDeleteAll";
QStringList exceptionalSymbols;
exceptionalSymbols << "XRectangle::~XRectangle()"
<< "XChar2b::~XChar2b()"
<< "XPoint::~XPoint()"
<< "glyph_metrics_t::"; // #### Qt 4.2
const QString stupidCSymbols[] = {
"Add_Glyph_Property",
"Check_Property",
"Coverage_Index",
"Get_Class",
"Get_Device",
"rcsid3",
"sfnt_module_class",
"t1cid_driver_class",
"t42_driver_class",
"winfnt_driver_class",
"pshinter_module_class",
"psnames_module_class",
// C symbols from Qt
"qt_addObject",
"qt_removeObject",
"qt_startup_hook",
};
QHash<QString,QStringList> excusedPrefixes;
excusedPrefixes[QString()] =
QStringList() << "Ui_Q"; // uic generated, like Ui_QPrintDialog
excusedPrefixes["QtCore"] =
QStringList() << "hb_"
<< "HB_"
// zlib symbols, for -qt-zlib ;(
<< "deflate"
<< "compress"
<< "uncompress"
<< "adler32"
<< "gz"
<< "inflate"
<< "zlib"
<< "zError"
<< "get_crc_table"
<< "crc32";
excusedPrefixes["QtGui"] =
QStringList() << "ftglue_"
<< "Load_"
<< "otl_"
<< "TT_"
<< "tt_"
<< "t1_"
<< "Free_"
<< "FT_"
<< "FTC_"
<< "ft_"
<< "ftc_"
<< "af_autofitter"
<< "af_dummy"
<< "af_latin"
<< "autofit_"
<< "XPanorami"
<< "Xinerama"
<< "bdf_"
<< "ccf_"
<< "gray_raster"
<< "pcf_"
<< "cff_"
<< "otv_"
<< "pfr_"
<< "ps_"
<< "psaux"
<< "png_";
excusedPrefixes["QtSql"] =
QStringList() << "sqlite3";
excusedPrefixes["QtWebKit"] =
QStringList() << "WebCore::"
<< "KJS::"
<< "kjs"
<< "kJS"
<< "JS"
// << "OpaqueJS"
<< "WTF"
<< "wtf_"
<< "SVG::"
<< "NPN_"
<< "cti" // ctiTrampoline and ctiVMThrowTrampoline from the JIT
#ifdef QT_NAMESPACE
<< "QWeb" // Webkit is only 'namespace aware'
<< "qWeb"
<< "qt"
<< "QGraphicsWebView"
<< "operator"
#endif
;
excusedPrefixes["phonon"] =
QStringList() << ns + "Phonon";
QDir dir(qtLibDir, "*.so");
const QStringList files = dir.entryList();
QVERIFY(!files.isEmpty());
bool isFailed = false;
for (const QString &lib : files) {
if (!keys.contains(lib))
continue;
if (lib.contains("Designer") || lib.contains("QtCLucene") || lib.contains("XmlPatternsSDK"))
continue;
bool isPhonon = lib.contains("phonon");
qDebug() << lib
<< ", " << dir.absolutePath();
QProcess proc;
proc.start("nm",
QStringList() << "-g" << "-C" << "-D" << "--format=posix"
<< "--defined-only" << dir.absolutePath() + "/" + lib);
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitCode(), 0);
QCOMPARE(QString::fromLocal8Bit(proc.readAllStandardError()), QString());
const auto content = proc.readAll();
QVERIFY(!content.isEmpty());
// treat output as L1, because we only parse ASCII tokens:
for (QLatin1StringView line : qTokenize(QLatin1StringView{content}, u'\n')) {
if (line.isEmpty())
continue;
QString symbol = QString::fromLocal8Bit(QByteArrayView{line});
if (symbol.startsWith("unsigned "))
// strip modifiers
symbol = symbol.mid(symbol.indexOf(' ') + 1);
if (symbol.startsWith("long long ")) {
symbol = symbol.mid(10);
} else if (symbol.startsWith("bool ") || symbol.startsWith("bool* ")
|| symbol.startsWith("char ") || symbol.startsWith("char* ")
|| symbol.startsWith("int ") || symbol.startsWith("int* ") || symbol.startsWith("int*&")
|| symbol.startsWith("short") || symbol.startsWith("long ")
|| symbol.startsWith("void ") || symbol.startsWith("void* ")
|| symbol.startsWith("double ") || symbol.startsWith("double* ")
|| symbol.startsWith("float ") || symbol.startsWith("float* ")) {
// partial templates have the return type in their demangled name, strip it
symbol = symbol.mid(symbol.indexOf(' ') + 1);
}
if (symbol.startsWith("const ") || symbol.startsWith("const* ") ||
symbol.startsWith("const& ")) {
// strip modifiers
symbol = symbol.mid(symbol.indexOf(' ') + 1);
}
if (symbol.mid(symbol.indexOf(' ')+1).startsWith("std::"))
continue;
if (symbol.startsWith("_") || symbol.startsWith("std::"))
continue;
if (symbol.startsWith("vtable ") || symbol.startsWith("VTT for ") ||
symbol.startsWith("construction vtable for"))
continue;
if (symbol.startsWith("typeinfo "))
continue;
if (symbol.startsWith("non-virtual thunk ") || symbol.startsWith("virtual thunk"))
continue;
if (symbol.startsWith(ns + "operator"))
continue;
if (symbol.startsWith("operator new") || symbol.startsWith("operator delete"))
continue;
if (symbol.startsWith("guard variable for "))
continue;
if (symbol.contains("(" + ns + "QTextStream"))
// QTextStream is excused.
continue;
if (symbol.contains("(" + ns + "Q3TextStream"))
// Q3TextStream is excused.
continue;
if (symbol.startsWith(ns + "bitBlt") || symbol.startsWith(ns + "copyBlt"))
// you're excused, too
continue;
bool symbolOk = false;
QHash<QString,QStringList>::ConstIterator it = excusedPrefixes.constBegin();
for ( ; it != excusedPrefixes.constEnd(); ++it) {
if (!lib.contains(it.key()))
continue;
for (const QString &prefix : it.value())
if (symbol.startsWith(prefix)) {
symbolOk = true;
break;
}
}
if (symbolOk)
continue;
for (const QString &cSymbolPattern : stupidCSymbols)
if (symbol.contains(cSymbolPattern)) {
symbolOk = true;
break;
}
if (symbolOk)
continue;
QStringList fields = symbol.split(' ');
// the last two fields are address and size and the third last field is the symbol type
QVERIFY(fields.size() > 3);
QString type = fields.at(fields.size() - 3);
// weak symbol
if (type == QLatin1String("W")) {
if (symbol.contains("qAtomic"))
continue;
if (symbol.contains("fstat")
|| symbol.contains("lstat")
|| symbol.contains("stat64")
)
continue;
const QStringList acceptedPatterns = qAlgorithmFunctions + exceptionalSymbols;
for (const QString &acceptedPattern : acceptedPatterns)
if (symbol.contains(acceptedPattern)) {
symbolOk = true;
break;
}
if (symbolOk)
continue;
QString plainSymbol;
for (int i = 0; i < fields.size() - 3; ++i) {
if (i > 0)
plainSymbol += QLatin1Char(' ');
plainSymbol += fields.at(i);
}
for (const QString &qtType : qtTypes)
if (plainSymbol.contains(qtType)) {
symbolOk = true;
break;
}
if (symbolOk)
continue;
}
QString prefix = ns + "q";
if (!symbol.startsWith(prefix, Qt::CaseInsensitive)
&& !(isPhonon && symbol.startsWith("Phonon"))) {
qDebug("symbol in '%s' does not start with prefix '%s': '%s'",
qPrintable(lib), qPrintable(prefix), qPrintable(symbol));
isFailed = true;
}
}
}
QVERIFY2(!isFailed, "Libraries contain non-prefixed symbols. See Debug output above.");
}
#endif
QTEST_MAIN(tst_Symbols)
#include "tst_symbols.moc"