From 5dc55d5230b5bec77d89581bebad3737b620c922 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 20 Apr 2024 01:06:51 +0100 Subject: [PATCH] Initial commit --- .clang-format | 65 ++++++ .gitignore | 35 ++++ CMakeLists.txt | 40 ++++ LICENSE | 28 +++ README.md | 19 ++ src/main.cpp | 555 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 742 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 src/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8423e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +build/ +result* +/.vscode/ +/.idea/ +.envrc +.cache + +*.o +*-protocol.c +*-protocol.h +.ccls-cache +*.so + +hyprctl/hyprctl + +gmon.out +*.out +*.tar.gz + +PKGBUILD + +src/version.h + +.direnv diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1ba7cbd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.19) + +set(VERSION 0.1.0) + +project(hyprwayland-scanner + DESCRIPTION "A hyprland version of wayland-scanner in and for C++" + VERSION ${VERSION} +) + +set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Configuring hyprwayland-scanner in Debug with CMake") + add_compile_definitions(HYPRLAND_DEBUG) +else() + add_compile_options(-O3) + message(STATUS "Configuring hyprwayland-scanner in Release with CMake") +endif() + + +# configure +set(CMAKE_CXX_STANDARD 23) +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value + -Wno-missing-field-initializers -Wno-narrowing) + +add_compile_definitions(SCANNER_VERSION="${VERSION}") + +# dependencies +message(STATUS "Checking deps...") + +find_package(Threads REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET pugixml) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") +add_executable(hyprwayland-scanner ${SRCFILES}) +target_link_libraries(hyprwayland-scanner PRIVATE rt Threads::Threads PkgConfig::deps) + +# Installation +install(TARGETS hyprwayland-scanner) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d405623 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Hypr Development + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder 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 HOLDER 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. diff --git a/README.md b/README.md index 0ec659d..3e0a60b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # hyprwayland-scanner A Hyprland implementation of wayland-scanner, in and for C++. + +## Usage + +```sh +hyprwayland-scanner '/path/to/proto' '/path/to/output/directory' +``` + +## Building + +```sh +cmake -B build +cmake --build build -j`nproc` +``` + +### Installation + +```sh +sudo cmake --install build +``` diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..8375e60 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,555 @@ +#include +#include +#include +#include +#include +#include +#include + +struct SRequestArgument { + std::string CType; + std::string wlType; + std::string interface; + std::string name; +}; + +struct SRequest { + std::vector args; + std::string name; +}; + +struct SEvent { + std::vector args; + std::string name; +}; + +struct SInterface { + std::vector requests; + std::vector events; + std::string name; + int version = 1; +}; + +struct { + std::vector ifaces; +} XMLDATA; + +std::string argsToShort(std::vector& args) { + std::string shortt = ""; + for (auto& a : args) { + if (a.wlType == "int") + shortt += "i"; + else if (a.wlType == "new_id") { + if (a.interface.empty()) + shortt += "su"; + shortt += "n"; + } else if (a.wlType == "uint") + shortt += "u"; + else if (a.wlType == "fixed") + shortt += "f"; + else if (a.wlType == "string") + shortt += "s"; + else if (a.wlType == "object") + shortt += "o"; + else if (a.wlType == "array") + shortt += "a"; + else if (a.wlType == "fd") + shortt += "h"; + else + throw std::runtime_error("Unknown arg in argsToShort"); + } + return shortt; +} + +std::string WPTypeToCType(const std::string& wptype) { + if (wptype == "uint" || wptype == "new_id") + return "uint32_t"; + if (wptype == "object") + return "wl_resource*"; + if (wptype == "int" || wptype == "fd") + return "int32_t"; + if (wptype == "fixed") + return "wl_fixed_t"; + if (wptype == "array") + return "wl_array*"; + if (wptype == "string") + return "const char*"; + throw std::runtime_error("unknown wp type"); + return ""; +} + +std::string HEADER; +std::string SOURCE; + +std::string camelize(std::string snake) { + std::string result = ""; + for (size_t i = 0; i < snake.length(); ++i) { + if (snake[i] == '_' && i != 0 && i + 1 < snake.length() && snake[i + 1] != '_') { + result += ::toupper(snake[i + 1]); + i++; + continue; + } + + result += snake[i]; + } + + return result; +} + +struct { + std::string name; + std::string nameOriginal; +} PROTO_DATA; + +void parseXML(pugi::xml_document& doc) { + for (auto& iface : doc.child("protocol").children("interface")) { + SInterface ifc; + ifc.name = iface.attribute("name").as_string(); + ifc.version = iface.attribute("version").as_int(); + + for (auto& rq : iface.children("request")) { + SRequest srq; + srq.name = rq.attribute("name").as_string(); + + for (auto& arg : rq.children("arg")) { + SRequestArgument sargm; + sargm.name = arg.attribute("name").as_string(); + sargm.CType = WPTypeToCType(arg.attribute("type").as_string()); + sargm.wlType = arg.attribute("type").as_string(); + sargm.interface = arg.attribute("interface").as_string(); + + srq.args.push_back(sargm); + } + + ifc.requests.push_back(srq); + } + + for (auto& ev : iface.children("event")) { + SEvent sev; + sev.name = ev.attribute("name").as_string(); + + for (auto& arg : ev.children("arg")) { + SRequestArgument sargm; + sargm.name = arg.attribute("name").as_string(); + sargm.CType = WPTypeToCType(arg.attribute("type").as_string()); + sargm.interface = arg.attribute("interface").as_string(); + sargm.wlType = arg.attribute("type").as_string(); + + sev.args.push_back(sargm); + } + + ifc.events.push_back(sev); + } + + XMLDATA.ifaces.push_back(ifc); + } +} + +void parseHeader() { + + // add some boilerplate + HEADER += R"#(#pragma once + +#include +#include +#include + +#define F std::function + +struct wl_client; +struct wl_resource; + +)#"; + + for (auto& iface : XMLDATA.ifaces) { + const auto IFACE_NAME_CAMEL = camelize(iface.name); + const auto IFACE_CLASS_NAME_CAMEL = camelize("C_" + iface.name); + + // begin the class + HEADER += std::format(R"#( +class {} {{ + public: + {}(wl_client* client, uint32_t version, uint32_t id); + ~{}(); + + // set a listener for when this resource is _being_ destroyed + void setOnDestroy(F handler) {{ + onDestroy = handler; + }} + + // set the data for this resource + void setData(void* data) {{ + pData = data; + }} + + // get the data for this resource + void* data() {{ + return pData; + }} + + // get the raw wl_resource ptr + wl_resource* resource() {{ + return pResource; + }} + +)#", + IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL); + + // add all setters for requests + HEADER += "\n // --------------- Requests --------------- //\n\n"; + + for (auto& rq : iface.requests) { + + std::string args = ", "; + for (auto& arg : rq.args) { + args += arg.CType + ", "; + } + + args.pop_back(); + args.pop_back(); + + HEADER += std::format(" void {}(F handler);\n", camelize("set_" + rq.name), args); + } + + // start events + + HEADER += "\n // --------------- Events --------------- //\n\n"; + + for (auto& ev : iface.events) { + std::string args = ""; + for (auto& arg : ev.args) { + args += arg.CType + ", "; + } + + if (!args.empty()) { + args.pop_back(); + args.pop_back(); + } + + HEADER += std::format(" void {}({});\n", camelize("send_" + ev.name), args); + } + + // end events + + // start private section + HEADER += "\n private:\n"; + + // start requests storage + HEADER += " struct {\n"; + + for (auto& rq : iface.requests) { + + std::string args = ", "; + for (auto& arg : rq.args) { + args += arg.CType + ", "; + } + + args.pop_back(); + args.pop_back(); + + HEADER += std::format(" F {};\n", args, camelize(rq.name)); + } + + // end requests storage + HEADER += " } requests;\n"; + + // constant resource stuff + HEADER += std::format(R"#( + void onDestroyCalled(); + + F onDestroy; + + wl_resource* pResource = nullptr; + + wl_listener resourceDestroyListener; + + void* pData = nullptr;)#", + IFACE_CLASS_NAME_CAMEL); + + HEADER += "\n};\n\n"; + } + + HEADER += "\n\n#undef F\n"; +} + +void parseSource() { + SOURCE += std::format(R"#(#define private public +#include "{}.hpp" +#undef private +)#", + PROTO_DATA.name); + + // reference interfaces + + // dummy + SOURCE += R"#( +static const wl_interface* dummyTypes[] = { nullptr }; +)#"; + + SOURCE += R"#( +// Reference all other interfaces. +// The reason why this is in snake is to +// be able to cooperate with existing +// wayland_scanner interfaces (they are interop) +)#"; + + std::vector declaredIfaces; + + for (auto& iface : XMLDATA.ifaces) { + const auto IFACE_WL_NAME = iface.name + "_interface"; + const auto IFACE_WL_NAME_CAMEL = camelize(iface.name + "_interface"); + + if (std::find(declaredIfaces.begin(), declaredIfaces.end(), IFACE_WL_NAME) == declaredIfaces.end()) { + SOURCE += std::format("extern const wl_interface {};\n#define {} {}\n", IFACE_WL_NAME, IFACE_WL_NAME_CAMEL, IFACE_WL_NAME); + + declaredIfaces.push_back(IFACE_WL_NAME); + } + + // do all referenced too + for (auto& rq : iface.requests) { + for (auto& arg : rq.args) { + if (arg.interface.empty()) + continue; + + const auto IFACE_WL_NAME2 = arg.interface + "_interface"; + const auto IFACE_WL_NAME_CAMEL2 = camelize(arg.interface + "_interface"); + + if (std::find(declaredIfaces.begin(), declaredIfaces.end(), IFACE_WL_NAME2) == declaredIfaces.end()) { + SOURCE += std::format("extern const wl_interface {};\n#define {} {}\n", IFACE_WL_NAME2, IFACE_WL_NAME_CAMEL2, IFACE_WL_NAME2); + declaredIfaces.push_back(IFACE_WL_NAME2); + } + } + } + + for (auto& ev : iface.events) { + for (auto& arg : ev.args) { + if (arg.interface.empty()) + continue; + + const auto IFACE_WL_NAME2 = arg.interface + "_interface"; + const auto IFACE_WL_NAME_CAMEL2 = camelize(arg.interface + "_interface"); + + if (std::find(declaredIfaces.begin(), declaredIfaces.end(), IFACE_WL_NAME2) == declaredIfaces.end()) { + SOURCE += std::format("extern const wl_interface {};\n#define {} {}\n", IFACE_WL_NAME2, IFACE_WL_NAME_CAMEL2, IFACE_WL_NAME2); + declaredIfaces.push_back(IFACE_WL_NAME2); + } + } + } + } + + // declare ifaces + + for (auto& iface : XMLDATA.ifaces) { + + const auto IFACE_NAME = iface.name; + const auto IFACE_NAME_CAMEL = camelize(iface.name); + const auto IFACE_CLASS_NAME_CAMEL = camelize("C_" + iface.name); + + // create handlers + for (auto& rq : iface.requests) { + const auto REQUEST_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name); + + std::string argsC = ", "; + for (auto& arg : rq.args) { + argsC += arg.CType + " " + arg.name + ", "; + } + + argsC.pop_back(); + argsC.pop_back(); + + std::string argsN = ", "; + for (auto& arg : rq.args) { + argsN += arg.name + ", "; + } + + argsN.pop_back(); + argsN.pop_back(); + + SOURCE += std::format(R"#( +static void {}(wl_client* client, wl_resource* resource{}) {{ + const auto PO = ({}*)wl_resource_get_user_data(resource); + if (PO->requests.{}) + PO->requests.{}(client, resource{}); +}} +)#", + REQUEST_NAME, argsC, IFACE_CLASS_NAME_CAMEL, camelize(rq.name), camelize(rq.name), argsN); + } + + // destroy handler + SOURCE += std::format(R"#( +static void _{}__DestroyListener(wl_listener* l, void* d) {{ + {}* pResource = wl_container_of(l, pResource, resourceDestroyListener); + pResource->onDestroyCalled(); +}} +)#", + IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL); + + // create vtable + + const auto IFACE_VTABLE_NAME = "_" + IFACE_CLASS_NAME_CAMEL + "VTable"; + + SOURCE += std::format(R"#( +static const void* {}[] = {{ +)#", + IFACE_VTABLE_NAME); + + for (auto& rq : iface.requests) { + const auto REQUEST_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name); + SOURCE += std::format(" {},\n", REQUEST_NAME); + } + + SOURCE += "};\n"; + + // create events + + int evid = 0; + for (auto& ev : iface.events) { + const auto EVENT_NAME = camelize("send_" + ev.name); + + std::string argsC = ", "; + for (auto& arg : ev.args) { + argsC += arg.CType + " " + arg.name + ", "; + } + + argsC.pop_back(); + argsC.pop_back(); + + std::string argsN = ", "; + for (auto& arg : ev.args) { + argsN += arg.name + ", "; + } + + argsN.pop_back(); + argsN.pop_back(); + + SOURCE += std::format(R"#( +void {}::{}({}) {{ + wl_resource_post_event(pResource, {}{}); +}} +)#", + IFACE_CLASS_NAME_CAMEL, EVENT_NAME, argsC, evid, argsN); + + evid++; + } + + // wayland interfaces and stuff + + // type tables + for (auto& rq : iface.requests) { + if (rq.args.empty()) + continue; + + const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name + "_types"); + SOURCE += std::format("static const wl_interface* {}[] = {{\n", TYPE_TABLE_NAME); + + for (auto& arg : rq.args) { + if (arg.interface.empty()) { + SOURCE += " nullptr,\n"; + continue; + } + + SOURCE += std::format(" &{}_interface,\n", arg.interface); + } + + SOURCE += "};\n"; + } + + const auto MESSAGE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_message"); + + // message + SOURCE += std::format(R"#( +static const wl_message {}[] = {{ +)#", + MESSAGE_NAME); + for (auto& rq : iface.requests) { + // create type table + const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name + "_types"); + + SOURCE += std::format(" {{ \"{}\", \"{}\", {}}},\n", rq.name, argsToShort(rq.args), rq.args.empty() ? "dummyTypes + 0" : TYPE_TABLE_NAME + " + 0"); + } + + SOURCE += "};\n"; + + // protocol body + SOURCE += std::format(R"#( +{}::{}(wl_client* client, uint32_t version, uint32_t id) {{ + pResource = wl_resource_create(client, &{}, version, id); + + if (!pResource) + return; + + wl_resource_set_user_data(pResource, this); + wl_list_init(&resourceDestroyListener.link); + resourceDestroyListener.notify = _{}__DestroyListener; + wl_resource_add_destroy_listener(pResource, &resourceDestroyListener); + + wl_resource_set_implementation(pResource, {}, this, nullptr); +}} + +{}::~{}() {{ + wl_list_remove(&resourceDestroyListener.link); + wl_list_init(&resourceDestroyListener.link); + + // if we still own the wayland resource, + // it means we need to destroy it. + if (wl_resource_get_user_data(pResource) == this) + wl_resource_destroy(pResource); +}} + +void {}::onDestroyCalled() {{ + wl_resource_set_user_data(pResource, nullptr); + + if (onDestroy) + onDestroy(this); +}} +)#", + IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_NAME + "_interface", IFACE_CLASS_NAME_CAMEL, IFACE_VTABLE_NAME, IFACE_CLASS_NAME_CAMEL, + IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL); + } +} + +int main(int argc, char** argv, char** envp) { + std::string outpath = ""; + std::string protopath = ""; + + for (int i = 1; i < argc; ++i) { + std::string curarg = argv[i]; + if (i == 1) + protopath = curarg; + else if (i == 2) + outpath = curarg; + } + + if (outpath.empty() || protopath.empty()) { + std::cerr << "Not enough args\n"; + return 1; + } + + // build! + + pugi::xml_document doc; + if (!doc.load_file(protopath.c_str())) { + std::cerr << "Couldn't load proto\n"; + return 1; + } + + PROTO_DATA.nameOriginal = doc.child("protocol").attribute("name").as_string(); + PROTO_DATA.name = camelize(PROTO_DATA.nameOriginal); + + const auto COPYRIGHT = + std::format("// Generated with hyprwayland-scanner {}. Made with vaxry's keyboard and ❤️.\n// {}\n\n/*\n This protocol's authors' copyright notice is:\n\n{}\n*/\n\n", SCANNER_VERSION, + PROTO_DATA.nameOriginal, std::string{doc.child("protocol").child("copyright").child_value()}); + + parseXML(doc); + parseHeader(); + parseSource(); + + std::ofstream header(outpath + "/" + PROTO_DATA.name + ".hpp", std::ios::trunc); + header << COPYRIGHT << HEADER; + header.close(); + std::ofstream source(outpath + "/" + PROTO_DATA.name + ".cpp", std::ios::trunc); + source << COPYRIGHT << SOURCE; + source.close(); + + return 0; +} \ No newline at end of file