1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <qbuffer.h>
5#include <qbytearray.h>
6#include <qdebug.h>
7#include <qfile.h>
8#include <qlist.h>
9#include <qstring.h>
10#include <qvarlengtharray.h>
11
12#include <stdio.h>
13#include <stdlib.h>
14#include <string.h>
15
16#include <qdbusconnection.h> // for the Export* flags
17#include <private/qdbusconnection_p.h> // for the qDBusCheckAsyncTag
18
19using namespace Qt::StringLiterals;
20
21// copied from dbus-protocol.h:
22static const char docTypeHeader[] =
23 "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\" "
24 "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n";
25
26#define ANNOTATION_NO_WAIT "org.freedesktop.DBus.Method.NoReply"
27#define QCLASSINFO_DBUS_INTERFACE "D-Bus Interface"
28#define QCLASSINFO_DBUS_INTROSPECTION "D-Bus Introspection"
29
30#include <qdbusmetatype.h>
31#include <private/qdbusmetatype_p.h>
32#include <private/qdbusutil_p.h>
33
34#include "moc.h"
35#include "generator.h"
36#include "preprocessor.h"
37
38#define PROGRAMNAME "qdbuscpp2xml"
39#define PROGRAMVERSION "0.2"
40#define PROGRAMCOPYRIGHT QT_COPYRIGHT
41
42static QString outputFile;
43static int flags;
44
45static const char help[] =
46 "Usage: " PROGRAMNAME " [options...] [files...]\n"
47 "Parses the C++ source or header file containing a QObject-derived class and\n"
48 "produces the D-Bus Introspection XML."
49 "\n"
50 "Options:\n"
51 " -p|-s|-m Only parse scriptable Properties, Signals and Methods (slots)\n"
52 " -P|-S|-M Parse all Properties, Signals and Methods (slots)\n"
53 " -a Output all scriptable contents (equivalent to -psm)\n"
54 " -A Output all contents (equivalent to -PSM)\n"
55 " -t <type>=<dbustype> Output <type> (ex: MyStruct) as <dbustype> (ex: {ss})\n"
56 " -o <filename> Write the output to file <filename>\n"
57 " -h Show this information\n"
58 " -V Show the program version and quit.\n"
59 "\n";
60
61int qDBusParametersForMethod(const FunctionDef &mm, QList<QMetaType> &metaTypes, QString &errorMsg)
62{
63 QList<QByteArray> parameterTypes;
64 parameterTypes.reserve(asize: mm.arguments.size());
65
66 for (const ArgumentDef &arg : mm.arguments)
67 parameterTypes.append(t: arg.normalizedType);
68
69 return qDBusParametersForMethod(parameters: parameterTypes, metaTypes, errorMsg);
70}
71
72
73static inline QString typeNameToXml(const char *typeName)
74{
75 QString plain = QLatin1StringView(typeName);
76 return plain.toHtmlEscaped();
77}
78
79static QString addFunction(const FunctionDef &mm, bool isSignal = false) {
80
81 QString xml = QString::asprintf(format: " <%s name=\"%s\">\n",
82 isSignal ? "signal" : "method", mm.name.constData());
83
84 // check the return type first
85 int typeId = QMetaType::fromName(name: mm.normalizedType).id();
86 if (typeId != QMetaType::Void) {
87 if (typeId) {
88 const char *typeName = QDBusMetaType::typeToSignature(type: QMetaType(typeId));
89 if (typeName) {
90 xml += QString::fromLatin1(ba: " <arg type=\"%1\" direction=\"out\"/>\n")
91 .arg(a: typeNameToXml(typeName));
92
93 // do we need to describe this argument?
94 if (!QDBusMetaType::signatureToMetaType(signature: typeName).isValid())
95 xml += QString::fromLatin1(ba: " <annotation name=\"org.qtproject.QtDBus.QtTypeName.Out0\" value=\"%1\"/>\n")
96 .arg(a: typeNameToXml(typeName: mm.normalizedType.constData()));
97 } else {
98 return QString();
99 }
100 } else if (!mm.normalizedType.isEmpty()) {
101 qWarning() << "Unregistered return type:" << mm.normalizedType.constData();
102 return QString();
103 }
104 }
105 QList<ArgumentDef> names = mm.arguments;
106 QList<QMetaType> types;
107 QString errorMsg;
108 int inputCount = qDBusParametersForMethod(mm, metaTypes&: types, errorMsg);
109 if (inputCount == -1) {
110 qWarning() << qPrintable(errorMsg);
111 return QString(); // invalid form
112 }
113 if (isSignal && inputCount + 1 != types.size())
114 return QString(); // signal with output arguments?
115 if (isSignal && types.at(i: inputCount) == QDBusMetaTypeId::message())
116 return QString(); // signal with QDBusMessage argument?
117
118 bool isScriptable = mm.isScriptable;
119 for (qsizetype j = 1; j < types.size(); ++j) {
120 // input parameter for a slot or output for a signal
121 if (types.at(i: j) == QDBusMetaTypeId::message()) {
122 isScriptable = true;
123 continue;
124 }
125
126 QString name;
127 if (!names.at(i: j - 1).name.isEmpty())
128 name = QString::fromLatin1(ba: "name=\"%1\" ").arg(a: QString::fromLatin1(ba: names.at(i: j - 1).name));
129
130 bool isOutput = isSignal || j > inputCount;
131
132 const char *signature = QDBusMetaType::typeToSignature(type: QMetaType(types.at(i: j)));
133 xml += QString::fromLatin1(ba: " <arg %1type=\"%2\" direction=\"%3\"/>\n")
134 .arg(args&: name,
135 args: QLatin1StringView(signature),
136 args: isOutput ? "out"_L1 : "in"_L1);
137
138 // do we need to describe this argument?
139 if (!QDBusMetaType::signatureToMetaType(signature).isValid()) {
140 const char *typeName = QMetaType(types.at(i: j)).name();
141 xml += QString::fromLatin1(ba: " <annotation name=\"org.qtproject.QtDBus.QtTypeName.%1%2\" value=\"%3\"/>\n")
142 .arg(a: isOutput ? "Out"_L1 : "In"_L1)
143 .arg(a: isOutput && !isSignal ? j - inputCount : j - 1)
144 .arg(a: typeNameToXml(typeName));
145 }
146 }
147
148 int wantedMask;
149 if (isScriptable)
150 wantedMask = isSignal ? QDBusConnection::ExportScriptableSignals
151 : QDBusConnection::ExportScriptableSlots;
152 else
153 wantedMask = isSignal ? QDBusConnection::ExportNonScriptableSignals
154 : QDBusConnection::ExportNonScriptableSlots;
155 if ((flags & wantedMask) != wantedMask)
156 return QString();
157
158 if (qDBusCheckAsyncTag(tag: mm.tag.constData()))
159 // add the no-reply annotation
160 xml += " <annotation name=\"" ANNOTATION_NO_WAIT "\" value=\"true\"/>\n"_L1;
161
162 QString retval = xml;
163 retval += QString::fromLatin1(ba: " </%1>\n").arg(a: isSignal ? "signal"_L1 : "method"_L1);
164
165 return retval;
166}
167
168
169static QString generateInterfaceXml(const ClassDef *mo)
170{
171 QString retval;
172
173 // start with properties:
174 if (flags & (QDBusConnection::ExportScriptableProperties |
175 QDBusConnection::ExportNonScriptableProperties)) {
176 static const char *accessvalues[] = {nullptr, "read", "write", "readwrite"};
177 for (const PropertyDef &mp : mo->propertyList) {
178 if (!((!mp.scriptable.isEmpty() && (flags & QDBusConnection::ExportScriptableProperties)) ||
179 (!mp.scriptable.isEmpty() && (flags & QDBusConnection::ExportNonScriptableProperties))))
180 continue;
181
182 int access = 0;
183 if (!mp.read.isEmpty())
184 access |= 1;
185 if (!mp.write.isEmpty())
186 access |= 2;
187
188 int typeId = QMetaType::fromName(name: mp.type).id();
189 if (!typeId) {
190 fprintf(stderr, PROGRAMNAME ": unregistered type: '%s', ignoring\n",
191 mp.type.constData());
192 continue;
193 }
194 const char *signature = QDBusMetaType::typeToSignature(type: QMetaType(typeId));
195 if (!signature)
196 continue;
197
198 retval += QString::fromLatin1(ba: " <property name=\"%1\" type=\"%2\" access=\"%3\"")
199 .arg(args: QLatin1StringView(mp.name),
200 args: QLatin1StringView(signature),
201 args: QLatin1StringView(accessvalues[access]));
202
203 if (!QDBusMetaType::signatureToMetaType(signature).isValid()) {
204 retval += QString::fromLatin1(ba: ">\n <annotation name=\"org.qtproject.QtDBus.QtTypeName\" value=\"%3\"/>\n </property>\n")
205 .arg(a: typeNameToXml(typeName: mp.type.constData()));
206 } else {
207 retval += "/>\n"_L1;
208 }
209 }
210 }
211
212 // now add methods:
213
214 if (flags & (QDBusConnection::ExportScriptableSignals | QDBusConnection::ExportNonScriptableSignals)) {
215 for (const FunctionDef &mm : mo->signalList) {
216 if (mm.wasCloned)
217 continue;
218 if (!mm.isScriptable && !(flags & QDBusConnection::ExportNonScriptableSignals))
219 continue;
220
221 retval += addFunction(mm, isSignal: true);
222 }
223 }
224
225 if (flags & (QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportNonScriptableSlots)) {
226 for (const FunctionDef &slot : mo->slotList) {
227 if (!slot.isScriptable && !(flags & QDBusConnection::ExportNonScriptableSlots))
228 continue;
229 if (slot.access == FunctionDef::Public)
230 retval += addFunction(mm: slot);
231 }
232 for (const FunctionDef &method : mo->methodList) {
233 if (!method.isScriptable && !(flags & QDBusConnection::ExportNonScriptableSlots))
234 continue;
235 if (method.access == FunctionDef::Public)
236 retval += addFunction(mm: method);
237 }
238 }
239 return retval;
240}
241
242QString qDBusInterfaceFromClassDef(const ClassDef *mo)
243{
244 QString interface;
245
246 for (const ClassInfoDef &cid : mo->classInfoList) {
247 if (cid.name == QCLASSINFO_DBUS_INTERFACE)
248 return QString::fromUtf8(ba: cid.value);
249 }
250 interface = QLatin1StringView(mo->classname);
251 interface.replace(before: "::"_L1, after: "."_L1);
252
253 if (interface.startsWith(s: "QDBus"_L1)) {
254 interface.prepend(s: "org.qtproject.QtDBus."_L1);
255 } else if (interface.startsWith(c: u'Q') &&
256 interface.size() >= 2 && interface.at(i: 1).isUpper()) {
257 // assume it's Qt
258 interface.prepend(s: "local.org.qtproject.Qt."_L1);
259 } else {
260 interface.prepend(s: "local."_L1);
261 }
262
263 return interface;
264}
265
266
267QString qDBusGenerateClassDefXml(const ClassDef *cdef)
268{
269 for (const ClassInfoDef &cid : cdef->classInfoList) {
270 if (cid.name == QCLASSINFO_DBUS_INTROSPECTION)
271 return QString::fromUtf8(ba: cid.value);
272 }
273
274 // generate the interface name from the meta object
275 QString interface = qDBusInterfaceFromClassDef(mo: cdef);
276
277 QString xml = generateInterfaceXml(mo: cdef);
278
279 if (xml.isEmpty())
280 return QString(); // don't add an empty interface
281 return QString::fromLatin1(ba: " <interface name=\"%1\">\n%2 </interface>\n")
282 .arg(args&: interface, args&: xml);
283}
284
285static void showHelp()
286{
287 printf(format: "%s", help);
288 exit(status: 0);
289}
290
291static void showVersion()
292{
293 printf(format: "%s version %s\n", PROGRAMNAME, PROGRAMVERSION);
294 printf(format: "D-Bus QObject-to-XML converter\n");
295 exit(status: 0);
296}
297
298class CustomType {
299public:
300 CustomType(const QByteArray &typeName)
301 : typeName(typeName)
302 {
303 metaTypeImpl.name = typeName.constData();
304 }
305 QMetaType metaType() const { return QMetaType(&metaTypeImpl); }
306
307private:
308 // not copiable and not movable because of QBasicAtomicInt
309 QtPrivate::QMetaTypeInterface metaTypeImpl =
310 { /*.revision=*/ 0,
311 /*.alignment=*/ 0,
312 /*.size=*/ 0,
313 /*.flags=*/ 0,
314 /*.typeId=*/ 0,
315 /*.metaObjectFn=*/ 0,
316 /*.name=*/ nullptr, // set by the constructor
317 /*.defaultCtr=*/ nullptr,
318 /*.copyCtr=*/ nullptr,
319 /*.moveCtr=*/ nullptr,
320 /*.dtor=*/ nullptr,
321 /*.equals=*/ nullptr,
322 /*.lessThan=*/ nullptr,
323 /*.debugStream=*/ nullptr,
324 /*.dataStreamOut=*/ nullptr,
325 /*.dataStreamIn=*/ nullptr,
326 /*.legacyRegisterOp=*/ nullptr
327 };
328 QByteArray typeName;
329};
330// Unlike std::vector, std::deque works with non-copiable non-movable types
331static std::deque<CustomType> s_customTypes;
332
333static void parseCmdLine(QStringList &arguments)
334{
335 flags = 0;
336 for (qsizetype i = 0; i < arguments.size(); ++i) {
337 const QString arg = arguments.at(i);
338
339 if (arg == "--help"_L1)
340 showHelp();
341
342 if (!arg.startsWith(c: u'-'))
343 continue;
344
345 char c = arg.size() == 2 ? arg.at(i: 1).toLatin1() : char(0);
346 switch (c) {
347 case 'P':
348 flags |= QDBusConnection::ExportNonScriptableProperties;
349 Q_FALLTHROUGH();
350 case 'p':
351 flags |= QDBusConnection::ExportScriptableProperties;
352 break;
353
354 case 'S':
355 flags |= QDBusConnection::ExportNonScriptableSignals;
356 Q_FALLTHROUGH();
357 case 's':
358 flags |= QDBusConnection::ExportScriptableSignals;
359 break;
360
361 case 'M':
362 flags |= QDBusConnection::ExportNonScriptableSlots;
363 Q_FALLTHROUGH();
364 case 'm':
365 flags |= QDBusConnection::ExportScriptableSlots;
366 break;
367
368 case 'A':
369 flags |= QDBusConnection::ExportNonScriptableContents;
370 Q_FALLTHROUGH();
371 case 'a':
372 flags |= QDBusConnection::ExportScriptableContents;
373 break;
374
375 case 't':
376 if (arguments.size() < i + 2) {
377 printf(format: "-t expects a type=dbustype argument\n");
378 exit(status: 1);
379 } else {
380 const QByteArray arg = arguments.takeAt(i: i + 1).toUtf8();
381 // lastIndexOf because the C++ type could contain '=' while the DBus type can't
382 const qsizetype separator = arg.lastIndexOf(c: '=');
383 if (separator == -1) {
384 printf(format: "-t expects a type=dbustype argument, but no '=' was found\n");
385 exit(status: 1);
386 }
387 const QByteArray type = arg.left(len: separator);
388 const QByteArray dbustype = arg.mid(index: separator+1);
389
390 s_customTypes.emplace_back(args: type);
391 QMetaType metaType = s_customTypes.back().metaType();
392 QDBusMetaType::registerCustomType(type: metaType, signature: dbustype);
393 }
394 break;
395
396 case 'o':
397 if (arguments.size() < i + 2 || arguments.at(i: i + 1).startsWith(c: u'-')) {
398 printf(format: "-o expects a filename\n");
399 exit(status: 1);
400 }
401 outputFile = arguments.takeAt(i: i + 1);
402 break;
403
404 case 'h':
405 case '?':
406 showHelp();
407 break;
408
409 case 'V':
410 showVersion();
411 break;
412
413 default:
414 printf(format: "unknown option: \"%s\"\n", qPrintable(arg));
415 exit(status: 1);
416 }
417 }
418
419 if (flags == 0)
420 flags = QDBusConnection::ExportScriptableContents
421 | QDBusConnection::ExportNonScriptableContents;
422}
423
424int main(int argc, char **argv)
425{
426 QStringList args;
427 args.reserve(asize: argc - 1);
428 for (int n = 1; n < argc; ++n)
429 args.append(t: QString::fromLocal8Bit(ba: argv[n]));
430 parseCmdLine(arguments&: args);
431
432 QList<ClassDef> classes;
433
434 if (args.isEmpty())
435 args << u"-"_s;
436 for (const auto &arg: std::as_const(t&: args)) {
437 if (arg.startsWith(c: u'-') && arg.size() > 1)
438 continue;
439
440 QFile f;
441 if (arg == u'-') {
442 f.open(stdin, ioFlags: QIODevice::ReadOnly | QIODevice::Text);
443 } else {
444 f.setFileName(arg);
445 f.open(flags: QIODevice::ReadOnly | QIODevice::Text);
446 }
447 if (!f.isOpen()) {
448 fprintf(stderr, PROGRAMNAME ": could not open '%s': %s\n",
449 qPrintable(arg), qPrintable(f.errorString()));
450 return 1;
451 }
452
453 Preprocessor pp;
454 Moc moc;
455 pp.macros["Q_MOC_RUN"];
456 pp.macros["__cplusplus"];
457
458 const QByteArray filename = arg.toLocal8Bit();
459
460 moc.filename = filename;
461 moc.currentFilenames.push(x: filename);
462
463 moc.symbols = pp.preprocessed(filename: moc.filename, device: &f);
464 moc.parse();
465
466 if (moc.classList.isEmpty())
467 return 0;
468 classes = moc.classList;
469
470 f.close();
471 }
472
473 QFile output;
474 if (outputFile.isEmpty()) {
475 output.open(stdout, ioFlags: QIODevice::WriteOnly);
476 } else {
477 output.setFileName(outputFile);
478 if (!output.open(flags: QIODevice::WriteOnly)) {
479 fprintf(stderr, PROGRAMNAME ": could not open output file '%s': %s",
480 qPrintable(outputFile), qPrintable(output.errorString()));
481 return 1;
482 }
483 }
484
485 output.write(data: docTypeHeader);
486 output.write(data: "<node>\n");
487 for (const ClassDef &cdef : std::as_const(t&: classes)) {
488 QString xml = qDBusGenerateClassDefXml(cdef: &cdef);
489 output.write(data: std::move(xml).toLocal8Bit());
490 }
491 output.write(data: "</node>\n");
492
493 return 0;
494}
495
496

source code of qtbase/src/tools/qdbuscpp2xml/qdbuscpp2xml.cpp