diff options
author | Brett Stottlemyer <bstottle@ford.com> | 2021-03-07 10:51:50 -0500 |
---|---|---|
committer | Brett Stottlemyer <bstottle@ford.com> | 2021-07-15 14:44:24 -0400 |
commit | 0ab10b008f4b713b7a58206b796668e05e80c2c6 (patch) | |
tree | 6795aadfd8e9d74d962ccd8709342dc414fd6065 | |
parent | d8b6dce0d2cf91e20905856aef06c77c8659da98 (diff) |
Add example of reading CBOR data from pythonwip/serialization
This is currently a test harness that will expand as the CBOR
functionality is prototyped.
A start at changes to repc to autogenerate python code for Replica types
is also included.
Change-Id: I97c3ad4c62c7d747c1918f88be41fb7643bbb46d
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Michael Brasser <michael.brasser@live.com>
23 files changed, 2258 insertions, 136 deletions
diff --git a/examples/remoteobjects/CMakeLists.txt b/examples/remoteobjects/CMakeLists.txt index ec71f6f..652c64b 100644 --- a/examples/remoteobjects/CMakeLists.txt +++ b/examples/remoteobjects/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(server) add_subdirectory(cppclient) +add_subdirectory(cbor_python) add_subdirectory(simpleswitch) add_subdirectory(websockets) if(TARGET Qt::Widgets) diff --git a/examples/remoteobjects/cbor_python/CMakeLists.txt b/examples/remoteobjects/cbor_python/CMakeLists.txt new file mode 100644 index 0000000..609eb34 --- /dev/null +++ b/examples/remoteobjects/cbor_python/CMakeLists.txt @@ -0,0 +1,3 @@ +# Generated from cbor_python.pro. + +add_subdirectory(qt_source) diff --git a/examples/remoteobjects/cbor_python/cbor_python.pro b/examples/remoteobjects/cbor_python/cbor_python.pro new file mode 100644 index 0000000..b559903 --- /dev/null +++ b/examples/remoteobjects/cbor_python/cbor_python.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs + +SUBDIRS += qt_source diff --git a/examples/remoteobjects/cbor_python/py_replica/py_replica.py b/examples/remoteobjects/cbor_python/py_replica/py_replica.py new file mode 100644 index 0000000..8d9509b --- /dev/null +++ b/examples/remoteobjects/cbor_python/py_replica/py_replica.py @@ -0,0 +1,653 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2021 Ford Motor Company +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the QtRemoteObjects module of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## BSD License Usage +## Alternatively, you may use this file under the terms of the BSD license +## as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import argparse +import asyncio +import collections +import logging +import signal +import socket +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import IntEnum +from functools import partial +from struct import pack, unpack, calcsize +from urllib.parse import urlparse +from weakref import WeakSet + +DEFAULT_URL = "tcp://127.0.0.1:5005" +logging.basicConfig(format='%(asctime)s.%(msecs)06d %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') +logger = logging.getLogger(__name__) + + +class PacketType(IntEnum): + Invalid = 0 + Handshake = 1 + InitPacket = 2 + InitDynamicPacket = 3 + AddObject = 4 + RemoveObject = 5 + InvokePacket = 6 + InvokeReplyPacket = 7 + PropertyChangePacket = 8 + ObjectList = 9 + Ping = 10 + Pong = 11 + + +class Decoder: + _QVariantDecoders = { + 1: lambda self: self.decodeBool(), + 2: lambda self: self.decodeS32(), + 3: lambda self: self.decodeU32(), + 6: lambda self: self.decodeDouble(), + 10: lambda self: self.decodeQString(), + 12: lambda self: self.decodeByteArray(), + 36: lambda self: self.decodeU16(), + 38: lambda self: self.decodeDouble(), # float is treated as double in QDataStream + } + + def __init__(self, data): + self.data = data + self.index = 0 + + def decodeU16(self): + val = unpack(">H", self.data[self.index:self.index+2])[0] + logger.debug(f"decodeU16: val = {val} (at index = {self.index})") + self.index += 2 + return val + + def decodeU32(self): + val = unpack(">I", self.data[self.index:self.index+4])[0] + logger.debug(f"decodeU32: val = {val} (at index = {self.index})") + self.index += 4 + return val + + def decodeS32(self): + val = unpack(">i", self.data[self.index:self.index+4])[0] + logger.debug(f"decodeS32: val = {val} (at index = {self.index})") + self.index += 4 + return val + + def decodeDouble(self): + val = unpack(">d", self.data[self.index:self.index+8])[0] + logger.debug(f"decodeDouble: val = {val} (at index = {self.index})") + self.index += 8 + return val + + def decodeQString(self): + l = self.decodeU32() + s = self.data[self.index:self.index+l].decode('utf-16be') + logger.debug(f"decodeQString: len = {l/2}, s = {s}") + self.index += l + return s + + def decodeByteArray(self): + l = self.decodeU32() + s = self.data[self.index:self.index+l] + logger.debug(f"decodeByteArray: len = {l}, s = {s}") + self.index += l + return s + + def decodeBool(self): + b = bool(self.data[self.index]) + logger.debug(f"decodeBool: result {b}") + self.index += 1 + return b + + def decodeQVariant(self): + typeId = self.decodeU32() + isNull = self.decodeBool() + decoder = Decoder._QVariantDecoders.get(typeId) + if decoder: + return decoder(self) + else: + raise RuntimeError(f"Tried to decode unrecognized QVariant type ({typeId}).") + + def decodeArgs(self): + parameters = [] + nArgs = self.decodeU32() + for i in range(nArgs): + parameters.append(self.decodeQVariant()) + return parameters + + +class Timer: + # Based on https://stackoverflow.com/a/45430833/169296 + + def __init__(self, timeout, callback): + self._timeout = timeout + self._callback = callback + self._task = asyncio.create_task(self._job()) + + async def _job(self): + await asyncio.sleep(self._timeout) + await self._callback() + + async def restart(self): + if not self._task.cancelled(): + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + self._task = asyncio.create_task(self._job()) + + +class Pipe(ABC): + handlers = {} + schema = None + + def __init__(self, url, queue, hbInterval): + self.url = url + self.queue = queue + self.hbInterval = hbInterval + self.handshake = False + self.error = self.reader = self.writer = self.timer = None + + @abstractmethod + async def connect(self): + pass + + async def run(self): + await self.connect() + while not self.error: + data = await self.reader.readexactly(4) + if not self.handshake: # Test Qt6 handshake + size, packetId, rev, firstChar = unpack(">BBBc", data) + logger.debug(f"Handshake size = {size} id = {packetId} rev = {rev} firstChar = " + f"{firstChar}") + if size < 3 or size > 252 or packetId != 1: + size = unpack(">I", data)[0] + firstChar = b'\x00' + else: + size -= 3 # size includes packetId, rev and firstChar which we've already read + else: + size = unpack(">I", data)[0] + logger.debug(f"Packet length {size}") + data = await self.reader.readexactly(size) + if not self.handshake: + if firstChar == b'\x00': + packetId = unpack(">H", data[:2])[0] + if packetId != PacketType.Handshake: + logger.error("Invalid pipe, unable to establish handshake") + self.error = True + continue + else: + # Make it look like the old style handshake packet + codec = (firstChar + data).decode('utf-8').encode('utf-16be') + data = b'\x00\x01' + pack('>I', len(codec)) + codec + logger.error(f"Data {data}") + self.handshake = True # The Node will delete us if the handshake isn't valid + if self.hbInterval > 0: + self.timer = Timer(self.hbInterval, self.sendHeartbeat) + elif self.timer: + await self.timer.restart() + await self.queue.put( (id(self), data) ) + self.writer.close() + self.handshake = False + await self.queue.put( (id(self), None) ) + + async def sendHeartbeat(self): + logger.debug("Sending Heartbeat") + self.writer.write(pack(">IH", 2, PacketType.Ping)) + await self.timer.restart() + + @classmethod + def __init_subclass__(cls, **kwargs): + """ + This creates a map of schema name ("tcp", "ws", etc) to the class (derived from Pipe) + that handles that type of pipe. + + This classmethod is called whenever a class derived from Pipe is parsed, assuming each + class has a class variable (not an instance variable) named `schema`. + """ + super().__init_subclass__(**kwargs) + if cls.schema is None: + raise RuntimeError(f"Invalid Pipe defined ({cls.__name__}). Pipes must declare a " + f"schema to support.") + if cls.schema in Pipe.handlers: + raise RuntimeError(f"Invalid Pipe defined ({cls.__name__}). Schema {cls.schema} " + f"already in use.") + Pipe.handlers[cls.schema] = cls + + +class TcpPipe(Pipe): + schema = "tcp" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + urlmeta = urlparse(self.url) + self.hostname = urlmeta.hostname + self.port = urlmeta.port + + async def connect(self): + # TODO error handling, reconnect, etc + self.reader, self.writer = await asyncio.open_connection(self.hostname, self.port) + + +class Node: + + def __init__(self, name=""): + self.Handlers = { + PacketType.Handshake: lambda decoder: self.onHandshake(decoder), + PacketType.InitPacket: lambda decoder: self.onInitPacket(decoder), + PacketType.InitDynamicPacket: lambda decoder: self.onInitDynamicPacket(decoder), + PacketType.RemoveObject: lambda decoder: self.onRemoveObject(decoder), + PacketType.InvokePacket: lambda decoder: self.onInvokePacket(decoder), + PacketType.InvokeReplyPacket: lambda decoder: self.onInvokeReplyPacket(decoder), + PacketType.PropertyChangePacket: lambda decoder: self.onPropertyChangePacket(decoder), + PacketType.ObjectList: lambda decoder: self.onObjectList(decoder), + PacketType.Pong: lambda decoder: self.onPong(decoder), + } + self.name = name + # We set write() and currentPipe to the pipe of the packet we are handling + self.write = self.currentPipe = None + self.hbInterval = 0 + self.queue = asyncio.Queue() + self.pipes = {} + self.replicaPrivateInstances = {} + + def acquire(self, cls, name=None): + name = name or cls.__name__ + replicaPrivate = self.replicaPrivateInstances.get(name) + if replicaPrivate is None: + replicaPrivate = ReplicaPrivate(cls, name) + self.replicaPrivateInstances[name] = replicaPrivate + return replicaPrivate.create() + + def setHeartbeatInterval(self, timeout): + self.hbInterval = timeout + + def connect(self, url): + urlmeta = urlparse(url) + if urlmeta.scheme not in Pipe.handlers: + logger.error(f"Unrecognized schema ({urlmeta.scheme}) for url ({url})") + logger.error(f" Known schemas: {Pipe.handlers.keys()}") + return + pipe = Pipe.handlers[urlmeta.scheme](url, self.queue, self.hbInterval) + self.pipes[id(pipe)] = pipe + asyncio.get_event_loop().create_task(pipe.run()) + + def onPipeError(self, msg): + logger.error(msg) + del self.pipes[id(self.currentPipe)] + + async def run(self): + while True: + pipeId, data = await self.queue.get() + if data is None: + del self.pipes[pipeId] + d = Decoder(data) + packetId = d.decodeU16() + handler = self.Handlers.get(packetId, None) + if handler: + self.currentPipe = self.pipes.get(pipeId) + if self.currentPipe is None: + continue # We deleted the pipe, but there were still packets in the queue + self.write = self.currentPipe.writer.write + logger.debug(f"{PacketType(packetId).name} packet received on node {self.name}") + handler(d) + self.write = self.currentPipe = None + logger.debug("") + else: + self.onPipeError(f"Invalid packet found with id = {packetId}") + + def onHandshake(self, d): + handshake = d.decodeQString() + if handshake not in ["QtRO 1.3", "QDataStream"]: + self.onPipeError(f"Unrecognized handshake ({handshake}) received.") + + def onObjectList(self, d): + l = d.decodeU32() + i = 0 + while l > 0: + i += 1 + l -= 1 + name = d.decodeQString() + typename = d.decodeQString() + checksum = d.decodeByteArray() + logger.debug(f"Object #{i}: name: {name} type: {typename} checksum: {checksum}") + len2 = len(name) * 2 + self.write(pack(f">IHI{len2}sc", 7 + len2, PacketType.AddObject, len2, + name.encode('utf-16be'), b'\x00')) + + def onInitPacket(self, d): + name = d.decodeQString() + properties = d.decodeArgs() + logger.info(f"Properties for {name}: {properties}") + self.replicaPrivateInstances[name].initialize(self.currentPipe, properties) + + def onInitDynamicPacket(self, d): pass + def onRemoveObject(self, d): pass + def onInvokePacket(self, d): + name = d.decodeQString() + call = d.decodeS32() + index = d.decodeS32() + parameters = d.decodeArgs() + serialId = d.decodeS32() + propertyIndex = d.decodeS32() + logger.info(f"Invoke: name: {name} call: {call} index: {index} serialId: {serialId} " + f"propertyIndex: {propertyIndex}") + self.replicaPrivateInstances[name].invoke(index, *parameters) + + def onInvokeReplyPacket(self, d): pass + def onPropertyChangePacket(self, d): + name = d.decodeQString() + index = d.decodeS32() + val = d.decodeQVariant() + logger.debug(f"PropertyChange: name: {name} index: {index} value: {val}") + + def onPong(self, d): pass + + +class Signal: + + def __init__(self): + self._slots = collections.deque() + + def __repr__(self): + res = super().__repr__() + if self._slots: + extra = f'slots:{len(self._slots)}' + return f'<{res[1:-1]} [{extra}]>' + + def emit(self, *args): + for slot in self._slots: + slot(*args) + + def connect(self, func): + self._slots.append(func) + + +class ReplicaPrivate: + """ + There can be multiple instances of the same replica type. We don't want + each to each to talk to the source indepenently, so the ReplicaPrivate class + represents + * The pipe (if available) to the source + * The "cache" of property values locally, shared by instances + * The link to all instances + """ + + def __init__(self, cls, name): + self.instances = WeakSet() + self.pipe = None + self.cls = cls + self.name = name + self.typename = cls.__name__ + self.properties = cls.defaults() + + def create(self): + replica = self.cls(self) + self.instances.add(replica) + return replica + + def write(self, buffer): + if self.pipe is None: + return + self.pipe.writer.write(buffer) + + def initialize(self, pipe, values): + self.pipe = pipe + self.properties = values + for instance in self.instances: + instance.initialize() + + def invoke(self, index, *parameters): + signal = self.cls.Signals[index] + for instance in self.instances: + print("invoke on instance", instance) + getattr(instance, signal).emit(*parameters) + + def isValid(self): + return self.pipe is not None + + +@dataclass(eq=False) +class Replica: + _priv: ReplicaPrivate = field(repr=False) + initialized: Signal = field(init=False, repr=False, default_factory=Signal) + + def __new__(cls, replicaPrivate=None): + if replicaPrivate is None: + raise RuntimeError("Replicas can only be created from a Node's acquire() method") + return super().__new__(cls) + + def name(self): + return self._priv.name + + def typename(self): + return self._priv.typename + + def initialize(self, *args): + self.initialized.emit() + + def callSlot(self, index, *args): + # 0 is Call.InvokeMetaMethod + self.sendInvokePacket(index, 0, self._priv.cls.slotTypes()[index], *args) + + def callSetter(self, index, *args): + # 2 is Call.WriteProperty + self.sendInvokePacket(index, 2, self._priv.cls.propTypes()[index], *args) + + def sendInvokePacket(self, index, callIndex, types, *args): + if not self._priv.isValid(): + logger.warning(f"Replica instance (name: {self.name()} type: {self.typename()}) tried " + f"to write before pipe available.") + return + encodedLen = len(self.name())*2 + prefixPart = [ + ("I", 0), # Length of packet, filled later + ("H", PacketType.InvokePacket), # PacketId + ("I", encodedLen), # Length of name as a QString + (f"{encodedLen}s", self.name().encode('utf-16be')), # Replica name string + ("i", callIndex), # Call + ("i", index), # Index + ("I", len(args)), # Number of args + ] + argsPart = [] + for ind, arg in enumerate(args): + f = types[ind] + if f == "i": + argsPart.append( ("i", 2) ) # Qt typeId for int + elif f == "f": + argsPart.append( ("i", 38) ) # Qt typeId for float + argsPart.append( ("c", b'\x00') ) # isNull + if f == "i": + argsPart.append( ("i", arg) ) + elif f == "f": + argsPart.append( ("d", arg) ) + suffixPart = [ + ("i", -1), # SerialId + ("i", -1), # PropertyIndex + ] + f, newArgs = map(list, zip(*prefixPart, *argsPart, *suffixPart)) + f = ">" + " ".join(f) + newArgs[0] = calcsize(f) - 4 # Exclude the 4 bytes for the packet length + buffer = pack(f, *newArgs) + self._priv.write(buffer) + + +@dataclass(eq=False) +class Simple(Replica): + Signals = ['iChanged', 'fChanged', 'random'] + iChanged: Signal = field(init=False, repr=False, default_factory=Signal) + fChanged: Signal = field(init=False, repr=False, default_factory=Signal) + random: Signal = field(init=False, repr=False, default_factory=Signal) + + @property + def i(self): + return self._priv.properties[0] + + def pushI(self, i): + print('pushI', i) + self.callSlot(0, i) + + @property + def f(self): + return self._priv.properties[1] + + @f.setter + def f(self, f): + print('f setter', f) + self.callSetter(1, f) + + def reset(self): + print('Calling reset slot') + self.callSlot(1) + + @classmethod + def defaults(cls): + return [2, -1.] + + @classmethod + def propTypes(cls): + return [ + ('i',), # i is of type int -> i + ('f',), # f is of type float -> f + ] + + @classmethod + def signalTypes(cls): + return [ + ('i',), + ('f',), + ('i',), + ] + + @classmethod + def slotTypes(cls): + return [ + ('i',), + (), + ] + + @classmethod + def signature(cls): + return b'c6f33edb0554ba4241aad1286a47c8189d65c845' + + @classmethod + def name(cls): + return 'Simple' + + +def handle_exception(loop, context): + msg = context.get("exception", context["message"]) + logger.error(f"Caught exception: {msg}") + logger.error(f"{context}") + logger.info("Shutting down...") + asyncio.create_task(shutdown(loop)) + + +async def shutdown(loop, signal=None): + if signal: + logger.info(f"Received exit signal {signal.name}...") + logger.info("Cancelling outstanding tasks") + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + [task.cancel() for task in tasks] + + await asyncio.gather(*tasks, return_exceptions=True) + loop.stop() + + +def handler(i, val): + print(f"handler #{i} fired {val}") + + +def main(args): + loop = asyncio.get_event_loop() + signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) + for s in signals: + loop.add_signal_handler( + s, lambda s=s: asyncio.create_task(shutdown(loop, signal=s)) + ) + loop.set_exception_handler(handle_exception) + node = Node("A") + if args.heartbeat: + node.setHeartbeatInterval(args.heartbeat) + s = node.acquire(Simple, "DifferentName") + print(f"Initial Simple: i = {s.i} f = {s.f}") + s.iChanged.connect(partial(handler, 3)) + s.iChanged.connect(partial(handler, 7)) + # s.reset() # This gives a warning about the pipe not being available + s.initialized.connect(s.reset) + s.initialized.connect(lambda : print(f"Now s: i = {s.i} f = {s.f}")) + s2 = node.acquire(Simple, "DifferentName") + s2.iChanged.connect(partial(handler, 42)) + async def change(): + await asyncio.sleep(1.5) + s2.f = 99.5 + await asyncio.sleep(.5) + s2.f = 93.9 + s2.initialized.connect(lambda : loop.create_task(change())) + node.connect(DEFAULT_URL) + try: + loop.create_task(node.run()) + loop.run_forever() + finally: + loop.close() + logger.info("Loop exited") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", action="count", default=0, + help="provide verbose information about the run (may be repeated)") + parser.add_argument("--heartbeat", type=int, default=0, + help="set heartbeat timeout (in seconds)") + args = parser.parse_args() + if args.verbose == 1: + logger.setLevel(logging.INFO) + elif args.verbose > 1: + logger.setLevel(logging.DEBUG) + main(args) diff --git a/examples/remoteobjects/cbor_python/qt_source/CMakeLists.txt b/examples/remoteobjects/cbor_python/qt_source/CMakeLists.txt new file mode 100644 index 0000000..11c31c6 --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/CMakeLists.txt @@ -0,0 +1,58 @@ +# Generated from qt_source.pro. + +cmake_minimum_required(VERSION 3.14) +project(QtSource LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/remoteobjects/cbor_python/qt_source") + +find_package(Qt6 COMPONENTS Core) +find_package(Qt6 COMPONENTS Gui) +find_package(Qt6 COMPONENTS RemoteObjects) + +qt_add_executable(QtSource + main.cpp + simple.cpp simple.h +) +set_target_properties(QtSource PROPERTIES + WIN32_EXECUTABLE FALSE + MACOSX_BUNDLE FALSE +) +target_link_libraries(QtSource PUBLIC + Qt::Core + Qt::Gui + Qt::RemoteObjects +) + +qt6_add_repc_sources(QtSource + Simple.rep +) + +install(TARGETS QtSource + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) + +# special case begin +add_custom_command( + TARGET QtSource POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/../py_replica/py_replica.py + $<TARGET_FILE_DIR:QtSource>/../py_replica/py_replica.py + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/Simple.rep + $<TARGET_FILE_DIR:QtSource>/Simple.rep + ) +install(FILES ../py_replica/py_replica.py DESTINATION "${INSTALL_EXAMPLEDIR}/../py_replica") +install(FILES Simple.rep DESTINATION "${INSTALL_EXAMPLEDIR}") +# special case end diff --git a/examples/remoteobjects/cbor_python/qt_source/Simple.rep b/examples/remoteobjects/cbor_python/qt_source/Simple.rep new file mode 100644 index 0000000..1757ae6 --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/Simple.rep @@ -0,0 +1,9 @@ +#include <QtCore> + +class Simple +{ + PROP(int i = 2); + PROP(float f = -1. READWRITE); + SIGNAL(random(int i)); + SLOT(void reset()); +}; diff --git a/examples/remoteobjects/cbor_python/qt_source/main.cpp b/examples/remoteobjects/cbor_python/qt_source/main.cpp new file mode 100644 index 0000000..db04ea4 --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/main.cpp @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Ford Motor Company +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtRemoteObjects module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QCoreApplication> +#include <QDebug> +#include <QRemoteObjectHost> + +#include "simple.h" + +void SigIntHandler() +{ + qDebug()<<"Ctrl-C received. Quitting."; + qApp->quit(); +} +#if defined(Q_OS_UNIX) || defined(Q_OS_LINUX) || defined(Q_OS_QNX) + #include <signal.h> + void unix_handler(int s) + { + if (s==SIGINT) + SigIntHandler(); + } +#elif defined(Q_OS_WIN32) + #include <windows.h> + BOOL WINAPI WinHandler(DWORD CEvent) + { + switch (CEvent) + { + case CTRL_C_EVENT: + SigIntHandler(); + break; + } + return TRUE; + } +#endif + +int main(int argc, char *argv[]) +{ + QLoggingCategory::setFilterRules("qt.remoteobjects*=true"); + QCoreApplication app(argc, argv); +#if defined(Q_OS_UNIX) || defined(Q_OS_LINUX) || defined(Q_OS_QNX) + signal(SIGINT, &unix_handler); +#elif defined(Q_OS_WIN32) + SetConsoleCtrlHandler((PHANDLER_ROUTINE)WinHandler, TRUE); +#endif + QRemoteObjectHost node(QUrl(QLatin1String("tcp://127.0.0.1:5005"))); + Simple simple; + simple.setF(3.14); + node.enableRemoting(&simple, "DifferentName"); + Q_UNUSED(simple); + return app.exec(); +} diff --git a/examples/remoteobjects/cbor_python/qt_source/qt_source.pro b/examples/remoteobjects/cbor_python/qt_source/qt_source.pro new file mode 100644 index 0000000..16895ee --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/qt_source.pro @@ -0,0 +1,26 @@ +QT += remoteobjects + +TARGET = QtSource +CONFIG += cmdline +CONFIG -= app_bundle + +REPC_SOURCE += Simple.rep + +DISTFILES += \ + Simple.rep + +SOURCES += main.cpp \ + simple.cpp + +HEADERS += simple.h + +OTHER_FILES += $$REPC_SOURCE + +repfiles.files = $$REPC_SOURCE +repfiles.path += $$[QT_INSTALL_EXAMPLES]/remoteobjects/cbor_python/qt_source +pythonfiles.files = py_replica/py_replica.py +pythonfiles.path += $$[QT_INSTALL_EXAMPLES]/remoteobjects/cbor_python/py_replica + +target.path = $$[QT_INSTALL_EXAMPLES]/remoteobjects/cbor_python/qt_source + +INSTALLS += target repfiles pythonfiles diff --git a/examples/remoteobjects/cbor_python/qt_source/simple.cpp b/examples/remoteobjects/cbor_python/qt_source/simple.cpp new file mode 100644 index 0000000..78f7995 --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/simple.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Ford Motor Company +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtRemoteObjects module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "simple.h" + +Simple::Simple(QObject *parent) : SimpleSimpleSource(parent) +{ +} + +void Simple::reset() +{ + setI(0); + m_timerId = startTimer(500); +} + +void Simple::timerEvent(QTimerEvent *) +{ + auto count = i(); + setI(count + 1); + if (count >= 3) { + killTimer(m_timerId); + } +} diff --git a/examples/remoteobjects/cbor_python/qt_source/simple.h b/examples/remoteobjects/cbor_python/qt_source/simple.h new file mode 100644 index 0000000..c69934b --- /dev/null +++ b/examples/remoteobjects/cbor_python/qt_source/simple.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Ford Motor Company +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtRemoteObjects module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SIMPLE_H +#define SIMPLE_H + +#include "rep_Simple_source.h" + +class Simple : public SimpleSimpleSource +{ + Q_OBJECT +public: + explicit Simple(QObject *parent = nullptr); + +signals: + +public slots: + void reset() override; + void timerEvent(QTimerEvent *) override; + +private: + int m_timerId; +}; + +#endif // SIMPLE_H diff --git a/examples/remoteobjects/pyside/replica.py b/examples/remoteobjects/pyside/replica.py new file mode 100644 index 0000000..a0c7843 --- /dev/null +++ b/examples/remoteobjects/pyside/replica.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2021 Ford Motor Company +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the QtRemoteObjects module of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## BSD License Usage +## Alternatively, you may use this file under the terms of the BSD license +## as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import signal +import sys +from PySide6.QtCore import QCoreApplication, QUrl +from PySide6.QtRemoteObjects import QRemoteObjectNode +from simple import SimpleReplica + +if __name__ == '__main__': + app = QCoreApplication(sys.argv) + node = QRemoteObjectNode() + node.connectToNode(QUrl("tcp://127.0.0.1:5005")) + # TODO: Fix PySide so we can use acquire() here + rep = SimpleReplica(node, "DifferentName") + def init(): + print(f'after initialization i = {rep.i} f = {rep.f}') + rep.reset() + rep.pushI(42) + rep.iChanged.connect(lambda i: print(f'Updated i received: {i}')) + # Changing f doesn't work from C++ due to float being 4 bytes in C++ and 8 in python + rep.f = 4.5 + rep.fChanged.connect(lambda f: print(f'Updated f received: {f}')) + print(f'start: i = {rep.i} f = {rep.f}') + + rep.initialized.connect(lambda: init()) + signal.signal(signal.SIGINT, signal.SIG_DFL) + sys.exit(app.exec()) diff --git a/examples/remoteobjects/pyside/simple.py b/examples/remoteobjects/pyside/simple.py new file mode 100644 index 0000000..bd56f7c --- /dev/null +++ b/examples/remoteobjects/pyside/simple.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2021 Ford Motor Company +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the QtRemoteObjects module of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## BSD License Usage +## Alternatively, you may use this file under the terms of the BSD license +## as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +''' +TODO: Remove before merging. This is an example generated by the updated + repc to show what the generated code would look like and simplify + trying the examples. +''' + +from abc import abstractmethod, ABC, ABCMeta +from typing import TypeVar, Generic, Iterator +from PySide6.QtCore import (ClassInfo, Property, QMetaObject, + QObject, Signal, Slot) +from PySide6.QtRemoteObjects import QRemoteObjectReplica + +QObjectType = type(QObject) +T = TypeVar('T') + +class QABCMeta(QObjectType, ABCMeta): + pass + +@ClassInfo({'RemoteObject Type':'Simple', 'RemoteObject Signature':'c6f33edb0554ba4241aad1286a47c8189d65c845'}) +class SimpleSource(QObject, Generic[T], metaclass=QABCMeta): + iChanged = Signal(int) + fChanged = Signal(float) + random = Signal(int) + + def __init__(self, parent=None): + super().__init__(parent) + + def _getI(self): + return self.getI() + + @abstractmethod + def getI(self): + pass + + def _setI(self, i): + return self.setI(i) + + @abstractmethod + def setI(self, i): + pass + + @abstractmethod + def pushI(self, i): + pass + + @Slot(int, name='pushI') + def _pushI(self, i): + return self.pushI(i) + + i = Property(int, _getI, _setI, notify = iChanged) + + def _getF(self): + return self.getF() + + @abstractmethod + def getF(self): + pass + + def _setF(self, f): + return self.setF(f) + + @abstractmethod + def setF(self, f): + pass + + f = Property(float, _getF, _setF, notify = fChanged) + + + @abstractmethod + def reset(self): + pass + + @Slot(name='reset') + def _reset(self): + return self.reset() + +class SimpleSimpleSource(SimpleSource): + + def __init__(self, parent=None): + super().__init__(parent) + self._i = int(2) + self._f = float(-1.) + + def getI(self): + return self._i + + def pushI(self, i): + self.setI(i) + + def setI(self, i): + if i != self._i: + self._i = i + self.iChanged.emit(i) + + def getF(self): + return self._f + + def setF(self, f): + if f != self._f: + self._f = f + self.fChanged.emit(f) + +@ClassInfo({'RemoteObject Type':'Simple', 'RemoteObject Signature':'c6f33edb0554ba4241aad1286a47c8189d65c845'}) +class SimpleReplica(QRemoteObjectReplica): + iChanged = Signal(int) + fChanged = Signal(float) + random = Signal(int) + pushI_index = -1 + setF_index = -1 + reset_index = -1 + + def __init__(self, node=None, name=None): + super().__init__() + self.initialize() + if SimpleReplica.pushI_index == -1: + SimpleReplica.pushI_index = self.metaObject().indexOfSlot('pushI(int)') + SimpleReplica.setF_index = self.metaObject().indexOfProperty('f') + SimpleReplica.reset_index = self.metaObject().indexOfSlot('reset()') + if node: + self.initializeNode(node, name) + + def initialize(self): + self.setProperties([int(2), float(-1.)]) + + def getI(self): + return self.propAsVariant(0) + + @Slot(int) + def pushI(self, i): + self.send(QMetaObject.InvokeMetaMethod, SimpleReplica.pushI_index, [i]) + + i = Property(int, getI, notify = iChanged) + + def getF(self): + return self.propAsVariant(1) + + def setF(self, f): + self.send(QMetaObject.WriteProperty, SimpleReplica.setF_index, [f]) + + f = Property(float, getF, setF, notify = fChanged) + + @Slot() + def reset(self): + self.send(QMetaObject.InvokeMetaMethod, SimpleReplica.reset_index, []) + diff --git a/examples/remoteobjects/pyside/source.py b/examples/remoteobjects/pyside/source.py new file mode 100644 index 0000000..f470d2a --- /dev/null +++ b/examples/remoteobjects/pyside/source.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2021 Ford Motor Company +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the QtRemoteObjects module of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## BSD License Usage +## Alternatively, you may use this file under the terms of the BSD license +## as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import sys +import signal +from PySide6.QtCore import QCoreApplication, QUrl, Slot +from PySide6.QtRemoteObjects import QRemoteObjectHost +from simple import SimpleSource, SimpleSimpleSource + + +class Simple(SimpleSimpleSource): + + def __init__(self, _i, _f): + super().__init__() + self.setI(_i) + self.setF(_f) + + @Slot() + def reset(self): + print(f'Derived Reset() called') + + +if __name__ == '__main__': + app = QCoreApplication(sys.argv) + node = QRemoteObjectHost(QUrl("tcp://127.0.0.1:5005")) + toosimple = SimpleSource() # This should fail when abc is working + simple = SimpleSimpleSource() # This should fail, too + working = Simple(3, 3.14) + print(f'initial: i = {working.i}, f = {working.f}') + node.enableRemoting(working, "DifferentName") + signal.signal(signal.SIGINT, signal.SIG_DFL) + sys.exit(app.exec()) diff --git a/examples/remoteobjects/remoteobjects.pro b/examples/remoteobjects/remoteobjects.pro index 9b22d21..10a6e80 100644 --- a/examples/remoteobjects/remoteobjects.pro +++ b/examples/remoteobjects/remoteobjects.pro @@ -3,6 +3,7 @@ CONFIG += debug_and_release ordered SUBDIRS = \ server \ cppclient \ + cbor_python \ simpleswitch \ websockets diff --git a/tools/repc/CMakeLists.txt b/tools/repc/CMakeLists.txt index a0fb8a5..9e22e51 100644 --- a/tools/repc/CMakeLists.txt +++ b/tools/repc/CMakeLists.txt @@ -14,6 +14,7 @@ qt_internal_add_tool(${target_name} cppcodegenerator.cpp cppcodegenerator.h main.cpp repcodegenerator.cpp repcodegenerator.h + pythoncodegenerator.cpp pythoncodegenerator.h utils.cpp utils.h DEFINES QT_NO_CAST_FROM_ASCII diff --git a/tools/repc/main.cpp b/tools/repc/main.cpp index 6c402ea..3d57321 100644 --- a/tools/repc/main.cpp +++ b/tools/repc/main.cpp @@ -35,6 +35,7 @@ #include <qjsonarray.h> #include "cppcodegenerator.h" +#include "pythoncodegenerator.h" #include "repcodegenerator.h" #include "repparser.h" #include "utils.h" @@ -50,11 +51,18 @@ enum Mode { OutRep = 4, OutSource = 8, OutReplica = 16, - OutMerged = OutSource | OutReplica + OutPython = 32, + OutPyside = 64, + OutMerged = OutSource | OutReplica, + OutPyVariant = OutPython | OutPyside, + OutSelected = OutRep | OutMerged | OutPyVariant }; static const QLatin1String REP("rep"); static const QLatin1String JSON("json"); +static const QLatin1String PY("py"); +static const QLatin1String PYTHON("python"); +static const QLatin1String PYSIDE("pyside"); static const QLatin1String REPLICA("replica"); static const QLatin1String SOURCE("source"); static const QLatin1String MERGED("merged"); @@ -140,6 +148,10 @@ int main(int argc, char **argv) mode |= OutSource; else if (outputType == MERGED) mode |= OutMerged; + else if (outputType == PY || outputType == PYTHON) + mode |= OutPython; + else if (outputType == PYSIDE) + mode |= OutPyside; else { fprintf(stderr, PROGRAM_NAME ": Unknown output type\"%s\".\n", qPrintable(outputType)); parser.showHelp(1); @@ -149,10 +161,12 @@ int main(int argc, char **argv) switch (files.count()) { case 2: outputFile = files.last(); - if (!(mode & (OutRep | OutSource | OutReplica))) { + if (!(mode & (OutRep | OutSource | OutReplica | OutPython | OutPyside))) { // try to figure out the Out mode from file extension if (outputFile.endsWith(QLatin1String(".rep"))) mode |= OutRep; + if (outputFile.endsWith(QLatin1String(".py"))) + mode |= OutPython; } Q_FALLTHROUGH(); case 1: @@ -171,7 +185,7 @@ int main(int argc, char **argv) fprintf(stderr, PROGRAM_NAME ": Unknown input type, please use -i option to specify one.\n"); parser.showHelp(1); } - if (!(mode & (OutRep | OutSource | OutReplica))) { + if (!(mode & (OutSelected))) { fprintf(stderr, PROGRAM_NAME ": Unknown output type, please use -o option to specify one.\n"); parser.showHelp(1); } @@ -228,10 +242,15 @@ int main(int argc, char **argv) if (mode & OutRep) { CppCodeGenerator generator(&output); generator.generate(classes, parser.isSet(alwaysClassOption)); - } else { - Q_ASSERT(mode & OutReplica); + } else if (mode & OutReplica) { RepCodeGenerator generator(&output); - generator.generate(classList2AST(classes), RepCodeGenerator::REPLICA, outputFile); + generator.generate(classList2AST(classes), GeneratorBase::Mode::Replica, outputFile); + } else if (mode & OutPyVariant) { + PythonCodeGenerator generator(&output); + if (mode & OutPython) + generator.generate(classList2AST(classes), PythonCodeGenerator::OutputStyle::DataStream); + else + generator.generate(classList2AST(classes), PythonCodeGenerator::OutputStyle::PySide); } } else { Q_ASSERT(!(mode & OutRep)); @@ -249,16 +268,24 @@ int main(int argc, char **argv) input.close(); - RepCodeGenerator generator(&output); - if ((mode & OutMerged) == OutMerged) - generator.generate(repparser.ast(), RepCodeGenerator::MERGED, outputFile); - else if (mode & OutReplica) - generator.generate(repparser.ast(), RepCodeGenerator::REPLICA, outputFile); - else if (mode & OutSource) - generator.generate(repparser.ast(), RepCodeGenerator::SOURCE, outputFile); - else { - fprintf(stderr, PROGRAM_NAME ": Unknown mode.\n"); - return 1; + if (mode & OutPyVariant) { + PythonCodeGenerator generator(&output); + if (mode & OutPython) + generator.generate(repparser.ast(), PythonCodeGenerator::OutputStyle::DataStream); + else + generator.generate(repparser.ast(), PythonCodeGenerator::OutputStyle::PySide); + } else { + RepCodeGenerator generator(&output); + if ((mode & OutMerged) == OutMerged) + generator.generate(repparser.ast(), GeneratorBase::Mode::Merged, outputFile); + else if (mode & OutReplica) + generator.generate(repparser.ast(), GeneratorBase::Mode::Replica, outputFile); + else if (mode & OutSource) + generator.generate(repparser.ast(), GeneratorBase::Mode::Source, outputFile); + else { + fprintf(stderr, PROGRAM_NAME ": Unknown mode.\n"); + return 1; + } } } diff --git a/tools/repc/pythoncodegenerator.cpp b/tools/repc/pythoncodegenerator.cpp new file mode 100644 index 0000000..bfd7bef --- /dev/null +++ b/tools/repc/pythoncodegenerator.cpp @@ -0,0 +1,575 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Ford Motor Company +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtRemoteObjects module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "pythoncodegenerator.h" +#include "repparser.h" +#include "utils.h" + +#include <QScopedPointer> +#include <QStringList> +#include <QTextStream> + +typedef QLatin1String l1; + +static QString cap(QString name) +{ + if (!name.isEmpty()) + name[0] = name[0].toUpper(); + return name; +} + +class PythonGenerator : public GeneratorImplBase +{ +public: + PythonGenerator(QTextStream &_stream) : GeneratorImplBase(_stream) {} + bool generateClass(const ASTClass &astClass, Mode mode) override; + void generateEnum(const ASTEnum &) override {} + void generatePod(const POD &) override {} + QString typeLookup(const QString &type) const; + QString paramString(const QStringList ¶ms) const; + QString tupleString(const QStringList &packParams) const; +}; + +class PysideGenerator : public GeneratorImplBase +{ +public: + PysideGenerator(QTextStream &_stream) : GeneratorImplBase(_stream) {} + bool generateClass(const ASTClass &astClass, Mode mode) override; + void generateEnum(const ASTEnum &) override {} + void generatePod(const POD &) override {} + void generatePrefix(const AST &) override; + QString typeLookup(const QString &type) const; + QString paramString(const QStringList ¶ms) const; + QString tupleString(const QStringList &packParams) const; +}; + +bool PythonGenerator::generateClass(const ASTClass &astClass, Mode mode) +{ + if (mode != Mode::Replica) // For now support Replica only + return true; + + bool validClass = true; + QStringList signalNames; + QStringList propertyPackTypes, propertyDefaults; + for (const auto &prop : astClass.properties) { + signalNames << l1("%1Changed").arg(prop.name); + auto packType = typeLookup(prop.type); + if (packType.isEmpty()) { + qWarning() << "Invalid property" << prop.name << "of type" << prop.type + << "as the type isn't convertible to/from python."; + validClass = false; + continue; + } + + propertyPackTypes << packType; + propertyDefaults << (prop.defaultValue.isNull() ? l1("None") : prop.defaultValue); + } + + QList<QStringList> signalPackParameters; + for (const auto &sig : astClass.signalsList) { + signalNames << sig.name; + QStringList packParams; + for (const auto ¶m : sig.params) { + auto paramType = typeLookup(param.type); + if (paramType.isEmpty()) { + qWarning() << "Invalid signal parameter of" << sig.name << "of type" << param.type + << "as the type isn't convertible to/from python."; + validClass = false; + continue; + } + packParams << paramType; + } + signalPackParameters << packParams; + } + + QStringList slotPackReturnValues; + QList<QStringList> slotPackParameters; + for (const auto &slot : astClass.slotsList) { + QStringList packParams; + if (slot.returnType == QLatin1String("void")) { + slotPackReturnValues << QString(); + } else { + auto returnType = typeLookup(slot.returnType); + if (returnType.isEmpty()) { + qWarning() << "Invalid slot return type of" << slot.name << "of type" + << slot.returnType << "as the type isn't convertible to/from python."; + validClass = false; + continue; + } + slotPackReturnValues << returnType; + } + for (const auto ¶m : slot.params) { + auto paramType = typeLookup(param.type); + if (paramType.isEmpty()) { + qWarning() << "Invalid slot parameter of" << slot.name << "of type" << param.type + << "as the type isn't convertible to/from python."; + validClass = false; + continue; + } + packParams << paramType; + } + slotPackParameters << packParams; + } + + if (!validClass) + return false; + + stream << "@dataclass(eq=False)\n"; + stream << "class " << astClass.name << "(Replica):\n"; + if (signalNames.count()) { + stream << " Signals = ['" << signalNames.join(l1("', '")) << "']\n"; + for (const auto &name : signalNames) + stream << " " << name << ": Signal = field(init=False, repr=False, " + << "default_factory=Signal)\n"; + } + + int index = -1; + int methodIndex = 0; + for (const auto &prop : astClass.properties) { + index++; + stream << "\n"; + stream << " @property\n"; + stream << " def " << prop.name << "(self):\n"; + stream << " return self._priv.properties[" << index << "]\n"; + switch (prop.modifier) { + case ASTProperty::Constant: + case ASTProperty::ReadOnly: + case ASTProperty::SourceOnlySetter: + case ASTProperty::ReadPush: + stream << "\n"; + stream << " def push" << cap(prop.name) << "(self, " << prop.name << "):\n"; + stream << " print('push" << cap(prop.name) << "', " << prop.name << ")\n"; + stream << " self.callSlot(" << methodIndex++ << ", " << prop.name << ")\n"; + break; + case ASTProperty::ReadWrite: + stream << "\n"; + stream << " @" << prop.name << ".setter\n"; + stream << " def " << prop.name << "(self, " << prop.name << "):\n"; + stream << " print('" << prop.name << " setter', " << prop.name << ")\n"; + stream << " self.callSetter(" << index << ", " << prop.name << ")\n"; + break; + } + } + + for (const auto &slot : astClass.slotsList) { + static bool firstLine = true; + if (firstLine) { + stream << "\n"; + firstLine = false; + } + QStringList paramNames; + for (const auto ¶m : slot.params) + paramNames << param.name; + auto parameters = paramString(paramNames); + stream << " def " << slot.name << "(self" << parameters << "):\n"; + stream << " print('Calling " << slot.name << " slot')\n"; + stream << " self.callSlot(" << methodIndex++ << parameters << ")\n"; + } + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def defaults(cls):\n"; + stream << " return [" << propertyDefaults.join(l1(", ")) << "]\n"; + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def propTypes(cls):\n"; + stream << " return [\n"; + for (int i = 0; i < astClass.properties.count(); i++) { + const auto prop = astClass.properties.at(i); + stream << " ('" << propertyPackTypes.at(i) << "',), # " << prop.name + << " is of type " << prop.type << " -> " << propertyPackTypes.at(i) << "\n"; + } + stream << " ]\n"; + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def signalTypes(cls):\n"; + stream << " return [\n"; + for (int i = 0; i < astClass.properties.count(); i++) + stream << " ('" << propertyPackTypes.at(i) << "',),\n"; + for (int i = 0; i < astClass.signalsList.count(); i++) + stream << " " << tupleString(signalPackParameters.at(i)) << ",\n"; + stream << " ]\n"; + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def slotTypes(cls):\n"; + stream << " return [\n"; + for (int i = 0; i < astClass.properties.count(); i++) { + auto prop = astClass.properties.at(i); + if (prop.modifier == ASTProperty::ReadPush) + stream << " ('" << propertyPackTypes.at(i) << "',),\n"; + } + for (int i = 0; i < astClass.slotsList.count(); i++) + stream << " " << tupleString(slotPackParameters.at(i)) << ",\n"; + stream << " ]\n"; + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def signature(cls):\n"; + stream << " return b'" << classSignature(astClass) << "'\n"; + + stream << "\n"; + stream << " @classmethod\n"; + stream << " def name(cls):\n"; + stream << " return '" << astClass.name << "'\n"; + + return true; +} + +QString PythonGenerator::typeLookup(const QString &type) const +{ + const static QMap<QString, QString> primitiveTypes{ + {l1("char"), l1("c")}, + {l1("bool"), l1("?")}, + {l1("short"), l1("h")}, + {l1("unsigned short"), l1("H")}, + {l1("int"), l1("i")}, + {l1("unsigned int"), l1("I")}, + {l1("long"), l1("l")}, + {l1("unsigned long"), l1("L")}, + {l1("long long"), l1("q")}, + {l1("unsigned long long"), l1("Q")}, + {l1("float"), l1("f")}, + {l1("double"), l1("d")}, + }; + if (primitiveTypes.contains(type)) + return primitiveTypes[type]; + + return {}; +} + +QString PythonGenerator::paramString(const QStringList ¶ms) const +{ + // Return a string to add to a function call of parameters + // This will either follow "self" or an index, so it should + // be an empty string or start with a "," + if (params.count() == 0) + return QString(); + + return l1(", %1").arg(params.join(l1(", "))); +} + +QString PythonGenerator::tupleString(const QStringList &packParams) const +{ + // Return a string for a proper python tuple for the classmethods + if (packParams.count() == 0) + return l1("()"); + + if (packParams.count() > 1) + return l1("('%1')").arg(packParams.join(l1("', '"))); + + return l1("('%1',)").arg(packParams.at(0)); +} + +bool PysideGenerator::generateClass(const ASTClass &astClass, Mode mode) +{ + if (mode == Mode::SimpleSource) { + stream << "class " << astClass.name << "SimpleSource(" << astClass.name << "Source):\n\n"; + stream << " def __init__(self, parent=None):\n"; + stream << " super().__init__(parent)\n"; + for (const auto &prop : astClass.properties) { + if (!prop.defaultValue.isNull()) + stream << " self._" << prop.name << " = " << prop.type << "(" + << prop.defaultValue << ")\n"; + } + + for (const auto &prop : astClass.properties) { + stream << "\n"; + stream << " def get" << cap(prop.name) << "(self):\n"; + stream << " return self._" << prop.name << "\n"; + if (hasPush(prop, mode)) + { + stream << "\n"; + stream << " def push" << cap(prop.name) << "(self, " << prop.name << "):\n"; + stream << " self.set" << cap(prop.name) << "(" << prop.name << ")\n"; + } + if (hasSetter(prop, mode)) + { + stream << "\n"; + stream << " def set" << cap(prop.name) << "(self, " << prop.name << "):\n"; + stream << " if " << prop.name << " != self._" << prop.name << ":\n"; + stream << " self._" << prop.name << " = " << prop.name << "\n"; + stream << " self." << prop.name << "Changed.emit(" + << prop.name << ")\n"; + } + } + stream << "\n"; + return true; + } + + stream << "@ClassInfo({'RemoteObject Type':'" << astClass.name + << "', 'RemoteObject Signature':'" << classSignature(astClass) << "'})\n"; + + if (mode == Mode::Source) + stream << "class " << astClass.name + << "Source(QObject, Generic[T], metaclass=QABCMeta):\n"; + else + stream << "class " << astClass.name << "Replica(QRemoteObjectReplica):\n"; + for (const auto &prop : astClass.properties) { + if (hasNotify(prop, mode)) + stream << " " << l1("%1Changed").arg(prop.name) << " = Signal(" + << prop.type << ")\n"; + } + for (const auto &sig : astClass.signalsList) { + if (sig.params.count() == 0) { + stream << " " << sig.name << " = Signal()\n"; + } else { + stream << " " << sig.name << " = Signal(" << sig.params.at(0).type; + for (int index = 1; index < sig.params.count(); index++) + stream << ", " << sig.params.at(index).type; + stream << ")\n"; + } + } + int indexCount = 0; + if (mode == Mode::Replica) { + for (const auto &prop : astClass.properties) { + if (hasPush(prop, mode)) { + indexCount++; + stream << " push" << cap(prop.name) << "_index = -1\n"; + } else if (hasSetter(prop, mode)) { + indexCount++; + stream << " set" << cap(prop.name) << "_index = -1\n"; + } + } + for (const auto &slot : astClass.slotsList) { + stream << " " << slot.name << "_index = -1\n"; + indexCount++; + } + } + stream << "\n"; + if (mode == Mode::Source) { + stream << " def __init__(self, parent=None):\n"; + stream << " super().__init__(parent)\n"; + } else { + stream << " def __init__(self, node=None, name=None):\n"; + stream << " super().__init__()\n"; + stream << " self.initialize()\n"; + if (indexCount > 0) { + bool first = true; + for (const auto &prop : astClass.properties) { + if (hasPush(prop, mode)) { + if (first) { + stream << " if " << astClass.name << "Replica.push" + << cap(prop.name) << "_index == -1:\n"; + first = false; + } + stream << " " << astClass.name << "Replica.push" << cap(prop.name) + << "_index = self.metaObject().indexOfSlot('push" << cap(prop.name) + << "(" << prop.type << ")')\n"; + } else if (hasSetter(prop, mode)) { + if (first) { + stream << " if " << astClass.name << "Replica.set" + << cap(prop.name) << "_index == -1:\n"; + first = false; + } + stream << " " << astClass.name << "Replica.set" << cap(prop.name) + << "_index = self.metaObject().indexOfProperty('" + << prop.name << "')\n"; + } + } + for (const auto &slot : astClass.slotsList) { + if (first) { + stream << " if " << astClass.name << "Replica." << slot.name + << "_index == -1:\n"; + first = false; + } + stream << " " << astClass.name << "Replica." << slot.name + << "_index = self.metaObject().indexOfSlot('" << slot.name << "(" + << slot.paramsAsString(ASTFunction::Normalized) << ")')\n"; + } + + } + stream << " if node:\n"; + stream << " self.initializeNode(node, name)\n\n"; + stream << " def initialize(self):\n"; + stream << " self.setProperties(["; + bool first = true; + for (const auto &prop : astClass.properties) { + if (!first) + stream << ", "; + stream << prop.type << "(" << prop.defaultValue << ")"; + first = false; + } + stream << "])\n"; + } + + int index = -1; + for (const auto &prop : astClass.properties) { + index++; + stream << "\n"; + if (mode == Mode::Source) { + stream << " def _get" << cap(prop.name) << "(self):\n"; + stream << " return self.get" << cap(prop.name) << "()\n\n"; + stream << " @abstractmethod\n"; + } + stream << " def get" << cap(prop.name) << "(self):\n"; + if (mode == Mode::Source) + stream << " pass\n"; + else + stream << " return self.propAsVariant(" << index << ")\n"; + if (hasSetter(prop, mode)) { + stream << "\n"; + if (mode == Mode::Source) { + stream << " def _set" << cap(prop.name) << "(self, " << prop.name << "):\n"; + stream << " return self.set" << cap(prop.name) << "(" << prop.name + << ")\n\n"; + stream << " @abstractmethod\n"; + } + stream << " def set" << cap(prop.name) << "(self, " << prop.name << "):\n"; + if (mode == Mode::Source) + stream << " pass\n"; + else + stream << " self.send(QMetaObject.WriteProperty, " << astClass.name + << "Replica.set" << cap(prop.name) << "_index, [" << prop.name << "])\n"; + } + if (hasPush(prop, mode)) { + stream << "\n"; + if (mode == Mode::Source) + stream << " @abstractmethod\n"; + else + stream << " @Slot(" << prop.type << ")\n"; + stream << " def push" << cap(prop.name) << "(self, " << prop.name << "):\n"; + if (mode == Mode::Source) { + stream << " pass\n\n"; + stream << " @Slot(" << prop.type << ", name='push" << cap(prop.name) << "')\n"; + stream << " def _push" << cap(prop.name) << "(self, " << prop.name << "):\n"; + stream << " return self.push" << cap(prop.name) << "(" << prop.name + << ")\n"; + } else { + stream << " self.send(QMetaObject.InvokeMetaMethod, " << astClass.name + << "Replica.push" << cap(prop.name) << "_index, [" << prop.name << "])\n"; + } + } + stream << "\n"; + if (mode == Mode::Source) + stream << " " << prop.name << " = Property(" << prop.type << ", _get" + << cap(prop.name); + else + stream << " " << prop.name << " = Property(" << prop.type << ", get" + << cap(prop.name); + if (hasSetter(prop, mode)) { + if (mode == Mode::Source) + stream << ", _set" << cap(prop.name); + else + stream << ", set" << cap(prop.name); + } + if (hasNotify(prop, mode)) + stream << ", notify = " << prop.name << "Changed"; + stream << ")\n"; + } + + stream << "\n"; + for (const auto &slot : astClass.slotsList) { + static bool firstLine = true; + if (firstLine) { + stream << "\n"; + firstLine = false; + } + QStringList names, types; + for (const auto ¶m : slot.params) { + names << param.name; + types << param.type; + } + if (mode == Mode::Source) + stream << " @abstractmethod\n"; + else + stream << " @Slot(" << types.join(l1(", ")) << ")\n"; + if (names.count() == 0) + stream << " def " << slot.name << "(self):\n"; + else + stream << " def " << slot.name << "(self, " << names.join(l1(", ")) << "):\n"; + if (mode == Mode::Source) { + stream << " pass\n\n"; + stream << " @Slot(" << types.join(l1(", ")) << (names.count() == 0 ? "" : ", ") + << "name='" << slot.name << "')\n"; + stream << " def _" << slot.name << "(self" << (names.count() == 0 ? "" : ", ") + << names.join(l1(", ")) << "):\n"; + stream << " return self." << slot.name << "(" << names.join(l1(", ")) + << ")\n"; + } else { + stream << " self.send(QMetaObject.InvokeMetaMethod, " << astClass.name + << "Replica." << slot.name << "_index, [" << names.join(l1(", ")) << "])\n"; + } + } + stream << "\n"; + return true; +} + +void PysideGenerator::generatePrefix(const AST &) +{ + stream << "from abc import abstractmethod, ABC, ABCMeta\n"; + stream << "from typing import TypeVar, Generic, Iterator\n"; + stream << "from PySide6.QtCore import (ClassInfo, Property, QMetaObject,\n"; + stream << " QObject, Signal, Slot)\n"; + stream << "from PySide6.QtRemoteObjects import QRemoteObjectReplica\n\n"; + + stream << "QObjectType = type(QObject)\n"; + stream << "T = TypeVar('T')\n\n"; + + stream << "class QABCMeta(QObjectType, ABCMeta):\n"; + stream << " pass\n\n"; +} + +QT_BEGIN_NAMESPACE + +PythonCodeGenerator::PythonCodeGenerator(QIODevice *outputDevice) + : m_outputDevice(outputDevice) +{ + Q_ASSERT(m_outputDevice); +} + +void PythonCodeGenerator::generate(const AST &ast, OutputStyle style) +{ + QTextStream stream(m_outputDevice); + + QScopedPointer<GeneratorImplBase> generator; + if (style == OutputStyle::DataStream) + generator.reset(new PythonGenerator(stream)); + else + generator.reset(new PysideGenerator(stream)); + + generator->generatePrefix(ast); + + for (const ASTEnum &en : ast.enums) + generator->generateEnum(en); + + for (const POD &pod : ast.pods) + generator->generatePod(pod); + + for (const ASTClass &astClass : ast.classes) { + generator->generateClass(astClass, GeneratorBase::Mode::Source); + generator->generateClass(astClass, GeneratorBase::Mode::SimpleSource); + generator->generateClass(astClass, GeneratorBase::Mode::Replica); + } + + generator->generateSuffix(ast); +} + +QT_END_NAMESPACE diff --git a/tools/repc/pythoncodegenerator.h b/tools/repc/pythoncodegenerator.h new file mode 100644 index 0000000..0138b93 --- /dev/null +++ b/tools/repc/pythoncodegenerator.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Ford Motor Company +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtRemoteObjects module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +struct AST; +class QIODevice; + +class PythonCodeGenerator +{ +public: + // Start with minimal QDataStream processing + // Protect for CBOR and PyQt + enum class OutputStyle + { + DataStream, + PySide + }; + + explicit PythonCodeGenerator(QIODevice *outputDevice); + + void generate(const AST &ast, OutputStyle style); + +private: + QIODevice *m_outputDevice; +}; + +QT_END_NAMESPACE diff --git a/tools/repc/repc.pro b/tools/repc/repc.pro index d3bcc43..24d2c68 100644 --- a/tools/repc/repc.pro +++ b/tools/repc/repc.pro @@ -14,11 +14,13 @@ SOURCES += \ main.cpp \ repcodegenerator.cpp \ cppcodegenerator.cpp \ + pythoncodegenerator.cpp \ utils.cpp HEADERS += \ repcodegenerator.h \ cppcodegenerator.h \ + pythoncodegenerator.h \ utils.h QMAKE_TARGET_DESCRIPTION = "Qt Remote Objects Compiler" diff --git a/tools/repc/repcodegenerator.cpp b/tools/repc/repcodegenerator.cpp index ac58130..5fbc4f1 100644 --- a/tools/repc/repcodegenerator.cpp +++ b/tools/repc/repcodegenerator.cpp @@ -27,13 +27,12 @@ ****************************************************************************/ #include "repcodegenerator.h" - #include "repparser.h" +#include "utils.h" #include <QFileInfo> #include <QMetaType> #include <QTextStream> -#include <QCryptographicHash> #include <QRegularExpression> using namespace Qt; @@ -117,66 +116,6 @@ RepCodeGenerator::RepCodeGenerator(QIODevice *outputDevice) Q_ASSERT(m_outputDevice); } -static QByteArray enumSignature(const ASTEnum &e) -{ - QByteArray ret; - ret += e.name.toLatin1(); - for (const ASTEnumParam ¶m : e.params) - ret += param.name.toLatin1() + QByteArray::number(param.value); - return ret; -} - -static QByteArray typeData(const QString &type, const QHash<QString, QByteArray> &specialTypes) -{ - QHash<QString, QByteArray>::const_iterator it = specialTypes.find(type); - if (it != specialTypes.end()) - return it.value(); - const auto pos = type.lastIndexOf(QLatin1String("::")); - if (pos > 0) - return typeData(type.mid(pos + 2), specialTypes); - return type.toLatin1(); -} - -static QByteArray functionsData(const QList<ASTFunction> &functions, const QHash<QString, QByteArray> &specialTypes) -{ - QByteArray ret; - for (const ASTFunction &func : functions) { - ret += func.name.toLatin1(); - for (const ASTDeclaration ¶m : func.params) { - ret += param.name.toLatin1(); - ret += typeData(param.type, specialTypes); - ret += QByteArray(reinterpret_cast<const char *>(¶m.variableType), sizeof(param.variableType)); - } - ret += typeData(func.returnType, specialTypes); - } - return ret; -} - -QByteArray RepCodeGenerator::classSignature(const ASTClass &ac) -{ - QCryptographicHash checksum(QCryptographicHash::Sha1); - QHash<QString, QByteArray> localTypes = m_globalEnumsPODs; - for (const ASTEnum &e : ac.enums) // add local enums - localTypes[e.name] = enumSignature(e); - - checksum.addData(ac.name.toLatin1()); - - // Checksum properties - for (const ASTProperty &p : ac.properties) { - checksum.addData(p.name.toLatin1()); - checksum.addData(typeData(p.type, localTypes)); - checksum.addData(reinterpret_cast<const char *>(&p.modifier), sizeof(p.modifier)); - } - - // Checksum signals - checksum.addData(functionsData(ac.signalsList, localTypes)); - - // Checksum slots - checksum.addData(functionsData(ac.slotsList, localTypes)); - - return checksum.result().toHex(); -} - void RepCodeGenerator::generate(const AST &ast, Mode mode, QString fileName) { QTextStream stream(m_outputDevice); @@ -229,15 +168,15 @@ void RepCodeGenerator::generate(const AST &ast, Mode mode, QString fileName) const QString replicaMetaTypeRegistrationCode = classMetaTypeRegistrationCode + generateMetaTypeRegistrationForPending(pendingMetaTypes); - if (mode == MERGED) { - generateClass(REPLICA, stream, astClass, replicaMetaTypeRegistrationCode); - generateClass(SOURCE, stream, astClass, classMetaTypeRegistrationCode); - generateClass(SIMPLE_SOURCE, stream, astClass, classMetaTypeRegistrationCode); + if (mode == Mode::Merged) { + generateClass(Mode::Replica, stream, astClass, replicaMetaTypeRegistrationCode); + generateClass(Mode::Source, stream, astClass, classMetaTypeRegistrationCode); + generateClass(Mode::SimpleSource, stream, astClass, classMetaTypeRegistrationCode); generateSourceAPI(stream, astClass); } else { - generateClass(mode, stream, astClass, mode == REPLICA ? replicaMetaTypeRegistrationCode : classMetaTypeRegistrationCode); - if (mode == SOURCE) { - generateClass(SIMPLE_SOURCE, stream, astClass, classMetaTypeRegistrationCode); + generateClass(mode, stream, astClass, mode == Mode::Replica ? replicaMetaTypeRegistrationCode : classMetaTypeRegistrationCode); + if (mode == Mode::Source) { + generateClass(Mode::SimpleSource, stream, astClass, classMetaTypeRegistrationCode); generateSourceAPI(stream, astClass); } } @@ -273,13 +212,13 @@ void RepCodeGenerator::generateHeader(Mode mode, QTextStream &out, const AST &as out << "\n" "#include <QtRemoteObjects/qremoteobjectnode.h>\n"; - if (mode == MERGED) { + if (mode == Mode::Merged) { out << "#include <QtRemoteObjects/qremoteobjectpendingcall.h>\n"; out << "#include <QtRemoteObjects/qremoteobjectreplica.h>\n"; out << "#include <QtRemoteObjects/qremoteobjectsource.h>\n"; if (hasModel) out << "#include <QtRemoteObjects/qremoteobjectabstractitemmodelreplica.h>\n"; - } else if (mode == REPLICA) { + } else if (mode == Mode::Replica) { out << "#include <QtRemoteObjects/qremoteobjectpendingcall.h>\n"; out << "#include <QtRemoteObjects/qremoteobjectreplica.h>\n"; if (hasModel) @@ -382,13 +321,13 @@ QString RepCodeGenerator::typeForMode(const ASTProperty &property, RepCodeGenera return property.type; if (property.type.startsWith(QStringLiteral("QAbstractItemModel"))) - return mode == REPLICA ? property.type + QStringLiteral("Replica*") : property.type + QStringLiteral("*"); + return mode == Mode::Replica ? property.type + QStringLiteral("Replica*") : property.type + QStringLiteral("*"); switch (mode) { - case REPLICA: return property.type + QStringLiteral("Replica*"); - case SIMPLE_SOURCE: + case Mode::Replica: return property.type + QStringLiteral("Replica*"); + case Mode::SimpleSource: Q_FALLTHROUGH(); - case SOURCE: return property.type + QStringLiteral("Source*"); + case Mode::Source: return property.type + QStringLiteral("Source*"); default: qCritical("Invalid mode"); } @@ -401,7 +340,7 @@ void RepCodeGenerator::generateSimpleSetter(QTextStream &out, const ASTProperty out << " virtual "; else out << " "; - out << "void set" << cap(property.name) << "(" << typeForMode(property, SIMPLE_SOURCE) << " " << property.name << ")"; + out << "void set" << cap(property.name) << "(" << typeForMode(property, Mode::SimpleSource) << " " << property.name << ")"; if (generateOverride) out << " override"; out << Qt::endl; @@ -415,13 +354,11 @@ void RepCodeGenerator::generateSimpleSetter(QTextStream &out, const ASTProperty void RepCodeGenerator::generatePOD(QTextStream &out, const POD &pod) { - QByteArray podData = pod.name.toLatin1(); + m_globalEnumsPODs[pod.name] = podSignature(pod); QStringList equalityCheck; - for (const PODAttribute &attr : pod.attributes) { + for (const PODAttribute &attr : pod.attributes) equalityCheck << QStringLiteral("left.%1() == right.%1()").arg(attr.name); - podData += attr.name.toLatin1() + typeData(attr.type, m_globalEnumsPODs); - } - m_globalEnumsPODs[pod.name] = podData; + out << "class " << pod.name << "\n" "{\n" " Q_GADGET\n" @@ -606,17 +543,17 @@ void RepCodeGenerator::generateStreamOperatorsForEnums(QTextStream &out, const Q void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass &astClass, const QString &metaTypeRegistrationCode) { - const QString className = (astClass.name + (mode == REPLICA ? QStringLiteral("Replica") : mode == SOURCE ? QStringLiteral("Source") : QStringLiteral("SimpleSource"))); - if (mode == REPLICA) + const QString className = (astClass.name + (mode == Mode::Replica ? QStringLiteral("Replica") : mode == Mode::Source ? QStringLiteral("Source") : QStringLiteral("SimpleSource"))); + if (mode == Mode::Replica) out << "class " << className << " : public QRemoteObjectReplica" << Qt::endl; - else if (mode == SIMPLE_SOURCE) + else if (mode == Mode::SimpleSource) out << "class " << className << " : public " << astClass.name << "Source" << Qt::endl; else out << "class " << className << " : public QObject" << Qt::endl; out << "{" << Qt::endl; out << " Q_OBJECT" << Qt::endl; - if (mode != SIMPLE_SOURCE) { + if (mode != Mode::SimpleSource) { out << " Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, \"" << astClass.name << "\")" << Qt::endl; out << " Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_SIGNATURE, \"" << QLatin1String(classSignature(astClass)) << "\")" << Qt::endl; for (int i = 0; i < astClass.modelMetadata.count(); i++) { @@ -635,7 +572,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass for (const ASTProperty &property : astClass.properties) { out << " Q_PROPERTY(" << typeForMode(property, mode) << " " << property.name << " READ " << property.name; if (property.modifier == ASTProperty::Constant) { - if (mode == REPLICA) // We still need to notify when we get the initial value + if (mode == Mode::Replica) // We still need to notify when we get the initial value out << " NOTIFY " << property.name << "Changed"; else out << " CONSTANT"; @@ -644,7 +581,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass else if (property.modifier == ASTProperty::ReadWrite) out << " WRITE set" << cap(property.name) << " NOTIFY " << property.name << "Changed"; else if (property.modifier == ASTProperty::ReadPush || property.modifier == ASTProperty::SourceOnlySetter) { - if (mode == REPLICA) // The setter slot isn't known to the PROP + if (mode == Mode::Replica) // The setter slot isn't known to the PROP out << " NOTIFY " << property.name << "Changed"; else // The Source can use the setter, since non-asynchronous out << " WRITE set" << cap(property.name) << " NOTIFY " << property.name << "Changed"; @@ -662,7 +599,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass out << "" << Qt::endl; out << "public:" << Qt::endl; - if (mode == REPLICA) { + if (mode == Mode::Replica) { out << " " << className << "() : QRemoteObjectReplica() { initialize(); }" << Qt::endl; out << " static void registerMetatypes()" << Qt::endl; out << " {" << Qt::endl; @@ -744,7 +681,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass } out << " setProperties(properties);" << Qt::endl; out << " }" << Qt::endl; - } else if (mode == SOURCE) { + } else if (mode == Mode::Source) { out << " explicit " << className << "(QObject *parent = nullptr) : QObject(parent)" << Qt::endl; out << " {" << Qt::endl; if (!metaTypeRegistrationCode.isEmpty()) @@ -763,7 +700,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass QStringList parameters; for (int index : constIndices) { const ASTProperty &property = astClass.properties.at(index); - parameters.append(QString::fromLatin1("%1 %2 = %3").arg(typeForMode(property, SOURCE), property.name, property.defaultValue)); + parameters.append(QString::fromLatin1("%1 %2 = %3").arg(typeForMode(property, Mode::Source), property.name, property.defaultValue)); } parameters.append(QStringLiteral("QObject *parent = nullptr")); out << " explicit " << className << "(" << parameters.join(QStringLiteral(", ")) << ") : " << astClass.name << "Source(parent)" << Qt::endl; @@ -781,7 +718,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass out << "" << Qt::endl; out << "public:" << Qt::endl; - if (mode == REPLICA && astClass.hasPersisted) { + if (mode == Mode::Replica && astClass.hasPersisted) { out << " ~" << className << "() override {" << Qt::endl; out << " QVariantList persisted;" << Qt::endl; for (int i = 0; i < astClass.properties.size(); i++) { @@ -796,11 +733,11 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass } out << "" << Qt::endl; - if (mode != SIMPLE_SOURCE) + if (mode != Mode::SimpleSource) generateConversionFunctionsForEnums(out, astClass.enums); //Next output getter/setter - if (mode == REPLICA) { + if (mode == Mode::Replica) { int i = 0; for (const ASTProperty &property : astClass.properties) { auto type = typeForMode(property, mode); @@ -832,7 +769,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass } out << "" << Qt::endl; } - } else if (mode == SOURCE) { + } else if (mode == Mode::Source) { for (const ASTProperty &property : astClass.properties) out << " virtual " << typeForMode(property, mode) << " " << property.name << "() const = 0;" << Qt::endl; for (const ASTProperty &property : astClass.properties) { @@ -854,7 +791,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass } } - if (mode != SIMPLE_SOURCE) { + if (mode != Mode::SimpleSource) { //Next output property signals if (!astClass.properties.isEmpty() || !astClass.signalsList.isEmpty()) { out << "" << Qt::endl; @@ -872,7 +809,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass // update (once) when the value is initialized. Put these last, so they don't mess // up the signal index order for (const ASTProperty &property : astClass.properties) { - if (mode == REPLICA && property.modifier == ASTProperty::Constant) + if (mode == Mode::Replica && property.modifier == ASTProperty::Constant) out << " void " << property.name << "Changed(" << fullyQualifiedTypeName(astClass, className, typeForMode(property, mode)) << " " << property.name << ");" << Qt::endl; } } @@ -889,7 +826,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass for (const ASTProperty &property : astClass.properties) { if (property.modifier == ASTProperty::ReadPush) { const auto type = fullyQualifiedTypeName(astClass, className, property.type); - if (mode != REPLICA) { + if (mode != Mode::Replica) { out << " virtual void push" << cap(property.name) << "(" << type << " " << property.name << ")" << Qt::endl; out << " {" << Qt::endl; out << " set" << cap(property.name) << "(" << property.name << ");" << Qt::endl; @@ -908,7 +845,7 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass const QList<ASTFunction> slotsList = transformEnumParams(astClass, astClass.slotsList, className); for (const ASTFunction &slot : slotsList) { const auto returnType = fullyQualifiedTypeName(astClass, className, slot.returnType); - if (mode != REPLICA) { + if (mode != Mode::Replica) { out << " virtual " << returnType << " " << slot.name << "(" << slot.paramsAsString() << ") = 0;" << Qt::endl; } else { // TODO: Discuss whether it is a good idea to special-case for void here, @@ -956,18 +893,18 @@ void RepCodeGenerator::generateClass(Mode mode, QTextStream &out, const ASTClass out << "private:" << Qt::endl; //Next output data members - if (mode == SIMPLE_SOURCE) { + if (mode == Mode::SimpleSource) { for (const ASTProperty &property : astClass.properties) - out << " " << typeForMode(property, SOURCE) << " " << "m_" << property.name << ";" << Qt::endl; + out << " " << typeForMode(property, Mode::Source) << " " << "m_" << property.name << ";" << Qt::endl; } - if (mode != SIMPLE_SOURCE) + if (mode != Mode::SimpleSource) out << " friend class QT_PREPEND_NAMESPACE(QRemoteObjectNode);" << Qt::endl; out << "};" << Qt::endl; out << "" << Qt::endl; - if (mode != SIMPLE_SOURCE) + if (mode != Mode::SimpleSource) generateStreamOperatorsForEnums(out, astClass.enums, className); out << "" << Qt::endl; @@ -1007,7 +944,7 @@ void RepCodeGenerator::generateSourceAPI(QTextStream &out, const ASTClass &astCl QList<qsizetype> propertyChangeIndex; for (qsizetype i = 0; i < propCount; ++i) { const ASTProperty &prop = astClass.properties.at(i); - const QString propTypeName = fullyQualifiedTypeName(astClass, QStringLiteral("typename ObjectType"), typeForMode(prop, SOURCE)); + const QString propTypeName = fullyQualifiedTypeName(astClass, QStringLiteral("typename ObjectType"), typeForMode(prop, Mode::Source)); out << QString::fromLatin1(" m_properties[%1] = QtPrivate::qtro_property_index<ObjectType>(&ObjectType::%2, " "static_cast<%3 (QObject::*)()>(nullptr),\"%2\");") .arg(QString::number(i+1), prop.name, propTypeName) << Qt::endl; @@ -1028,7 +965,7 @@ void RepCodeGenerator::generateSourceAPI(QTextStream &out, const ASTClass &astCl out << QString::fromLatin1(" m_signals[%1] = QtPrivate::qtro_signal_index<ObjectType>(&ObjectType::%2Changed, " "static_cast<void (QObject::*)(%3)>(nullptr),m_signalArgCount+%4,&m_signalArgTypes[%4]);") .arg(QString::number(i+1), onChangeProperties.at(i).name, - fullyQualifiedTypeName(astClass, QStringLiteral("typename ObjectType"), typeForMode(onChangeProperties.at(i), SOURCE)), + fullyQualifiedTypeName(astClass, QStringLiteral("typename ObjectType"), typeForMode(onChangeProperties.at(i), Mode::Source)), QString::number(i)) << Qt::endl; QList<ASTFunction> signalsList = transformEnumParams(astClass, astClass.signalsList, QStringLiteral("typename ObjectType")); @@ -1188,7 +1125,7 @@ void RepCodeGenerator::generateSourceAPI(QTextStream &out, const ASTClass &astCl .arg(QString::number(i), prop.name, prop.type) << Qt::endl; else out << QString::fromLatin1(" case %1: return QByteArrayLiteral(\"%2Changed(%3)\");") - .arg(QString::number(i), prop.name, typeForMode(prop, SOURCE)) << Qt::endl; + .arg(QString::number(i), prop.name, typeForMode(prop, Mode::Source)) << Qt::endl; } for (int i = 0; i < signalCount; ++i) { diff --git a/tools/repc/repcodegenerator.h b/tools/repc/repcodegenerator.h index 7a727e4..d345405 100644 --- a/tools/repc/repcodegenerator.h +++ b/tools/repc/repcodegenerator.h @@ -33,6 +33,8 @@ #include <QSet> #include <QString> +#include "utils.h" + QT_BEGIN_NAMESPACE struct AST; struct ASTClass; @@ -43,22 +45,13 @@ struct ASTProperty; class QIODevice; class QTextStream; -class RepCodeGenerator +class RepCodeGenerator : public GeneratorBase { public: - enum Mode - { - REPLICA, - SOURCE, - SIMPLE_SOURCE, - MERGED - }; - explicit RepCodeGenerator(QIODevice *outputDevice); void generate(const AST &ast, Mode mode, QString fileName); - QByteArray classSignature(const ASTClass &ac); private: void generateHeader(Mode mode, QTextStream &out, const AST &ast); QString generateMetaTypeRegistration(const QSet<QString> &metaTypes); @@ -85,7 +78,6 @@ private: private: QIODevice *m_outputDevice; - QHash<QString, QByteArray> m_globalEnumsPODs; }; QT_END_NAMESPACE diff --git a/tools/repc/utils.cpp b/tools/repc/utils.cpp index b6260a3..946f394 100644 --- a/tools/repc/utils.cpp +++ b/tools/repc/utils.cpp @@ -26,12 +26,13 @@ ** ****************************************************************************/ -#include <qjsonvalue.h> -#include <qjsonarray.h> -#include <qjsonobject.h> +#include <QCryptographicHash> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonValue> -#include "utils.h" #include "repparser.h" +#include "utils.h" #define _(X) QLatin1String(X) @@ -286,4 +287,136 @@ AST classList2AST(const QJsonArray &classes) return ret; } +static QByteArray typeData(const QString &type, const QHash<QString, QByteArray> &specialTypes) +{ + QHash<QString, QByteArray>::const_iterator it = specialTypes.find(type); + if (it != specialTypes.end()) + return it.value(); + const auto pos = type.lastIndexOf(QLatin1String("::")); + if (pos > 0) + return typeData(type.mid(pos + 2), specialTypes); + return type.toLatin1(); +} + +static QByteArray functionsData(const QList<ASTFunction> &functions, const QHash<QString, QByteArray> &specialTypes) +{ + QByteArray ret; + for (const ASTFunction &func : functions) { + ret += func.name.toLatin1(); + for (const ASTDeclaration ¶m : func.params) { + ret += param.name.toLatin1(); + ret += typeData(param.type, specialTypes); + ret += QByteArray(reinterpret_cast<const char *>(¶m.variableType), sizeof(param.variableType)); + } + ret += typeData(func.returnType, specialTypes); + } + return ret; +} + +GeneratorBase::GeneratorBase() +{ +} + +GeneratorBase::~GeneratorBase() +{ + +} + +QByteArray GeneratorBase::enumSignature(const ASTEnum &e) +{ + QByteArray ret; + ret += e.name.toLatin1(); + for (const ASTEnumParam ¶m : e.params) + ret += param.name.toLatin1() + QByteArray::number(param.value); + return ret; +} + +QByteArray GeneratorBase::classSignature(const ASTClass &ac) +{ + QCryptographicHash checksum(QCryptographicHash::Sha1); + auto specialTypes = m_globalEnumsPODs; + for (const ASTEnum &e : ac.enums) // add local enums + specialTypes[e.name] = enumSignature(e); + + checksum.addData(ac.name.toLatin1()); + + // Checksum properties + for (const ASTProperty &p : ac.properties) { + checksum.addData(p.name.toLatin1()); + checksum.addData(typeData(p.type, specialTypes)); + checksum.addData(reinterpret_cast<const char *>(&p.modifier), sizeof(p.modifier)); + } + + // Checksum signals + checksum.addData(functionsData(ac.signalsList, specialTypes)); + + // Checksum slots + checksum.addData(functionsData(ac.slotsList, specialTypes)); + + return checksum.result().toHex(); +} + +QByteArray GeneratorBase::podSignature(const POD &pod) +{ + QByteArray podData = pod.name.toLatin1(); + for (const PODAttribute &attr : pod.attributes) + podData += attr.name.toLatin1() + typeData(attr.type, m_globalEnumsPODs); + + return podData; +} + +GeneratorImplBase::GeneratorImplBase(QTextStream &_stream) : stream(_stream) +{ +} + +bool GeneratorImplBase::hasNotify(const ASTProperty &property, Mode mode) +{ + switch (property.modifier) { + case ASTProperty::Constant: + if (mode == Mode::Replica) // We still need to notify when we get the initial value + return true; + else + return false; + case ASTProperty::ReadOnly: + case ASTProperty::ReadWrite: + case ASTProperty::ReadPush: + case ASTProperty::SourceOnlySetter: + return true; + } + Q_UNREACHABLE(); +} + +bool GeneratorImplBase::hasPush(const ASTProperty &property, Mode mode) +{ + Q_UNUSED(mode) + switch (property.modifier) { + case ASTProperty::ReadPush: + return true; + case ASTProperty::Constant: + case ASTProperty::ReadOnly: + case ASTProperty::ReadWrite: + case ASTProperty::SourceOnlySetter: + return false; + } + Q_UNREACHABLE(); +} + +bool GeneratorImplBase::hasSetter(const ASTProperty &property, Mode mode) +{ + switch (property.modifier) { + case ASTProperty::Constant: + case ASTProperty::ReadOnly: + return false; + case ASTProperty::ReadWrite: + return true; + case ASTProperty::ReadPush: + case ASTProperty::SourceOnlySetter: + if (mode == Mode::Replica) // The setter slot isn't known to the PROP + return false; + else // The Source can use the setter, since non-asynchronous + return true; + } + Q_UNREACHABLE(); +} + QT_END_NAMESPACE diff --git a/tools/repc/utils.h b/tools/repc/utils.h index 236f60e..bdbfa9d 100644 --- a/tools/repc/utils.h +++ b/tools/repc/utils.h @@ -30,14 +30,66 @@ #define UTILS_H #include <QByteArray> +#include <QHash> +#include <QString> +#include <QTextStream> QT_BEGIN_NAMESPACE + class QJsonValue; class QJsonArray; struct AST; +struct ASTClass; +struct ASTEnum; +struct ASTProperty; +struct POD; QByteArray generateClass(const QJsonValue &cls, bool alwaysGenerateClass = false); AST classList2AST(const QJsonArray &classes); + +// The Qt/C++ generator is not easy to refactor at this point, so GeneratorBase +// has the shared components used by all generators +class GeneratorBase +{ +public: + + enum class Mode + { + Replica, + Source, + SimpleSource, + Merged + }; + + GeneratorBase(); + virtual ~GeneratorBase(); + +protected: + QByteArray classSignature(const ASTClass &ac); + QByteArray enumSignature(const ASTEnum &e); + QByteArray podSignature(const POD &pod); + QHash<QString, QByteArray> m_globalEnumsPODs; +}; + +// GeneratorImplBase has shared components used by new generators +class GeneratorImplBase : public GeneratorBase +{ +public: + GeneratorImplBase(QTextStream &_stream); + + virtual bool generateClass(const ASTClass &astClass, Mode mode) = 0; + virtual void generateEnum(const ASTEnum &astEnum) = 0; + virtual void generatePod(const POD &pod) = 0; + virtual void generatePrefix(const AST &) {} + virtual void generateSuffix(const AST &) {} + bool hasNotify(const ASTProperty &property, Mode mode); + bool hasPush(const ASTProperty &property, Mode mode); + bool hasSetter(const ASTProperty &property, Mode mode); + +protected: + QTextStream &stream; +}; + QT_END_NAMESPACE #endif // UTILS_H |