diff --git a/include/el/codable.hpp b/include/el/codable.hpp new file mode 100644 index 0000000..7c07996 --- /dev/null +++ b/include/el/codable.hpp @@ -0,0 +1,275 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:28 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Codable helper classes and macros for defining structures +and classes that can be encoded to and/or decoded from json. + +This functionality is based on Niels Lohmann's JSON for modern C++ library +and depends on it. It must be includable as follows: + +#include +*/ + +#pragma once + +#include "cxxversions.h" + +#include +#include +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "metaprog.hpp" +#include "codable_types.hpp" + + +namespace el +{ + /** + * @brief Class interface for + * decodable structures and classes. + * + * This also provides the converter function from_json() + * which allows integration with the nlohmann::json library's + * builtin conversion system. This generates an operator allowing + * the assignment of a json object to any decodable object. + */ + class decodable + { + protected: + virtual ~decodable() = default; + + public: + /** + * @brief function which decodes the decodable + * object from json-encoded data + * + * @param _output the json instance to decode. + * This can be a number, object, list or whatever json type + * is used to represent this codable. Invalid type will throw + * a decoder exception. + */ + virtual void _el_codable_decode(const nlohmann::json &_input) = 0; + + /** + * @brief function to convert this decodable from json using + * the functionality provided by the nlohmann::json library + * + * @param _j_output json instance to decode + * @param _t_input decodable to decode from json + */ + friend void from_json(const nlohmann::json &_j_input, decodable &_t_output) + { + _t_output._el_codable_decode(_j_input); + }; + }; + + /** + * @brief Class interface for + * encodable structures and classes. + * + * This also provides the converter function to_json() + * which allows integration with the nlohmann::json library's + * builtin conversion system. This generates an operator allowing + * the assignment of any decodable object to a json object. + */ + class encodable + { + protected: + virtual ~encodable() = default; + + public: + + /** + * @brief function which encodes the encodable's + * members to json. + * + * @param _output the json instance to save the json encoded object to. + * This might be converted to number, object, list or whatever json type + * is used to represent this encodable. + */ + virtual void _el_codable_encode(nlohmann::json &_output) const = 0; + + /** + * @brief function to convert this encodable to json using + * the functionality provided by the nlohmann::json library + * + * @param _j_output json instance to encode to + * @param _t_input encodable to encode + */ + friend void to_json(nlohmann::json &_j_output, const encodable &_t_input) + { + _t_input._el_codable_encode(_j_output); + } + }; + + /** + * @brief Class interface for + * codable structures and classes. + * + * This combines encodable and decodable + * for objects that need to be both en- and decoded. + */ + class codable : public encodable, public decodable + { + protected: + virtual ~codable() = default; + }; + + +#ifdef __EL_ENABLE_CXX20 +/** + * The following concepts define constraints that allow targeting specific + * kinds of codables such as a class that is either ONLY an encodable + * or ONLY a decodable or both. + */ + + /** + * @brief Constrains _T to be ONLY derived from encodable + * and NOT from decodable + */ + template + concept EncodableOnly = std::derived_from<_T, encodable> && !std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be at derived at least from encodable + * (but can additionally also derive from decodable) + */ + template + concept AtLeaseEncodable = std::derived_from<_T, encodable>; + + /** + * @brief Constrains _T to be ONLY derived from decodable + * and NOT from encodable + */ + template + concept DecodableOnly = std::derived_from<_T, decodable> && !std::derived_from<_T, encodable>; + + /** + * @brief Constrains _T to be at derived at least from decodable + * (but can additionally also derive from encodable) + */ + template + concept AtLeastDecodable = std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be derived BOTH from encodable + * and from decodable, making it a full codable + */ + template + concept FullCodable = std::derived_from<_T, encodable> && std::derived_from<_T, decodable>; + + /** + * @brief Constrains _T to be derived from encodable + * and/or decodable. This constrains a type to be any + * sort of codable. + */ + template + concept AnyCodable = std::derived_from<_T, encodable> || std::derived_from<_T, decodable>; + +#endif // __EL_ENABLE_CXX20 + + +/** + * Automatic constructor generation + * + */ + +// (private) generates a constructor argument for a structure member (of a codable) +#define __EL_CODABLE_CONSTRUCTOR_ARG(member) decltype(member) & _ ## member, +// (private) generates a constructor initializer list entry for a member from the above defined argument +#define __EL_CODABLE_CONSTRUCTOR_INIT(member) member(_ ## member), + +// (public) generates a constructor for a given structure which initializes the given members +#define EL_CODABLE_GENERATE_CONSTRUCTORS(TypeName, ...) \ + private: \ + inline int __el_codable_ctorgen_dummy; \ + public: \ + TypeName() = default; \ + TypeName(EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_CONSTRUCTOR_ARG, __VA_ARGS__) char __dummy = 0) \ + : EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_CONSTRUCTOR_INIT, __VA_ARGS__) \ + __el_codable_ctorgen_dummy(__dummy) /* dummy is because comma left by macro */ \ + {} + +/** + * Encoding/Decoding code generation + * + */ + +// (private) generates code which uses a member's encoder function to add it to a json object +#define __EL_CODABLE_ENCODE_KEY(member) _el_codable_encode_ ## member (#member, _output); +// (private) generates code which uses a member's decoder function to retrieve it's value from a json object +#define __EL_CODABLE_DECODE_KEY(member) _el_codable_decode_ ## member (#member, _input); + +// (public) generates the declaration of the encoder method for a specific member +#define EL_ENCODER(member) void _el_codable_encode_ ## member (const char *member_name, nlohmann::json &encoded_data) const +// (public) generates the declaration of the decoder method for a specific member +#define EL_DECODER(member) void _el_codable_decode_ ## member (const char *member_name, const nlohmann::json &encoded_data) + +// (private) generates the default encoder method for a member +#define __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ + /* these dummy templates make this function less specialized than one without, \ + so the user can manually define their encoder which will take precedence over \ + this one */ \ + template \ + EL_ENCODER(member) \ + { \ + /*encoded_data[member_name] = member;*/ \ + ::el::codable_types::encode_to_object(encoded_data, member_name, member); \ + } + +// (private) generates the default decoder method for a member +#define __EL_CODABLE_DEFINE_DEFAULT_DECODER(member) \ + /* these dummy templates make this function less specialized than one without, \ + so the user can manually define their encoder which will take precedence over \ + this one */ \ + template \ + EL_DECODER(member) \ + { \ + /* explicit convert using .get to avoid unwanted casting paths */ \ + /*member = encoded_data.at(member_name).get();*/ \ + ::el::codable_types::decode_from_object(encoded_data, member_name, member); \ + } + +// (private) generates the default encoder/decoder methods for a class member +#define __EL_CODABLE_DEFINE_DEFAULT_CONVERTERS(member) \ + __EL_CODABLE_DEFINE_DEFAULT_ENCODER(member) \ + __EL_CODABLE_DEFINE_DEFAULT_DECODER(member) + +// (public) generates the methods necessary to make a structure encodable. +// Only the provided members will be made encodable, the others will not be touched. +#define EL_DEFINE_ENCODABLE(Name, ...) \ + \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_ENCODER, __VA_ARGS__) \ + \ + virtual void _el_codable_encode(nlohmann::json &_output) const override \ + { \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_ENCODE_KEY, __VA_ARGS__) \ + } + +// (public) generates the methods necessary to make a structure decodable. +// Only the provided members will be made decodable, the others will not be touched. +#define EL_DEFINE_DECODABLE(Name, ...) \ + \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DEFINE_DEFAULT_DECODER, __VA_ARGS__) \ + \ + virtual void _el_codable_decode(const nlohmann::json &_input) override \ + { \ + EL_METAPROG_DO_FOR_EACH(__EL_CODABLE_DECODE_KEY, __VA_ARGS__) \ + } + +// (public) generates the methods necessary to make a structure codable (encodable and decodable). +// Only the provided members will be made encodable/decodable, the others will not be touched. +#define EL_DEFINE_CODABLE(Name, ...) \ + EL_DEFINE_ENCODABLE(Name, __VA_ARGS__) \ + EL_DEFINE_DECODABLE(Name, __VA_ARGS__) \ + +} // namespace el \ No newline at end of file diff --git a/include/el/codable_types.hpp b/include/el/codable_types.hpp new file mode 100644 index 0000000..0cbc481 --- /dev/null +++ b/include/el/codable_types.hpp @@ -0,0 +1,185 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +11.01.24, 09:28 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Additional codable support for standard and extended types that are commonly +used. + +This functionality is based on Niels Lohmann's JSON for modern C++ library +and depends on it. It must be includable as follows: + +#include +*/ + +#pragma once + +#include +#include + +namespace el::codable_types +{ + /** + * Codables use the "decode_from_object" and "encode_to_object" methods that can be overloaded + * to support various type conversions. + * + * Unlike the functionality provided by nlohmann's JSON library ("to_json" and "from_json"), + * these functions get the entire context of the containing object. This allows them to be + * more flexible, such as coding optionals. + * + * The namespace is called "el::codable_types" and not just "el" because this namespace may be extended + * by the user of the library to support conversion of custom types. + */ + + + /** + * @brief object key decoder using nlohmann's generic decoding + * mechanism. This enables the decoding of any C++ object + * supported by nlohmann JSON and any types made decodable using + * this mechanism. + * + * @tparam _T datatype to decode from the object + * @param _object json object to decode from + * @param _key the key in the above object to decode + * @param _out_data destination of decoded data + */ + template + void decode_from_object( + const nlohmann::json &_object, + const std::string &_key, + _T &_out_data + ) + { + // this uses the from_json() functions to decode + _out_data = _object.at(_key).get<_T>(); + } + + /** + * @brief object key encoder using nlohmann's generic encoding + * mechanism. This enables the encoding of any C++ object + * supported by nlohmann JSON and any types made encodable using + * this mechanism. + * + * @tparam _T datatype to encode to the object + * @param _object json object to store encoded output in + * @param _key the key in the above object to store the encoded output + * @param _in_data data to encode + */ + template + void encode_to_object( + nlohmann::json &_object, + const std::string &_key, + const _T &_in_data + ) + { + _object[_key] = _in_data; + } + + /** + * @brief object key decoder overload for optional types. + * This enables the decoding std::optionals from json, for any + * contained type that can be decoded. + * If key is not found in object, optional will be reset (nullptr). + * If key is found, optional is assigned the decoded value. + * + * @tparam _T datatype contained in optional + * @param _object json object to decode from + * @param _key the key in the above object to decode + * @param _out_data destination optional of decoded data + */ + template + void decode_from_object( + const nlohmann::json &_object, + const std::string &_key, + std::optional<_OT> &_out_data + ) + { + if (!_object.contains(_key)) + _out_data.reset(); + else + { + // default construct the optional to ensure it has a value in case it didn't before + _out_data.emplace(); + decode_from_object<_OT>(_object, _key, *_out_data); // pass content reference is UD without value + //_out_data = _object.at(_key).get<_OT>(); + } + } + + /** + * @brief object key encoder overload for optional types. + * This enables the encoding std::optionals to json, for any + * contained type that can be encoded. + * If optional is empty (nullptr), the key will not be added + * to output object (if it's already ther, it's untouched). + * If the optional contains a value, it is encoded and added to + * the json object. + * + * @tparam _T datatype contained in optional + * @param _object json object to store encoded output in + * @param _key the key in the above object to store the encoded output + * @param _in_data optional data to encode + */ + template + void encode_to_object( + nlohmann::json &_object, + const std::string &_key, + const std::optional<_OT> &_in_data + ) + { + if (_in_data.has_value()) + encode_to_object(_object, _key, *_in_data); + //_object[_key] = *_in_data; + } + +} // namespace codable_types + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/** + * @brief (de)serializer for std::optional + * See https://json.nlohmann.me/features/arbitrary_types/#how-do-i-convert-third-party-types + * for explanation why namespace is needed. + * This has been replaced by the custom encoder/decoder method supporting more functionality. + * @tparam _T contained type + */ +//template +//struct adl_serializer> +//{ +// /** +// * @brief nlohmann JSON decoder for optionals. Expects JSON null for +// * empty optional case and the value otherwise. +// * +// * @param _j_input input json data +// * @param _t_output decoded optional +// */ +// static void from_json(const nlohmann::json &_j_input, std::optional<_T> &_t_output) +// { +// if (_j_input.is_null()) +// _t_output.reset(); +// else +// _t_output = _j_input.get<_T>(); +// }; +// +// /** +// * @brief nlohmann JSON encoder for optionals. Emits JSON null for +// * empty optional case and the encoded value otherwise. +// * +// * @tparam _T contained type +// * @param _j_output output json data +// * @param _t_input optional to encode +// */ +// static void to_json(nlohmann::json &_j_output, const std::optional<_T> &_t_input) +// { +// if (!_t_input.has_value()) +// _j_output = nullptr; +// else +// _j_output = *_t_input; +// } +// +//}; +NLOHMANN_JSON_NAMESPACE_END \ No newline at end of file diff --git a/include/el/cxxversions.h b/include/el/cxxversions.h index ebc4e94..5aba88c 100644 --- a/include/el/cxxversions.h +++ b/include/el/cxxversions.h @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 26.11.22, 18:25 All rights reserved. @@ -8,18 +8,31 @@ All rights reserved. This source code is licensed under the Apache-2.0 license found in the LICENSE file in the root directory of this source tree. -Preprocessor definitions for detecting C++ verisons -and enabeling supported features. +Preprocessor definitions for detecting C++ versions +and enabling supported features. -Currentyl this might not work for all compilers and it might not work at all. +The library checks for the ENABLE macro definitions. + +Currently this might not work for all compilers and it might not work at all. In order to bypass all version checking, just #define __EL_ENABLE_CXX11 #define __EL_ENABLE_CXX17 +#define __EL_ENABLE_CXX20 to enable library features for versions not detected using the __cplusplus definition. */ #pragma once +#ifdef __linux__ +#define __EL_PLATFORM_LINUX +#endif +#ifdef __APPLE__ +#define __EL_PLATFORM_APPLE +#endif +#ifdef _WIN32 +#define __EL_PLATFORM_WINDOWS +#endif + // check for C++ 11 compatablility #if __cplusplus > 199711L @@ -42,4 +55,27 @@ to enable library features for versions not detected using the __cplusplus defin #define __EL_CXX17 #define __EL_ENABLE_CXX17 +#endif + +// check for C++ 20 compatibility +#if __cplusplus >= 202002L + +#define __EL_CXX20 +#define __EL_ENABLE_CXX20 + +#endif + + +/** + * == Library feature selection == + * Some features might not be desired on certain platforms, + * such as the use of RTTI in embedded systems. Flags defined + * here are used to switch of these features. This may + * have the effect of certain classes acting differently. + */ + +#ifndef EL_DISABLE_EXCEPTIONS + +#define __EL_ENABLE_EXCEPTIONS + #endif \ No newline at end of file diff --git a/include/el/exceptions.hpp b/include/el/exceptions.hpp new file mode 100644 index 0000000..37e6b36 --- /dev/null +++ b/include/el/exceptions.hpp @@ -0,0 +1,56 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 23:23 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +el-std base exceptions +*/ + +#pragma once + +#include +#include + +#include "strutil.hpp" + + +namespace el +{ + /** + * @brief el-std base exception allowing custom messages + */ + class exception : public std::exception + { + private: + std::string m_message; + + public: + + exception(const char *_msg) + : m_message(_msg) + {} + + exception(const std::string &_msg) + : m_message(_msg) + {} + + template + exception(const std::string &_msg_fmt, _Args... _args) + : m_message(strutil::format(_msg_fmt, _args...)) + {} + + virtual ~exception() noexcept = default; + + virtual const char *what() const noexcept override + { + return m_message.c_str(); + } + }; + +} // namespace el + diff --git a/include/el/flags.hpp b/include/el/flags.hpp new file mode 100644 index 0000000..a17a07f --- /dev/null +++ b/include/el/flags.hpp @@ -0,0 +1,79 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 15:26 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +flag types with specific restrictions +*/ + +#pragma once + +#include "cxxversions.h" + + +namespace el +{ + using flag = bool; + + /** + * @brief set-only flag. + * + * This single use flag behaves like a normal boolean flag, + * except it can only be set but never cleared again. + * When default initialized, the flag is initialized to false. + * When copy/move initialized, the flag is initialized as expected. + */ + class soflag + { + protected: + flag internal_flag = false; + + public: + // allow default init + soflag() = default; + soflag(const soflag &) = default; + soflag(soflag &&) = default; + + // cannot copy assign as that would allow clearing + soflag &operator=(const soflag &) = delete; + + /** + * @brief sets the set only flag to the value of + * the provided flag if it is set. If the provided + * flag _x is cleared, nothing will happen. + * + * @param _x whether to set + * @return flag& value of the soflag after the operation (not _x) + */ + inline flag &operator=(flag _x) noexcept + { + if (_x) + internal_flag = _x; + + return internal_flag; + } + + /** + * @brief sets the flag. + * If already set, nothing happens. + */ + inline void set() noexcept + { + internal_flag = true; + } + + /** + * @return flag the current value of the flag (set/cleared) + */ + inline operator flag() const noexcept + { + return internal_flag; + } + }; + +} // namespace el diff --git a/include/el/hashable_path.hpp b/include/el/hashable_path.hpp index 2e6ed58..05b1c81 100644 --- a/include/el/hashable_path.hpp +++ b/include/el/hashable_path.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 20.11.22, 21:43 All rights reserved. diff --git a/include/el/jsonutils.hpp b/include/el/jsonutils.hpp index fb766c9..0a585a0 100644 --- a/include/el/jsonutils.hpp +++ b/include/el/jsonutils.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 19.11.22, 23:57 All rights reserved. @@ -11,12 +11,12 @@ LICENSE file in the root directory of this source tree. utility functions for the nlohmann json library. This depends on the nlohmann::json library -which must be includable like this: "#include " +which must be includeable like this: "#include " */ #pragma once -#include +#include #include #include "cxxversions.h" diff --git a/include/el/logging.hpp b/include/el/logging.hpp new file mode 100644 index 0000000..38e0259 --- /dev/null +++ b/include/el/logging.hpp @@ -0,0 +1,280 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 22:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Simple logging framework +*/ + +#pragma once + +#include "cxxversions.h" + +#include +#include +#include +#include + +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "strutil.hpp" +#include "rtti_utils.hpp" + + +// color escape sequences +#define _EL_LOG_ANSI_COLOR_RED "\e[31m" +#define _EL_LOG_ANSI_COLOR_GREEN "\e[32m" +#define _EL_LOG_ANSI_COLOR_YELLOW "\e[33m" +#define _EL_LOG_ANSI_COLOR_BLUE "\e[34m" +#define _EL_LOG_ANSI_COLOR_MAGENTA "\e[35m" +#define _EL_LOG_ANSI_COLOR_CYAN "\e[36m" +#define _EL_LOG_ANSI_COLOR_RESET "\e[0m" + +#define _EL_LOG_PREFIX_BUFFER_SIZE 100 + +#define _EL_LOG_FILEW 15 +#define _EL_LOG_LINEW 4 + +#if defined(__EL_PLATFORM_LINUX) +# include +#elif defined(__EL_PLATFORM_WINDOWS) +# include +# define PATH_MAX MAX_PATH +#elif defined(__EL_PLATFORM_APPLE) +# include +#endif + +/** + * Source location information. + * Since C++20 there is builtin portable support for getting + * the source location by using the header. + * It provides the std::source_location type as well as the function + * std::source_location::current() to get the current source location. + * The returned object can be used to retrieve file name, line number, column + * and function name. + * + * Before C++20, macros and other builtin symbols had to be used. This is not quite + * portable however. + * + * We use the C++20 features where possible and fallback to macros otherwise. + */ + +#ifdef __EL_ENABLE_CXX20 + +#define _EL_LOG_FILE std::source_location::current().file_name() +#define _EL_LOG_LINE std::source_location::current().line() +#define _EL_LOG_FUNCTION std::source_location::current().function_name() + +#else + +// these might not be possible for every compiler +#define _EL_LOG_FILE __FILE__ +#define _EL_LOG_LINE __LINE__ +#define _EL_LOG_FUNCTION __PRETTY_FUNCTION__ + +#endif + + +#define EL_DEFINE_LOGGER() el::logging::logger el::logging::logger_inst +#define EL_LOGC(fmt, ...) el::logging::logger_inst.critical(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGE(fmt, ...) el::logging::logger_inst.error(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGW(fmt, ...) el::logging::logger_inst.warning(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGI(fmt, ...) el::logging::logger_inst.info(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGD(fmt, ...) el::logging::logger_inst.debug(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) +#define EL_LOGT(fmt, ...) el::logging::logger_inst.testing(_EL_LOG_FILE, _EL_LOG_LINE, fmt, ## __VA_ARGS__) + +#define EL_LOG_EXCEPTION_MSG(msg, ex) EL_LOGE(msg ": %s", el::logging::format_exception(ex).c_str()) +#define EL_LOG_EXCEPTION(ex) EL_LOG_EXCEPTION_MSG("Exception occurred", ex) + +#define EL_LOG_FUNCTION_CALL() EL_LOGD("Function call: \e[1;3m%s\e", _EL_LOG_FUNCTION) // the color reset happens at end of line anyway, bold is reset there too + + +namespace el::logging +{ + class logger + { + private: + + void generate_prefix(char *_output_buffer, const char *_file, int _line, const char *_level) + { + // A file name/path can be (_EL_LOG_FILEW - 1) characters long and will be + // printed in a _EL_LOG_FILEW characters wide area, so ther will always be a space + // to the left side. If the path is _EL_LOG_FILEW or more characters long, then it will + // be truncated on the left side and the space will be replaced by an overflow indicator ('<') + + // substring filename for alignment if necessary + char file_name[_EL_LOG_FILEW + 1] = {'\0'}; + size_t file_len = strnlen(_file, PATH_MAX); + if (file_len != PATH_MAX && file_len > (_EL_LOG_FILEW - 1)) // bigger than width -1 because we always want to have one space to the left unless for the overflow indicator + { + // if filename is to wide, truncate it to the _EL_LOG_FILEW rightmost characters + // and add '<' to it's start + strncpy(file_name, _file + (file_len - _EL_LOG_FILEW), _EL_LOG_FILEW); + file_name[_EL_LOG_FILEW] = '\0'; + file_name[0] = '<'; + } + else + { + // otherwise just copy it + strncpy(file_name, _file, _EL_LOG_FILEW); + file_name[_EL_LOG_FILEW] = '\0'; + } + + snprintf( + _output_buffer, + _EL_LOG_PREFIX_BUFFER_SIZE - 1, + "[%*.*s@%-*d ] %s: ", // at least one space on the side always + _EL_LOG_FILEW, + _EL_LOG_FILEW, + file_name, + _EL_LOG_LINEW, + _line, + _level + ); + } + + public: + logger() = default; + + // Critical + + void critical(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "C"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void critical(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + critical(_file, _line, strutil::format(_fmt, _args...)); + } + + // Error + + void error(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "E"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RED << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void error(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + error(_file, _line, strutil::format(_fmt, _args...)); + } + + + // Warning + + void warning(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "W"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_YELLOW << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void warning(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + warning(_file, _line, strutil::format(_fmt, _args...)); + } + + // Info + + void info(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "I"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_RESET << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void info(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + info(_file, _line, strutil::format(_fmt, _args...)); + } + + // Debug + + void debug(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "D"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_GREEN << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void debug(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + debug(_file, _line, strutil::format(_fmt, _args...)); + } + + // Testing (intended for unit testing info output) + + void testing(const char *_file, int _line, const std::string &_message) + { + // generate prefix + char prefix_buffer[_EL_LOG_PREFIX_BUFFER_SIZE]; + generate_prefix(prefix_buffer, _file, _line, "T"); + + // print in color + std::cout << _EL_LOG_ANSI_COLOR_MAGENTA << prefix_buffer << _message << _EL_LOG_ANSI_COLOR_RESET << std::endl; + } + + template + void testing(const char *_file, int _line, const std::string &_fmt, _Args... _args) + { + // format the message + testing(_file, _line, strutil::format(_fmt, _args...)); + } + + }; + + // declaration of global logger instance which has to be defined by the user + extern logger logger_inst; + + /** + * @brief turns the passed exception into a string + * in the format ": " + * + * Note the type name is demangled in GCC, the raw name (may be demangled + * depending on impl) is used for other compilers + * @param _e the exception to print + * @return std::string the printed exception + */ + inline std::string format_exception(const std::exception &_e) + { + return rtti::demangle_if_possible(typeid(_e).name()) + "\n what(): " + _e.what(); + } + +} // namespace el::log diff --git a/include/el/metaprog.hpp b/include/el/metaprog.hpp new file mode 100644 index 0000000..1d7f089 --- /dev/null +++ b/include/el/metaprog.hpp @@ -0,0 +1,150 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:34 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +A collection of macros allowing basic macro-based metaprogramming. +*/ + +#pragma once + + +// based on macros from nlohmann's JSON for modern C++ library + +#define EL_METAPROG_EXPAND( x ) x +#define EL_METAPROG_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define EL_METAPROG_PASTE(...) EL_METAPROG_EXPAND(EL_METAPROG_GET_MACRO(__VA_ARGS__, \ + EL_METAPROG_PASTE64, \ + EL_METAPROG_PASTE63, \ + EL_METAPROG_PASTE62, \ + EL_METAPROG_PASTE61, \ + EL_METAPROG_PASTE60, \ + EL_METAPROG_PASTE59, \ + EL_METAPROG_PASTE58, \ + EL_METAPROG_PASTE57, \ + EL_METAPROG_PASTE56, \ + EL_METAPROG_PASTE55, \ + EL_METAPROG_PASTE54, \ + EL_METAPROG_PASTE53, \ + EL_METAPROG_PASTE52, \ + EL_METAPROG_PASTE51, \ + EL_METAPROG_PASTE50, \ + EL_METAPROG_PASTE49, \ + EL_METAPROG_PASTE48, \ + EL_METAPROG_PASTE47, \ + EL_METAPROG_PASTE46, \ + EL_METAPROG_PASTE45, \ + EL_METAPROG_PASTE44, \ + EL_METAPROG_PASTE43, \ + EL_METAPROG_PASTE42, \ + EL_METAPROG_PASTE41, \ + EL_METAPROG_PASTE40, \ + EL_METAPROG_PASTE39, \ + EL_METAPROG_PASTE38, \ + EL_METAPROG_PASTE37, \ + EL_METAPROG_PASTE36, \ + EL_METAPROG_PASTE35, \ + EL_METAPROG_PASTE34, \ + EL_METAPROG_PASTE33, \ + EL_METAPROG_PASTE32, \ + EL_METAPROG_PASTE31, \ + EL_METAPROG_PASTE30, \ + EL_METAPROG_PASTE29, \ + EL_METAPROG_PASTE28, \ + EL_METAPROG_PASTE27, \ + EL_METAPROG_PASTE26, \ + EL_METAPROG_PASTE25, \ + EL_METAPROG_PASTE24, \ + EL_METAPROG_PASTE23, \ + EL_METAPROG_PASTE22, \ + EL_METAPROG_PASTE21, \ + EL_METAPROG_PASTE20, \ + EL_METAPROG_PASTE19, \ + EL_METAPROG_PASTE18, \ + EL_METAPROG_PASTE17, \ + EL_METAPROG_PASTE16, \ + EL_METAPROG_PASTE15, \ + EL_METAPROG_PASTE14, \ + EL_METAPROG_PASTE13, \ + EL_METAPROG_PASTE12, \ + EL_METAPROG_PASTE11, \ + EL_METAPROG_PASTE10, \ + EL_METAPROG_PASTE9, \ + EL_METAPROG_PASTE8, \ + EL_METAPROG_PASTE7, \ + EL_METAPROG_PASTE6, \ + EL_METAPROG_PASTE5, \ + EL_METAPROG_PASTE4, \ + EL_METAPROG_PASTE3, \ + EL_METAPROG_PASTE2, \ + EL_METAPROG_PASTE1)(__VA_ARGS__)) +#define EL_METAPROG_PASTE2(func, v1) func(v1) +#define EL_METAPROG_PASTE3(func, v1, v2) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE2(func, v2) +#define EL_METAPROG_PASTE4(func, v1, v2, v3) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE3(func, v2, v3) +#define EL_METAPROG_PASTE5(func, v1, v2, v3, v4) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE4(func, v2, v3, v4) +#define EL_METAPROG_PASTE6(func, v1, v2, v3, v4, v5) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE5(func, v2, v3, v4, v5) +#define EL_METAPROG_PASTE7(func, v1, v2, v3, v4, v5, v6) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE6(func, v2, v3, v4, v5, v6) +#define EL_METAPROG_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define EL_METAPROG_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define EL_METAPROG_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define EL_METAPROG_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define EL_METAPROG_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define EL_METAPROG_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define EL_METAPROG_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define EL_METAPROG_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define EL_METAPROG_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define EL_METAPROG_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define EL_METAPROG_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define EL_METAPROG_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define EL_METAPROG_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define EL_METAPROG_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define EL_METAPROG_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define EL_METAPROG_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define EL_METAPROG_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define EL_METAPROG_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define EL_METAPROG_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define EL_METAPROG_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define EL_METAPROG_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define EL_METAPROG_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define EL_METAPROG_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define EL_METAPROG_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define EL_METAPROG_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define EL_METAPROG_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define EL_METAPROG_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define EL_METAPROG_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define EL_METAPROG_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define EL_METAPROG_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define EL_METAPROG_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define EL_METAPROG_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define EL_METAPROG_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define EL_METAPROG_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define EL_METAPROG_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define EL_METAPROG_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define EL_METAPROG_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define EL_METAPROG_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define EL_METAPROG_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define EL_METAPROG_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define EL_METAPROG_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define EL_METAPROG_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define EL_METAPROG_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define EL_METAPROG_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define EL_METAPROG_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define EL_METAPROG_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define EL_METAPROG_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define EL_METAPROG_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define EL_METAPROG_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define EL_METAPROG_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define EL_METAPROG_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define EL_METAPROG_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define EL_METAPROG_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define EL_METAPROG_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define EL_METAPROG_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define EL_METAPROG_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define EL_METAPROG_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) EL_METAPROG_PASTE2(func, v1) EL_METAPROG_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define EL_METAPROG_DO_FOR_EACH(func, ...) EL_METAPROG_EXPAND(EL_METAPROG_PASTE(func, __VA_ARGS__)) diff --git a/include/el/msglink/dependencies.md b/include/el/msglink/dependencies.md new file mode 100644 index 0000000..ec778e1 --- /dev/null +++ b/include/el/msglink/dependencies.md @@ -0,0 +1,11 @@ +# msglink dependencies + +The C++ implementation of msglink currently uses Niels Lohmann's JSON library as well as WebSocket++. +Both of those are bundled or provided as submodules. + +WebSocket++ needs ASIO for network communication. It needs to be installed, ideally in the standalone version without boost: + +```bash +# On debian or derivatives: +sudo apt install libasio-dev +``` \ No newline at end of file diff --git a/include/el/msglink/errors.hpp b/include/el/msglink/errors.hpp new file mode 100644 index 0000000..5133ccc --- /dev/null +++ b/include/el/msglink/errors.hpp @@ -0,0 +1,226 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 22:03 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink exceptions +*/ + +#pragma once + +#include +#include + +#include "../exceptions.hpp" +#include "internal/ws_close_code.hpp" + + +namespace el::msglink +{ + namespace wspp = websocketpp; + + /** + * @brief base class for all errors related to msglink + */ + class msglink_error : public el::exception + { + using el::exception::exception; + }; + + /** + * @brief exception indicating that initialization + * could not be performed for some reason + */ + class initialization_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief exception indicating that the server/client couldn't be + * started because of incorrect state (e.g. not initialized) + */ + class launch_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief tried to access invalid connection instance + * (wspp callback with unknown connection handle) + */ + class invalid_connection_error: public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief msglink error to represent wspp error. + * (which is basically just asio error) + * All wspp::exceptions will be caught and rethrown as + * socket_errors by msglink, so everything inherits from + * msglink_error + */ + class socket_error : public msglink_error + { + public: + + wspp::lib::error_code m_code; // is just asio::error_code which should be std::error_code on modern system + + socket_error(const wspp::exception &_e) + : msglink_error(_e.m_msg) + , m_code(_e.m_code) + {} + + wspp::lib::error_code code() const noexcept { + return m_code; + } + + }; + + /** + * @brief received malformed event which either couldn't be parsed + * as json or was otherwise structurally invalid, e.g. missing properties. + */ + class malformed_message_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief attempted to create or register a new transaction but a + * transaction with the same ID already exists. + */ + class duplicate_transaction_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief attempted to retrieve an active transaction with invalid ID + * or the active transaction does not match the required type. + */ + class invalid_transaction_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief received messages which do not conform to the expected + * conversation as defined by the protocol. + */ + class protocol_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief attempted to access some sort of object like + * an event subscription for subscribing/unsubscribing + * but the identifier (name, id number, ...) is invalid + * and the object cannot be accessed. + */ + class invalid_identifier_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief link is not compatible with the link of the other party. + * This may be thrown during authentication. + */ + class incompatible_link_error : public msglink_error + { + close_code_t m_code; + + public: + + incompatible_link_error(close_code_t _code ,const char *_msg) + : msglink_error(_msg) + , m_code(_code) + {} + + template + incompatible_link_error(close_code_t _code, const std::string &_msg_fmt, _Args... _args) + : msglink_error(_msg_fmt, _args...) + , m_code(_code) + {} + + close_code_t code() const noexcept + { + return m_code; + } + }; + + /** + * @brief received unknown (invalid) incoming msglink event. + * This means, the event is either not defined or defined as + * outgoing only. This is a protocol error because undefined + * events should be detected and caught during authentication. + * Transmitting messages for unknown events after auth success + * does not conform to the protocol and is likely the result + * of a library implementation mistake. + */ + class invalid_incoming_event_error : public protocol_error + { + using protocol_error::protocol_error; + }; + + /** + * @brief attempted to emit an unknown (invalid) outgoing msglink event. + * This means, the event is either not defined or defined as + * incoming only. + * This is only thrown as a result of local emit function calls. + */ + class invalid_outgoing_event_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief this is thrown in a remote function call when the remote + * party responds with an error. + * The what() string contains the info provided by the remote client + */ + class remote_function_error : public msglink_error + { + using msglink_error::msglink_error; + }; + + /** + * @brief exception used to indicate an unexpected + * error code occurred like e.g. in some asio-related operation. + * This uses std::error_code which is the same as + * asio::error_code and therefor also wspp::error_code in modern C++. + * Multiple error code enums might be used though. + */ + class unexpected_error : public msglink_error + { + public: + + std::error_code m_code; // should be just std::error_code on modern systems + + unexpected_error(const std::error_code &_ec) + : msglink_error(_ec.message()) + , m_code(_ec) + {} + + std::error_code code() const noexcept { + return m_code; + } + }; + + /** + * @brief tried to parse/serialize an invalid message type. + */ + class invalid_msg_type_error : public msglink_error + { + using msglink_error::msglink_error; + }; + +} // namespace el::msglink diff --git a/include/el/msglink/event.hpp b/include/el/msglink/event.hpp new file mode 100644 index 0000000..e6518b3 --- /dev/null +++ b/include/el/msglink/event.hpp @@ -0,0 +1,162 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +11.11.23, 23:00 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink event class used to define custom events +*/ + +#pragma once + +#include "../cxxversions.h" + +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "../codable.hpp" + + +namespace el::msglink +{ + + /** + * @brief base class for all incoming msglink event + * definition classes. To create an incoming event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_INCOMING_EVENT macro to generate the required boilerplate. + * Incoming events are el::decodable[s], + * meaning they must be decodable from json. If a decoder for a member cannot + * be generated automatically or needs to be altered, the EL_DECODER macro can be + * used like with codables to manually define the decoder. + */ + struct incoming_event : public el::decodable + { + virtual ~incoming_event() = default; + + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_incoming_event_dummy() const noexcept = 0; + + bool __isincoming; + }; + + /** + * @brief base class for all outgoing msglink event + * definition classes. To create an outgoing event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_OUTGOING_EVENT macro to generate the required boilerplate. + * Outgoing events are el::encodable[s], + * meaning they must be encodable to json. If a encoder for a member cannot + * be generated automatically or needs to be altered, the EL_ENCODER macro can be + * used like with codables to manually define the encoder. + */ + struct outgoing_event : public el::encodable + { + virtual ~outgoing_event() = default; + + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept = 0; + }; + + /** + * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink event + * definition classes. It is simply a composite class inheriting form outgoing_event + * and incoming_event to save you the hassle of having to do that manually. + * Derived classes must satisfy all the requirements of incoming and outgoing events. + * To create a bidirectional event define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT macro to generate the required boilerplate. + */ + struct bidirectional_event : public incoming_event, public outgoing_event + { + virtual ~bidirectional_event() = default; + }; + + +#ifdef __EL_ENABLE_CXX20 + /** + * The following concepts define constraints that allow targeting specific + * kinds of events such as an event class that is either ONLY an incoming + * event or ONLY an outgoing event or a bidirectional event (BOTH incoming + * and outgoing) + */ + + /** + * @brief Constrains _ET to be ONLY derived from incoming_event + * and NOT from outgoing_event + */ + template + concept IncomingOnlyEvent = std::derived_from<_ET, incoming_event> && !std::derived_from<_ET, outgoing_event>; + + /** + * @brief Constrains _ET to be at derived at least from incoming_event + * (but can additionally also derive from outgoing_event) + */ + template + concept AtLeastIncomingEvent = std::derived_from<_ET, incoming_event>; + + /** + * @brief Constrains _ET to be ONLY derived from outgoing_event + * and NOT from incoming_event + */ + template + concept OutgoingOnlyEvent = std::derived_from<_ET, outgoing_event> && !std::derived_from<_ET, incoming_event>; + + /** + * @brief Constrains _ET to be at derived at least from outgoing_event + * (but can additionally also derive from incoming_event) + */ + template + concept AtLeastOutgoingEvent = std::derived_from<_ET, outgoing_event>; + + /** + * @brief Constrains _ET to be derived BOTH from incoming_event + * and from outgoing_event, making it a bidirectional event + */ + template + concept BidirectionalEvent = std::derived_from<_ET, incoming_event> && std::derived_from<_ET, outgoing_event>; + + /** + * @brief Constrains _ET to be derived from incoming_event + * and or outgoing_event. This constrains a type to be any + * sort of event. + */ + template + concept AnyEvent = std::derived_from<_ET, incoming_event> || std::derived_from<_ET, outgoing_event>; + +#endif // __EL_ENABLE_CXX20 + + +// (public) generates the necessary boilerplate code for an incoming event class. +// The members listed in the arguments will be made decodable using el::decodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_INCOMING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_incoming_event_dummy() const noexcept override {} \ + EL_DEFINE_DECODABLE(TypeName, __VA_ARGS__) + + +// (public) generates the necessary boilerplate code for an outgoing event class. +// The members listed in the arguments will be made encodable using el::encodable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_OUTGOING_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept override {} \ + EL_DEFINE_ENCODABLE(TypeName, __VA_ARGS__) + + +// (public) generates the necessary boilerplate code for an event class. +// The members listed in the arguments will be made codable using el::codable +// and are part of the event's data. +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT(TypeName, ...) \ + static inline const char *_event_name = #TypeName; \ + virtual void _el_msglink_is_incoming_event_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_event_dummy() const noexcept override {} \ + EL_DEFINE_CODABLE(TypeName, __VA_ARGS__) + + +} // namespace el::msglink \ No newline at end of file diff --git a/include/el/msglink/function.hpp b/include/el/msglink/function.hpp new file mode 100644 index 0000000..705782c --- /dev/null +++ b/include/el/msglink/function.hpp @@ -0,0 +1,174 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +21.01.24, 20:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink function class used to define custom remote functions +*/ + +#pragma once + +#include "../cxxversions.h" + +#include +#ifdef __EL_ENABLE_CXX20 +#include +#endif + +#include "../codable.hpp" + + +namespace el::msglink +{ + + + /** + * @brief base class for all incoming msglink function + * definition classes. To create an incoming function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_INCOMING_FUNCTION macro to generate the required boilerplate. + * Incoming functions must additionally have one structure called "parameters_t" which must be an el::decodable + * and one structure called "results_t" which must be an el::encodable. They can be defined with + * default codable definition macros. + */ + struct incoming_function + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept = 0; + }; + + /** + * @brief base class for all outgoing msglink function + * definition classes. To create an outgoing function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_OUTGOING_FUNCTION macro to generate the required boilerplate. + * Outgoing functions must additionally have one structure called "parameters_t" which must be an el::encodable + * and one structure called "results_t" which must be an el::decodable. They can be defined with + * default codable definition macros. + */ + struct outgoing_function + { + // dummy method which must be overriden to ensure the correct + // generate macro is used. + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept = 0; + }; + + /** + * @brief shortcut base class for all bidirectional (incoming and outgoing) msglink function + * definition classes. It is simply a composite class inheriting form outgoing_function + * and incoming_function to save you the hassle of having to do that manually. + * Derived classes must satisfy all the requirements of incoming and outgoing functions. + * To create a bidirectional function define a class inheriting from this one. + * Then use the EL_MSGLINK_DEFINE_BIDIRECTIONAL_FUNCTION macro to generate the required boilerplate. + */ + struct bidirectional_function : public incoming_function, public outgoing_function + {}; + + +#ifdef __EL_ENABLE_CXX20 + /** + * The following concepts define constraints that allow targeting specific + * kinds of functions such as an function class that is either ONLY an incoming + * function or ONLY an outgoing function or a bidirectional function (BOTH incoming + * and outgoing) + */ + + /** + * @brief Constrains _ET to be ONLY derived from incoming_function + * and NOT from outgoing_function + */ + template + concept IncomingOnlyFunction = std::derived_from<_ET, incoming_function> && !std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be at derived at least from incoming_function + * (but can additionally also derive from outgoing_function) + */ + template + concept AtLeastIncomingFunction = std::derived_from<_ET, incoming_function>; + + /** + * @brief Constrains _ET to be ONLY derived from outgoing_function + * and NOT from incoming_function + */ + template + concept OutgoingOnlyFunction = std::derived_from<_ET, outgoing_function> && !std::derived_from<_ET, incoming_function>; + + /** + * @brief Constrains _ET to be at derived at least from outgoing_function + * (but can additionally also derive from incoming_function) + */ + template + concept AtLeastOutgoingFunction = std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be derived BOTH from incoming_function + * and from outgoing_function, making it a bidirectional function + */ + template + concept BidirectionalFunction = std::derived_from<_ET, incoming_function> && std::derived_from<_ET, outgoing_function>; + + /** + * @brief Constrains _ET to be derived from incoming_function + * and or outgoing_function. This constrains a type to be any + * sort of function. + */ + template + concept AnyFunction = std::derived_from<_ET, incoming_function> || std::derived_from<_ET, outgoing_function>; + +#endif // __EL_ENABLE_CXX20 + + +// (public) generates the necessary boilerplate code for an incoming function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_INCOMING_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters_t>::value, \ + "incoming function parameters type must be decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::encodable, results_t>::value, \ + "incoming function results type must be encodable" \ + ); + + +// (public) generates the necessary boilerplate code for an outgoing function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_OUTGOING_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::encodable, parameters_t>::value, \ + "incoming function parameters type must be encodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results_t>::value, \ + "incoming function results type must be decodable" \ + ); + + +// (public) generates the necessary boilerplate code for a function class. +// The first and only argument is the structure type itself which is used to get the name. +#define EL_MSGLINK_DEFINE_BIDIRECTIONAL_FUNCTION(TypeName, ...) \ + static inline const char *_function_name = #TypeName; \ + virtual void _el_msglink_is_incoming_proc_dummy() const noexcept override {} \ + virtual void _el_msglink_is_outgoing_proc_dummy() const noexcept override {} \ + static_assert( \ + std::is_base_of<::el::decodable, parameters_t>::value && \ + std::is_base_of<::el::encodable, parameters_t>::value, \ + "bidirectional function parameters type must be en- and decodable" \ + ); \ + static_assert( \ + std::is_base_of<::el::decodable, results_t>::value && \ + std::is_base_of<::el::encodable, results_t>::value, \ + "bidirectional function results type must be en- and decodable" \ + ); + + +} // namespace el::msglink \ No newline at end of file diff --git a/include/el/msglink/internal/context.hpp b/include/el/msglink/internal/context.hpp new file mode 100644 index 0000000..9824df8 --- /dev/null +++ b/include/el/msglink/internal/context.hpp @@ -0,0 +1,128 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +23.02.24, 09:38 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Context class, of which an instance is created by the communication class and that is shared with all +objects related to the msglink communication class tree instance, such as client handlers and links. +This class contains global context data required for communication +*/ + +#pragma once + +#include +#include + +namespace el::msglink +{ + + class tracking_mutex : + public std::mutex + { + private: + std::atomic m_holder = std::thread::id{}; + + public: + void lock() + { + std::mutex::lock(); + m_holder = std::this_thread::get_id(); + EL_LOGD("ct locked"); + } + + void unlock() + { + m_holder = std::thread::id(); + std::mutex::unlock(); + EL_LOGD("ct unlocked"); + } + + bool try_lock() + { + if (std::mutex::try_lock()) { + m_holder = std::thread::id(); + return true; + } + return false; + } + + /** + * @return true if the mutex is locked by the caller of this method. + */ + bool locked_by_caller() const + { + return m_holder == std::this_thread::get_id(); + } + + }; + + class ct_context + { + public: // types + using mutex_type_t = tracking_mutex; + using lock_type_t = std::unique_lock; + + private: + // mutex to guard the state of the entire msglink communication class tree and make + // it entirely thread safe. + // This has to be locked at the beginning of every public method call or other external entry + // into the class tree (such as asio callback). Lock using get_lock() method. + mutex_type_t master_guard; + + public: + // the main io service used for communication and callback scheduling + std::unique_ptr io_service; + + public: + + ct_context() + : io_service(new asio::io_service()) + { + } + + /** + * @brief acquires a lock on the master class tree guard + * and returns it. The lock is held until the object is destructed. + * + * @return lock_type_t (std::unique_lock) + */ + lock_type_t get_lock() + { + return lock_type_t(master_guard); + } + + /** + * @brief acquires a lock on the master class tree guard + * and returns it unless a lock is already held by the calling thread. + * If the lock is already held, an empty unique_lock is returned. + * Because of this, the returned object should not be accessed with the assumption of an owning lock. + * If the lock is held by it, it is held until the object is destroyed. + * In any case, this function guarantees the calling thread is holding the lock + * after return. + * + * This function is used in places when it cannot be guaranteed that a call is external, such as in destructors. + * It should only be used sparely. + * + * @return lock_type_t lock if the lock was not locked jet + */ + lock_type_t get_soft_lock() + { + // This operation is not atomic. It could happen, that between this owner check + // and the following attemted lock, another thread locks the mutex. In that case, the calling + // thread has to wait. + // This is not a problem however. The only thing this protects against is that the same thread + // doesn't try to re-acquire the lock. + if (master_guard.locked_by_caller()) + return lock_type_t(); + // actually get the lock + return lock_type_t(master_guard); + } + }; + +} // namespace el::msglink + diff --git a/include/el/msglink/internal/link_interface.hpp b/include/el/msglink/internal/link_interface.hpp new file mode 100644 index 0000000..a344856 --- /dev/null +++ b/include/el/msglink/internal/link_interface.hpp @@ -0,0 +1,58 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +29.11.23, 08:46 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Class used as a common interface for the underlying networking class +used by the link instance to communicate to the other party. + +Both server and client networking implementations inherit from this and provide +the common interface declared by this class. +*/ + +#pragma once + +#include +#include + + +namespace el::msglink +{ + class link_interface + { + public: + /** + * @brief closes the connection and possibly destroys the link + * (depending on the function of the communication backend, e.g. + * server vs. client) + * + * @param _code the close code specifying the error/reason causing the close. + * @param _reason human readable reason for close. + */ + virtual void close_connection(int _code, std::string _reason) noexcept = 0; + + /** + * @brief encodes and then sends json content through the + * communication channels + * + * @param _jcontent json document to send + */ + void send_message(const nlohmann::json &_jcontent) + { + send_message(_jcontent.dump()); + }; + + protected: + /** + * @brief sends a message via the communication channel. + * + * @param _content string content to send + */ + virtual void send_message(const std::string &_content) = 0; + }; +} // namespace el::msglink diff --git a/include/el/msglink/internal/messages.hpp b/include/el/msglink/internal/messages.hpp new file mode 100644 index 0000000..a6d81fb --- /dev/null +++ b/include/el/msglink/internal/messages.hpp @@ -0,0 +1,177 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 20:30 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Codables for internal communication messages +*/ + +#pragma once + +#include +#include + +#include "../../codable.hpp" +#include "types.hpp" +#include "msgtype.hpp" + +namespace el::msglink +{ + /** + * @brief base type for all messages + */ + struct base_msg_t + { + std::string type; + tid_t tid; + }; + + struct msg_pong_t + // not base message because no tid required + : public encodable + { + std::string type = __EL_MSGLINK_MSG_NAME_PONG; + + EL_DEFINE_ENCODABLE( + msg_evt_sub_t, + type + ) + }; + + struct msg_auth_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_AUTH; + proto_version_t proto_version; + link_version_t link_version; + std::optional no_ping; + std::set events; + std::set data_sources; + std::set functions; + + EL_DEFINE_CODABLE( + msg_auth_t, + type, + tid, + proto_version, + link_version, + no_ping, + events, + data_sources, + functions + ) + }; + + struct msg_auth_ack_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_AUTH_ACK; + EL_DEFINE_CODABLE( + msg_auth_ack_t, + type, + tid + ) + }; + + struct msg_evt_sub_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_SUB; + std::string name; + + EL_DEFINE_CODABLE( + msg_evt_sub_t, + type, + tid, + name + ) + }; + + struct msg_evt_unsub_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_UNSUB; + std::string name; + + EL_DEFINE_CODABLE( + msg_evt_sub_t, + type, + tid, + name + ) + }; + + struct msg_evt_emit_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_EVT_EMIT; + std::string name; + nlohmann::json data; + + EL_DEFINE_CODABLE( + msg_evt_emit_t, + type, + tid, + name, + data + ) + }; + + struct msg_func_call_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_CALL; + std::string name; + nlohmann::json params; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + name, + params + ) + }; + + struct msg_func_err_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_ERR; + std::string info; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + info + ) + }; + + struct msg_func_result_t + : public base_msg_t + , public codable + { + std::string type = __EL_MSGLINK_MSG_NAME_FUNC_RESULT; + nlohmann::json results; + + EL_DEFINE_CODABLE( + msg_func_call_t, + type, + tid, + results + ) + }; + +} // namespace el diff --git a/include/el/msglink/internal/msgtype.hpp b/include/el/msglink/internal/msgtype.hpp new file mode 100644 index 0000000..cbf3df2 --- /dev/null +++ b/include/el/msglink/internal/msgtype.hpp @@ -0,0 +1,161 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +20.11.23, 18:35 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Defines all message types possible and conversions from/to string +*/ + +#pragma once + +#include + +#include "../errors.hpp" + + +#define __EL_MSGLINK_MSG_NAME_PONG "pong" +#define __EL_MSGLINK_MSG_NAME_AUTH "auth" +#define __EL_MSGLINK_MSG_NAME_AUTH_ACK "auth_ack" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB "evt_sub" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK "evt_sub_ack" +#define __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK "evt_sub_nak" +#define __EL_MSGLINK_MSG_NAME_EVT_UNSUB "evt_unsub" +#define __EL_MSGLINK_MSG_NAME_EVT_EMIT "evt_emit" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB "data_sub" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK "data_sub_ack" +#define __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK "data_sub_nak" +#define __EL_MSGLINK_MSG_NAME_DATA_UNSUB "data_unsub" +#define __EL_MSGLINK_MSG_NAME_DATA_CHANGE "data_change" +#define __EL_MSGLINK_MSG_NAME_FUNC_CALL "func_call" +#define __EL_MSGLINK_MSG_NAME_FUNC_ERR "func_err" +#define __EL_MSGLINK_MSG_NAME_FUNC_RESULT "func_result" + + +namespace el::msglink +{ + enum class msg_type_t + { + PONG, + AUTH, + AUTH_ACK, + EVENT_SUB, + EVENT_SUB_ACK, + EVENT_SUB_NAK, + EVENT_UNSUB, + EVENT_EMIT, + DATA_SUB, + DATA_SUB_ACK, + DATA_SUB_NAK, + DATA_UNSUB, + DATA_CHANGE, + FUNC_CALL, + FUNC_ERR, + FUNC_RESULT, + }; + + inline const char *msg_type_to_string(const msg_type_t _msg_type) + { + switch (_msg_type) + { + using enum msg_type_t; + + case PONG: + return __EL_MSGLINK_MSG_NAME_PONG; + break; + case AUTH: + return __EL_MSGLINK_MSG_NAME_AUTH; + break; + case AUTH_ACK: + return __EL_MSGLINK_MSG_NAME_AUTH_ACK; + break; + case EVENT_SUB: + return __EL_MSGLINK_MSG_NAME_EVT_SUB; + break; + case EVENT_SUB_ACK: + return __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK; + break; + case EVENT_SUB_NAK: + return __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK; + break; + case EVENT_UNSUB: + return __EL_MSGLINK_MSG_NAME_EVT_UNSUB; + break; + case EVENT_EMIT: + return __EL_MSGLINK_MSG_NAME_EVT_EMIT; + break; + case DATA_SUB: + return __EL_MSGLINK_MSG_NAME_DATA_SUB; + break; + case DATA_SUB_ACK: + return __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK; + break; + case DATA_SUB_NAK: + return __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK; + break; + case DATA_UNSUB: + return __EL_MSGLINK_MSG_NAME_DATA_UNSUB; + break; + case DATA_CHANGE: + return __EL_MSGLINK_MSG_NAME_DATA_CHANGE; + break; + case FUNC_CALL: + return __EL_MSGLINK_MSG_NAME_FUNC_CALL; + break; + case FUNC_ERR: + return __EL_MSGLINK_MSG_NAME_FUNC_ERR; + break; + case FUNC_RESULT: + return __EL_MSGLINK_MSG_NAME_FUNC_RESULT; + break; + + default: + throw invalid_msg_type_error("Invalid enum value: " + std::to_string((int)_msg_type)); + } + } + + inline msg_type_t msg_type_from_string(const std::string &_msg_type_name) + { + using enum msg_type_t; + + if (_msg_type_name == __EL_MSGLINK_MSG_NAME_PONG) + return PONG; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH) + return AUTH; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_AUTH_ACK) + return AUTH_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB) + return EVENT_SUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB_ACK) + return EVENT_SUB_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_SUB_NAK) + return EVENT_SUB_NAK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_UNSUB) + return EVENT_UNSUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_EVT_EMIT) + return EVENT_EMIT; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB) + return DATA_SUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB_ACK) + return DATA_SUB_ACK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_SUB_NAK) + return DATA_SUB_NAK; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_UNSUB) + return DATA_UNSUB; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_DATA_CHANGE) + return DATA_CHANGE; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_CALL) + return FUNC_CALL; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_ERR) + return FUNC_ERR; + else if (_msg_type_name == __EL_MSGLINK_MSG_NAME_FUNC_RESULT) + return FUNC_RESULT; + else + throw invalid_msg_type_error("Invalid type name: " + _msg_type_name); + } +} // namespace el::msglink + diff --git a/include/el/msglink/internal/proto_version.hpp b/include/el/msglink/internal/proto_version.hpp new file mode 100644 index 0000000..04ec94d --- /dev/null +++ b/include/el/msglink/internal/proto_version.hpp @@ -0,0 +1,53 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 21:17 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +functions to check msglink protocol version compatibility +*/ + +#pragma once + +#include +#include + +#include "../../strutil.hpp" + + +namespace el::msglink::proto_version +{ + + using proto_version_t = std::array; + + // the current protocol version of this source tree + inline const proto_version_t current = {0, 1, 0}; + + // all lower protocol versions compatible with this one + inline const std::set compatible_versions = + { + {0, 1, 0}, + }; + + /** + * @brief checks if the protocol version _other + * is compatible with the current protocol version. + * + * @param _other protocol version to check + * @return true _other is compatible with the current version + * @return false _other is not compatible with the current version + */ + inline bool is_compatible(const proto_version_t &_other) + { + return compatible_versions.contains(_other); + } + + inline std::string to_string(const proto_version_t &_ver) + { + return strutil::format("[%u.%u.%u]", _ver[0], _ver[1], _ver[2]); + } +} // namespace el::msglink diff --git a/include/el/msglink/internal/transaction.hpp b/include/el/msglink/internal/transaction.hpp new file mode 100644 index 0000000..3ae732d --- /dev/null +++ b/include/el/msglink/internal/transaction.hpp @@ -0,0 +1,88 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +30.11.23, 13:09 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Structures to represent running transactions +*/ + +#pragma once + +#include +#include + +#include "types.hpp" + + +namespace el::msglink +{ + enum class inout_t + { + INCOMING = 0, + OUTGOING = 1 + }; + + struct transaction_t + { + tid_t id; + const inout_t direction = inout_t::OUTGOING; + + // dummy to make dynamic polymorphic type casting work + virtual void _poly_dummy() const noexcept {}; + + bool is_incoming() const noexcept + { + return direction == inout_t::INCOMING; + } + + bool is_outgoing() const noexcept + { + return direction == inout_t::OUTGOING; + } + + /** + * @brief asserts that the transaction is outgoing + * and throws a protocol error if it is not. + * + * This might be use for example when an acknowledgement message is received + * where it doesn't make sense in some cases for the remote party to send + * an acknowledgement to it's own request. + * + * @param _exmsg message for the protocol error + */ + void assert_is_outgoing(const char *_exmsg) + { + if (!is_outgoing()) + throw protocol_error(_exmsg); + } + + transaction_t() = default; + transaction_t(tid_t _id, inout_t _direction) + : id(_id) + , direction(_direction) + {} + }; + + using transaction_ptr_t = std::shared_ptr; + + struct transaction_auth_t : public transaction_t + { + using transaction_t::transaction_t; + }; + + struct transaction_function_call_t : public transaction_t // only used for outgoing + { + // function called when the result message is received. + std::function handle_result; + // function called when the error message is received + std::function handle_error; + + using transaction_t::transaction_t; + }; + +} // namespace el::msglink diff --git a/include/el/msglink/internal/types.hpp b/include/el/msglink/internal/types.hpp new file mode 100644 index 0000000..60a6b90 --- /dev/null +++ b/include/el/msglink/internal/types.hpp @@ -0,0 +1,27 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +26.11.23, 21:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink type aliases used in other files to easily be able to change types +*/ + +#pragma once + +#include + +#include "proto_version.hpp" + + +namespace el::msglink +{ + using tid_t = int64_t; // transaction ID + using sub_id_t = int64_t; // subscription ID + using proto_version::proto_version_t; + using link_version_t = uint32_t; +} // namespace el::msglink diff --git a/include/el/msglink/internal/ws_close_code.hpp b/include/el/msglink/internal/ws_close_code.hpp new file mode 100644 index 0000000..b1b7b34 --- /dev/null +++ b/include/el/msglink/internal/ws_close_code.hpp @@ -0,0 +1,59 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +29.11.23, 09:38 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Websocket close codes specific to msglink. +*/ + +#pragma once + +namespace el::msglink +{ + enum class close_code_t : uint16_t + { + CLOSED_BY_USER = 1000, + PROTO_VERSION_INCOMPATIBLE = 3001, + LINK_VERSION_MISMATCH = 3002, + EVENT_REQUIREMENTS_NOT_SATISFIED = 3003, + DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED = 3004, + FUNCTION_REQUIREMENTS_NOT_SATISFIED = 3005, + MALFORMED_MESSAGE = 3006, + PROTOCOL_ERROR = 3007, + UNDEFINED_LINK_ERROR = 3100 + }; + + inline const char *close_code_name(close_code_t _code) + { + switch (_code) + { + using enum close_code_t; + + case CLOSED_BY_USER: + return "closed by user"; + case PROTO_VERSION_INCOMPATIBLE: + return "proto version incompatible"; + case LINK_VERSION_MISMATCH: + return "link version mismatch"; + case EVENT_REQUIREMENTS_NOT_SATISFIED: + return "event requirements not satisfied"; + case DATA_SOURCE_REQUIREMENTS_NOT_SATISFIED: + return "data source requirements not satisfied"; + case FUNCTION_REQUIREMENTS_NOT_SATISFIED: + return "function requirements not satisfied"; + case MALFORMED_MESSAGE: + return "malformed message"; + case PROTOCOL_ERROR: + return "protocol error"; + case UNDEFINED_LINK_ERROR: + return "undefined link error"; + default: + return "N/A"; + }; + } +} // namespace el::msglink diff --git a/include/el/msglink/internal/wspp.hpp b/include/el/msglink/internal/wspp.hpp new file mode 100644 index 0000000..eeaa77c --- /dev/null +++ b/include/el/msglink/internal/wspp.hpp @@ -0,0 +1,63 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +03.11.23, 14:13 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Includes and environment configuration for the websocket++ classes +needed by the msglink implementation +*/ + +#pragma once + +#include +#include +#include + + +namespace el::msglink +{ + // typedefs and namespaces to make code more readably + namespace wspp = websocketpp; + namespace pl = std::placeholders; + + struct wspp_config : public wspp::config::asio + { + typedef wspp_config type; // apply standard names for what is this config type + typedef asio base; // standard name for what this config is based on (the default asio config) + + typedef base::concurrency_type concurrency_type; + + typedef base::request_type request_type; + typedef base::response_type response_type; + + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::basic_socket::endpoint + socket_type; + }; + + typedef websocketpp::transport::asio::endpoint + transport_type; + }; + + typedef wspp::server wsserver; + +} // namespace el::msglink \ No newline at end of file diff --git a/include/el/msglink/link.hpp b/include/el/msglink/link.hpp new file mode 100644 index 0000000..8c0a83b --- /dev/null +++ b/include/el/msglink/link.hpp @@ -0,0 +1,1357 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +05.11.23, 18:26 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +This file implements the link class which can be inherited by +the user to define the API/protocol of a link +(Can be used on client and server side). +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "../logging.hpp" +#include "../flags.hpp" +#include "../rtti_utils.hpp" +#include "event.hpp" +#include "function.hpp" +#include "errors.hpp" +#include "subscriptions.hpp" +#include "internal/msgtype.hpp" +#include "internal/messages.hpp" +#include "internal/types.hpp" +#include "internal/proto_version.hpp" +#include "internal/link_interface.hpp" +#include "internal/transaction.hpp" +#include "internal/context.hpp" + + +namespace el::msglink +{ + /** + * @brief class that defines the custom communication "protocol" + * used to communicate with the other party. + * The link class defines which events, data subscriptions and functions + * can be transmitted and/or received from/to the other communication party. + * It is also responsible for defining the data structure and type associated + * with each of those interactions. + * + * Both clients and server have to define the link in order to be + * able to communicate. Ideally, the client and server links would match up + * meaning that for every e.g. event one party can send, the other party knows of + * and can receive this event. + * + * If a link receives any interaction from the other party that it either doesn't + * known of, cannot decode or otherwise interpret, an error is automatically + * reported to the sending party so a language-dependent error handling scheme such + * as an exception can catch it. + * + */ + class link + { + private: + // global class tree context + ct_context &ctx; + + // the link interface representing the underlying communication class + // used to send messages and manage the connection + link_interface &interface; + + // the value to step for every new transaction ID generated. + // This is set to either 1 or -1 in the constructor depending on wether + // it is being used by a server or a client + const int8_t tid_step_value; + // running counter for transaction IDs (initialized to tid_step_value) + std::atomic tid_counter; // uses atomic fetch and count + // map of active transactions that take multiple back and forth messages to complete + std::map active_transactions; + + // flags set to track the authentication process + soflag auth_ack_sent; + soflag auth_ack_received; + // set as soon as the login_ack has been sent and received + soflag authentication_done; + + // flag identifying if pong messages need to be sent out + bool pong_messages_required = false; + + // set of all possible outgoing events that are defined (including bidirectional ones) + std::set available_outgoing_events; + // set of all outgoing events that the other party has subscribed to and therefore need to be transmitted + std::set active_outgoing_events; + // set of all possible incoming events that are defined (including bidirectional ones) + std::set available_incoming_events; + // set of all incoming events that have been subscribed to and have listeners + std::set active_incoming_events; + + // running counter for all sorts of subscription IDs + std::atomic sub_id_counter = 0; // uses atomic increment + + // map of all active incoming events to their subscription IDs + std::unordered_multimap< + std::string, + sub_id_t + > event_names_to_subscription_id; + // map of subscription ID to event subscription + std::unordered_map< + sub_id_t, + std::shared_ptr + > event_subscription_ids_to_objects; + + // set of all possible outgoing functions that this party may want to call on the other party + std::set available_outgoing_functions; + // set of incoming not required, below map is used + + // type of the intermediary handler function + using function_handler_function_t = std::function; + + // map of all incoming function names to their handlers + std::unordered_map< + std::string, + function_handler_function_t + > available_incoming_function_names_to_functions; + + private: // methods + + /** + * @return tid_t new transaction ID according to the + * internal running series + */ + tid_t generate_new_tid() noexcept + { + // no lock needed here because of atomic. + // fetch the OLD value and then add step value atomically + // so first value is 1/-1 as defined in the spec + return tid_counter.fetch_add(tid_step_value); + } + + /** + * @brief Creates and registers a new transaction + * with given type, ID and init parameters in the active + * transaction map. + * It then returns a pointer to the created transaction + * + * @tparam _TR transaction type + * @tparam _Args ctor parameter types + * @param _tid transaction ID + * @param _args further ctor arguments + * @return std::shared_ptr<_TR> pointer to created transaction + */ + template _TR, typename... _Args> + inline std::shared_ptr<_TR> create_transaction(tid_t _tid, _Args ..._args) + { + if (active_transactions.contains(_tid)) + throw duplicate_transaction_error("Transaction with ID=%d already exists", _tid); + + auto new_transaction = std::make_shared<_TR>( + _tid, + std::forward<_Args>(_args)... + ); + active_transactions[_tid] = new_transaction; + + return new_transaction; + } + + /** + * @brief Retrieves the active transaction of the required + * type and ID. If there is no transaction with this ID or the + * transaction does not match the expected type, + * invalid_transaction_error is thrown. + * + * @tparam _TR expected transaction type + * @param _tid ID of action to retrieve + * @return std::shared_ptr<_TR> the targeted action + */ + template _TR> + inline std::shared_ptr<_TR> get_transaction(tid_t _tid) + { + transaction_ptr_t transaction; + + try + { + transaction = active_transactions.at(_tid); + } + catch (const std::out_of_range) + { + throw invalid_transaction_error("No active transaction with ID=%d", _tid); + } + + std::shared_ptr<_TR> target_type_transaction = + std::dynamic_pointer_cast<_TR>(transaction); + + if (target_type_transaction == nullptr) + throw invalid_transaction_error( + "Active transaction with ID=%d (%s) does not match the required type %s", + _tid, + rtti::demangle_if_possible(typeid(*transaction).name()).c_str(), + rtti::demangle_if_possible(typeid(_TR).name()).c_str() + ); + + return target_type_transaction; + } + + /** + * @brief completes a transaction by removing it from the + * map of active transactions + * + * @tparam _TR deduced transaction type + * @param _transaction transaction to remove + */ + template _TR> + inline void complete_transaction(const std::shared_ptr<_TR> &_transaction) noexcept + { + active_transactions.erase(_transaction->id); + } + + /** + * @brief updates authentication done flag from + * the other authentication flags' values + */ + void update_auth_done() noexcept + { + if (auth_ack_sent && auth_ack_received && !authentication_done) + { + authentication_done.set(); + on_authentication_done(); + } + } + + /** + * @return link_version_t link version of the link instance + */ + link_version_t get_link_version() const noexcept + { + return _el_msglink_get_link_version(); + } + + /** + * @brief sends a pong message + */ + void send_pong_message() + { + msg_pong_t msg; + interface.send_message(msg); + } + + /** + * @brief sends an event subscribe message for a specific event + * + * @param _event_name event to send sub message for + */ + void send_event_subscribe_message(const std::string &_event_name) + { + msg_evt_sub_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + interface.send_message(msg); + } + + /** + * @brief sends an event unsubscribe message for a specific event + * + * @param _event_name event to send unsub message for + */ + void send_event_unsubscribe_message(const std::string &_event_name) + { + msg_evt_unsub_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + interface.send_message(msg); + } + + /** + * @brief encodes event data and sens an event emit message for a + * specific event + * + * @param _event_name event to send + * @param _evt data to encode + */ + void send_event_emit_message( + const std::string &_event_name, + const outgoing_event &_evt + ) { + msg_evt_emit_t msg; + msg.tid = generate_new_tid(); + msg.name = _event_name; + msg.data = _evt; + interface.send_message(msg); + } + + /** + * @brief handles incoming messages (already parsed) before authentication is complete + * to perform the authentication. + * + * @param _jmsg parsed json message + */ + void handle_message_pre_auth( + const msg_type_t _msg_type, + const nlohmann::json &_jmsg + ) + { + switch (_msg_type) + { + using enum msg_type_t; + + case AUTH: + { + // validate message + msg_auth_t msg(_jmsg); + // this transaction completes immediately, no need to register + + // check protocol version if we are the higher one + if (proto_version::current > msg.proto_version) + if (!proto_version::is_compatible(msg.proto_version)) + throw incompatible_link_error( + close_code_t::PROTO_VERSION_INCOMPATIBLE, + "Incompatible protocol versions: this=%u, other=%u", + proto_version::to_string(proto_version::current).c_str(), + proto_version::to_string(msg.proto_version).c_str() + ); + + // check user defined link version + if (msg.link_version != get_link_version()) + throw incompatible_link_error( + close_code_t::LINK_VERSION_MISMATCH, + "Link versions don't match: this=%u, other=%u", + get_link_version(), + msg.link_version + ); + + // check if pong messages are required + if (msg.no_ping.has_value()) + pong_messages_required = *msg.no_ping; + + // check event list + // all the events I may require (incoming) must be included in the events + // the other party can provide (it's outgoing events) + if (!std::includes( + msg.events.begin(), msg.events.end(), + available_incoming_events.begin(), available_incoming_events.end() + )) + throw incompatible_link_error( + close_code_t::EVENT_REQUIREMENTS_NOT_SATISFIED, + "Remote party does not satisfy the event requirements (missing events)" + ); + + // check data sources + + // check functions + // all the functions this party may call (outgoing) must be included in the other + // parties callable (incoming) functions + if (!std::includes( + msg.functions.begin(), msg.functions.end(), + available_outgoing_functions.begin(), available_outgoing_functions.end() + )) + throw incompatible_link_error( + close_code_t::FUNCTION_REQUIREMENTS_NOT_SATISFIED, + "Remote party does not satisfy the function requirements (missing functions)" + ); + + // all good, send acknowledgement message, transaction complete + msg_auth_ack_t response; + response.tid = msg.tid; + interface.send_message(response); + auth_ack_sent.set(); + } + break; + + case AUTH_ACK: + { + msg_auth_ack_t msg(_jmsg); + + auto transaction = get_transaction(msg.tid); + transaction->assert_is_outgoing("Received AUTH ACK for foreign AUTH transaction"); + + complete_transaction(transaction); + auth_ack_received.set(); + } + break; + + default: + throw protocol_error("Invalid pre-auth message type: %s", msg_type_to_string(_msg_type)); + break; + } + + update_auth_done(); + } + + /** + * @brief called immediately after authentication done flag is set + * (as soon as both parties are authenticated). + * This function sends some initial post-auth messages to the other + * party. + */ + void on_authentication_done() + { + EL_LOG_FUNCTION_CALL(); + + // send event subscribe messages for all events subscribed before + // auth was complete (e.g. events with fixed handlers created during + // definition) + for (const auto &event_name : active_incoming_events) + { + send_event_subscribe_message(event_name); + } + + // ... do same for datasubs and RPCs + } + + /** + * @brief handles incoming messages (already parsed) after authentication is complete + * and both parties are authenticated. + * + * @param _jmsg parsed message + */ + void handle_message_post_auth( + const msg_type_t _msg_type, + const nlohmann::json &_jmsg + ) + { + + switch (_msg_type) + { + using enum msg_type_t; + + case EVENT_SUB: + { + msg_evt_sub_t msg(_jmsg); + + if (!available_outgoing_events.contains(msg.name)) + { + EL_LOGW("Received EVENT_SUB message for invalid event. This is likely a library implementation issue and should not happen."); + break; + } + + // otherwise activate (=subscribe to) the event + active_outgoing_events.insert(msg.name); + + // no response required + } + break; + case EVENT_UNSUB: + { + msg_evt_unsub_t msg(_jmsg); + + if (!active_outgoing_events.contains(msg.name)) + { + EL_LOGW("Received EVENT_UNSUB message for an event which was not subscribed and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // otherwise unsubscribe from the event + active_outgoing_events.erase(msg.name); + + // no response required + } + break; + case EVENT_EMIT: + { + msg_evt_emit_t msg(_jmsg); + + if (!active_incoming_events.contains(msg.name) || !event_names_to_subscription_id.contains(msg.name)) + { + EL_LOGW("Received EVENT_EMIT message for an event which was not subscribed to, isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // call all the listeners + auto range = event_names_to_subscription_id.equal_range(msg.name); // this doesn't throw even when there are no matches + for (auto it = range.first; it != range.second; ++it) + { + try + { + auto sub = event_subscription_ids_to_objects.at(it->second); + // the callback is scheduled using asio, so it is called independently of this + // call stack. This way, the subscription can manage the lock and a lock + // is not held during the callback to the user code. + ctx.io_service->post([sub, data = msg.data](){ + sub->asio_cb_call_handler(data); + }); + } + catch(const std::out_of_range& e) + { + throw invalid_identifier_error("Attempted to call event listener of invalid subscription ID. This is likely due to a library bug."); + } + } + + // no response required + } + break; + case DATA_SUB: + break; + case DATA_SUB_ACK: + break; + case DATA_SUB_NAK: + break; + case DATA_UNSUB: + break; + case DATA_CHANGE: + break; + case FUNC_CALL: + { + msg_func_call_t msg(_jmsg); + + if (!available_incoming_function_names_to_functions.contains(msg.name)) + { + EL_LOGW("Received FUNC_CALL message for a function which isn't incoming and/or doesn't exist. This is likely a library implementation issue and should not happen."); + break; + } + + // the handler is scheduled using asio, so it is called independently of this + // call stack. This way, no lock is held during the callback to user code. + // the handler function is copied from the locked map, so it is guaranteed to be valid during the asio callback. + ctx.io_service->post([this, msg = msg, handler = available_incoming_function_names_to_functions.at(msg.name)](){ + // run the handler without holding the lock + nlohmann::json results_object; + try + { + results_object = handler(msg.params); + } + catch (const std::exception &_e) + { + // acquire lock since this is an external callback + auto lock = ctx.get_lock(); + + // error during handler execution, respond with error message + msg_func_err_t response; + response.tid = msg.tid; + response.info = _e.what(); + interface.send_message(response); + return; + } + + // re-acquire lock from external callback + auto lock = ctx.get_lock(); + + // send result on success + msg_func_result_t response; + response.tid = msg.tid; + response.results = results_object; + interface.send_message(response); + }); + + } + break; + case FUNC_ERR: + { + msg_func_err_t msg(_jmsg); + auto transaction = get_transaction(msg.tid); + + // run error callback to complete future + if (transaction->handle_error != nullptr) + transaction->handle_error(msg.info); + + // complete transaction -> destroys lambdas and releases promise + complete_transaction(transaction); + } + break; + case FUNC_RESULT: + { + msg_func_result_t msg(_jmsg); + auto transaction = get_transaction(msg.tid); + + // run result callback to complete future + if (transaction->handle_result != nullptr) + transaction->handle_result(msg.results); + + // complete transaction -> destroys lambdas and releases promise + complete_transaction(transaction); + } + break; + + default: + throw protocol_error("Invalid post-auth message type: %s", msg_type_to_string(_msg_type)); + break; + } + + } + + /** + * @return sub_id_t new unique subscription ID + */ + sub_id_t generate_new_sub_id() noexcept + { + return ++sub_id_counter; + } + + /** + * @brief registers an event subscription in the internal + * map and subscribes the event from the other party + * if it isn't already. + */ + subscription_hdl_ptr add_event_subscription( + const std::string &_event_name, + event_subscription::handler_function_t _handler_function + ) { + + std::string event_name = _event_name; // copy for lambda capture + // create subscription object + const sub_id_t sub_id = generate_new_sub_id(); + auto subscription = std::shared_ptr(new event_subscription( + ctx, + _handler_function, + [this, _event_name, sub_id](void) // cancel function + { + EL_LOGD("cancel event %s:%d", _event_name.c_str(), sub_id); + // create copy of name and ID because this lambda and it's captures + // may be destroyed during the below function call + std::string l_event_name = _event_name; + sub_id_t l_sub_id = sub_id; + this->remove_event_subscription(l_event_name, l_sub_id); + } + )); + + // register the subscription + event_subscription_ids_to_objects.emplace( + sub_id, + subscription + ); + event_names_to_subscription_id.emplace( + _event_name, + sub_id + ); + + // activate the event if it is not already active + if (active_incoming_events.contains(_event_name)) + goto exit; // another listener already exits, the event is already active + + // add to list of active events + active_incoming_events.insert(_event_name); + // if authentication is done already send the subscribe message now. + // If auth is not done, sub messages will be sent as soon + // as authentication_done is set. + if (authentication_done) + send_event_subscribe_message(_event_name); + + exit: + return subscription_hdl_ptr( + new subscription_hdl( + ctx, + subscription + ) + ); + } + + /** + * @brief removes an event subscription and deactivates + * the event by sending unsubscribe message if required + * + * @param _event_name + * @param _subscription_id + */ + void remove_event_subscription( + const std::string &_event_name, + sub_id_t _subscription_id + ) { + // count amount of subscriptions left + size_t sub_count = 0; + + // iterator to store position to delete + auto target_it = event_names_to_subscription_id.end(); + + // go through all subscriptions, counting them and identifying the ony that is to be deleted + auto range = event_names_to_subscription_id.equal_range(_event_name); // this doesn't throw even when there are no matches + for (auto it = range.first; it != range.second; ++it) + { + sub_count++; + + auto sub_id = it->second; + if (it->second == _subscription_id) + { + // save position + target_it = it; + // subscription cannot be erased directly here because iterators would be invalidated + } + } + + // if a subscription was found, delete it now and remove it from count + if (target_it != event_names_to_subscription_id.end()) + { + event_names_to_subscription_id.erase(target_it); + sub_count--; + } + + // if there are no subscriptions left, deactivate the event + if (sub_count == 0) + { + active_incoming_events.erase(_event_name); + if (authentication_done) + send_event_unsubscribe_message(_event_name); + } + + // remove from id to sub object set + // Attention: This might cause the object to be destroyed, which + // will cause any parameters passed to this function by reference + // from a lambda context to become dangling pointers. After this, don't + // use parameters anymore if possible even though our cancel() lambda is designed + // in a way to avoid this issue by copying values to stack. + try + { + // make sure the subscription is invalidated + event_subscription_ids_to_objects.at(_subscription_id)->invalidate(); + // possibly delete the element + event_subscription_ids_to_objects.erase(_subscription_id); + } + catch(const std::out_of_range& e) + { + throw invalid_identifier_error("Attempted to remove event subscription with invalid subscription ID %d. This is likely a library bug.", _subscription_id); + } + } + + protected: + + /** + * @return int64_t user defined link version. + * Use the EL_LINK_VERSION macro to generate this function. + */ + virtual link_version_t _el_msglink_get_link_version() const noexcept = 0; + + /** + * @brief Macro used to define the user defined link version inside the link + * definitions. This simply creates a method returning the provided number. + * + * The link version is an integer that defined the version of the + * user defined protocol the link represents. When two parties connect, their + * link versions must match. + */ +#define EL_MSGLINK_LINK_VERSION(version_num) virtual el::msglink::link_version_t _el_msglink_get_link_version() const noexcept override { return version_num; } + + /** + * The following methods are used to define events, data subscriptions + * and RPCs and provide optional shortcut functionality + */ + + /** + * @brief Defines a bidirectional event. This method does not add + * any listeners. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event)) + */ + template + void define_event() + { + // save name + std::string event_name = _ET::_event_name; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + } + + /** + * @brief Shortcut for defining a bidirectional event + * and adding an event listener that is a method of the link. + * + * The event listener must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind listener + * to the instance. When an external listener function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event), can be deduced from method parameter) + * @tparam _LT the link class the handler is a method of (can also be deduced) + * @param _listener the handler method for the event + */ + template _LT> + event_sub_hdl_ptr define_event( + void (_LT:: *_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + static_cast<_LT *>(this), + new_event_inst + ); + } + ); + } + + /** + * @brief Shortcut for defining a bidirectional event + * and adding an event listener that is an arbitrary function. + * + * The event listener can be an arbitrary function matching the call signature + * ``` + * void(_ET &_evt) + * ```. + * If the listener is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event and el::msglink::outgoing_event + * (aka. el::msglink::bidirectional_event), can be deduced from method parameter) + * @param _listener the handler function for the event + */ + template + event_sub_hdl_ptr define_event( + void (*_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming and outgoing + available_incoming_events.insert(event_name); + available_outgoing_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + new_event_inst + ); + } + ); + } + + /** + * @brief Defines an incoming only event. This method does not add + * any listeners. + * + * @tparam _ET the event class of the event to register (must inherit from el::msglink::incoming_event) + */ + template + void define_event() + { + // save name + std::string event_name = _ET::_event_name; + + // define as incoming + available_incoming_events.insert(event_name); + } + + /** + * @brief Shortcut for defining an incoming only event + * and adding an event listener that is a method of the link. + * + * The event listener must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind listener + * to the instance. When an external listener function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event, can be deduced from method parameter) + * @tparam _LT the link class the handler is a method of (can also be deduced) + * @param _listener the handler method for the event + */ + template _LT> + event_sub_hdl_ptr define_event( + void (_LT:: *_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming + available_incoming_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + static_cast<_LT *>(this), + new_event_inst + ); + } + ); + } + + /** + * @brief Shortcut for defining an incoming only event + * and adding an event listener that is an arbitrary function. + * + * The event listener can be an arbitrary function matching the call signature + * ``` + * void(_ET &_evt) + * ```. + * If the listener is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _ET the event class of the event to register + * (must inherit from el::msglink::incoming_event, can be deduced from method parameter) + * @param _listener the handler function for the event + */ + template + event_sub_hdl_ptr define_event( + void (*_listener)(_ET &) + ) { + // save name and handler function + std::string event_name = _ET::_event_name; + std::function listener = _listener; + + // define as incoming + available_incoming_events.insert(event_name); + + // create subscription with handler function + return add_event_subscription( + event_name, + [this, listener](const nlohmann::json &_data) + { + EL_LOGD("hievent %s", _data.dump().c_str()); + _ET new_event_inst; + new_event_inst = _data; + listener( + new_event_inst + ); + } + ); + } + + /** + * @brief Defines an outgoing only event. + * + * @tparam _ET the event class of the event to register (must inherit from el::msglink::outgoing_event) + */ + template + void define_event() + { + // save name + std::string event_name = _ET::_event_name; + + // define as outgoing + available_outgoing_events.insert(event_name); + } + + + /** + * == Functions == + * + */ + + /** + * @brief Shortcut for defining a bidirectional function + * with a function that is a method of the link. + * + * The function must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind the function + * to the instance. When an external function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function and el::msglink::outgoing_function, can be deduced from method parameter) + * @tparam _LT the link class the handler function is a method of (can also be deduced) + * @param _handler the method containing the function code + */ + template _LT> + void define_function( + typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define outgoing + available_outgoing_functions.insert(function_name); + + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + static_cast<_LT *>(this), + function_parameters + )); + }; + } + + /** + * @brief Shortcut for defining a bidirectional function + * with an arbitrary handler function. + * + * The handler can be an arbitrary function matching the call signature + * ``` + * _FT::results_t(_FT::parameters_t &_params) + * ```. + * If the handler is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function and el::msglink::outgoing_function, can be deduced from method parameter) + * @param _handler the method containing the function code + */ + template + void define_function( + typename _FT::results_t (*_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as outgoing + available_outgoing_functions.insert(function_name); + + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + function_parameters + )); + }; + } + + /** + * @brief Shortcut for defining an incoming only function + * with a function that is a method of the link. + * + * The function must be a method + * of the link it is registered on. This is a shortcut + * to avoid having to use std::bind to bind the function + * to the instance. When an external function is needed, this + * is the wrong overload. + * + * @note Method function pointer: + * https://isocpp.org/wiki/faq/pointers-to-members#typedef-for-ptr-to-memfn + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function, can be deduced from method parameter) + * @tparam _LT the link class the handler function is a method of (can also be deduced) + * @param _handler the method containing the function code + */ + template _LT> + void define_function( + typename _FT::results_t (_LT:: *_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + static_cast<_LT *>(this), + function_parameters + )); + }; + } + + /** + * @brief Shortcut for defining an incoming only function + * with an arbitrary handler function. + * + * The handler can be an arbitrary function matching the call signature + * ``` + * _FT::results_t(_FT::parameters_t &_params) + * ```. + * If the handler is a method of the link instance, + * there is a special overload to simplify that case. This is not that overload. + * + * @tparam _FT the function class of the function to register + * (must inherit from el::msglink::incoming_function, can be deduced from method parameter) + * @param _handler the method containing the function code + */ + template + void define_function( + typename _FT::results_t (*_handler)(typename _FT::parameters_t &) + ) { + // save name and handler function + std::string function_name = _FT::_function_name; + std::function handler_fn = _handler; + + // define as incoming by creating intermediary handler for data conversion + available_incoming_function_names_to_functions[function_name] = [this, handler_fn](const nlohmann::json &_data) -> nlohmann::json + { + EL_LOGD("proc hdl %s", _data.dump().c_str()); + typename _FT::parameters_t function_parameters = _data; + return static_cast(handler_fn( + function_parameters + )); + }; + } + + /** + * @brief Defines an outgoing only function. + * + * @tparam _FT the function class of the function to register (must inherit from el::msglink::outgoing_function) + */ + template + void define_function() + { + // save name + std::string function_name = _FT::_function_name; + + // define as outgoing + available_outgoing_functions.insert(function_name); + } + + public: + + /** + * The following functions are used to access events, data subscriptions + * or RPCs such as by registering listeners, emitting events or updating data. + */ + + /** + * @brief emits a msglink event. + * + * @attention (external entry: public method) + * @tparam _ET event type to emit + * @param _event event body to emit + */ + template + void emit(const _ET &_event) + { + auto lock = ctx.get_lock(); + + // make sure that this event is defined + if (!available_outgoing_events.contains(_ET::_event_name)) + throw invalid_outgoing_event_error("Event '%s' cannot be emitted because it is not defined as outgoing", _ET::_event_name); + + // check if the event is needed + if (!active_outgoing_events.contains(_ET::_event_name)) + return; + + send_event_emit_message(_ET::_event_name, _event); + } + + /** + * @brief calls (or rather initiates) a msglink remote function. + * This returns a future that will contain the result of the function as soon + * as the remote party as responded with the result, or an exception if it + * responds with an error. This "call" method is supposed to be called from an + * external thread that is also able to await the future. Awaiting the future on the + * communication thread will block the asio i/o loop and therefore result in a deadlock. + * + * @attention (external entry: public method) + * @tparam _FT type of the function to call + * @param _params function parameters to pass + * @return std::future future function results data + */ + template + std::future call(const typename _FT::parameters_t &_params) + { + auto lock = ctx.get_lock(); + + // create the transaction (this this initializes the promise) + auto transaction = create_transaction( + generate_new_tid(), + inout_t::OUTGOING + ); + + // create promise for result (shared, will be deleted when lambdas are released) + auto promise = std::make_shared>(); + + // register response handlers + // (being careful not to introduce cyclic references via shared ptr) + transaction->handle_result = [promise]( // called from withing message handler, no external entry + const nlohmann::json &_result + ) { + try + { + // decode results from json and return them + promise->set_value(_result); + } + catch (const std::exception &) + { + // set exception if decode fails + promise->set_exception(std::current_exception()); + } + }; + transaction->handle_error = [promise]( // called from withing message handler, no external entry + const std::string &_info + ) { + // save error in promise + promise->set_exception( + std::make_exception_ptr( + remote_function_error(_info) + ) + ); + }; + + // send call message + msg_func_call_t msg; + msg.tid = transaction->id; + msg.name = _FT::_function_name; + msg.params = _params; + interface.send_message(msg); + + // return future + return promise->get_future(); + } + + public: + + /** + * @brief Construct a new link object. + * + * @param _ctx global class tree context passed from the owning class + * @param _is_server determines the TID series used (+n or -n) + * @param _interface interface representing the communication class used to manage connection + */ + link(ct_context &_ctx, bool _is_server, link_interface &_interface) + : ctx(_ctx) + , tid_step_value(_is_server ? 1 : -1) + , tid_counter(tid_step_value) + , interface(_interface) + {} + + ~link() + { + // possibly external entry + auto lock = ctx.get_soft_lock(); + + EL_LOG_FUNCTION_CALL(); + // invalidate all event subscriptions to make sure there are no dangling pointers + for (auto &[id, sub] : event_subscription_ids_to_objects) + sub->invalidate(); + } + + /** + * @brief valid link definitions must implement this define method + * to define the protocol by calling the specialized define + * methods for events and other interactions. + * + */ + virtual void define() noexcept = 0; + + /** + * @brief called by link interface when the connection has been + * established and communication can begin. + */ + void on_connection_established() + { + EL_LOGD("connection established called"); + + auto transaction = create_transaction( + generate_new_tid(), + inout_t::OUTGOING + ); + + // send initial auth message + msg_auth_t msg; + msg.tid = transaction->id; + msg.proto_version = proto_version::current; + msg.link_version = get_link_version(); + msg.events = available_outgoing_events; // all events this party can provide (so the other one can subscribe to them) + //msg.data_sources = ...; + // all functions this party can provide (so the other one can call them) + std::transform( // using transform to fill msg.functions with key from function map + available_incoming_function_names_to_functions.begin(), available_incoming_function_names_to_functions.end(), + std::inserter(msg.functions, msg.functions.end()), + [](auto entry){ return entry.first; } + ); + interface.send_message(msg); + } + + /** + * @brief called by link interface when an incoming message has been received. + * + * @param _msg_content message data + */ + void on_message(const std::string &_msg_content) + { + try + { + nlohmann::json jmsg = nlohmann::json::parse(_msg_content); + + // read message type (always present) + std::string msg_type = jmsg.at("type"); + // if we received a pong message, ignore it but give warning. + // This should never happen as the msglink C++ client doesn't need request this. + // And in case we are a server, we shouldn't be getting it in the first place + if (msg_type == __EL_MSGLINK_MSG_NAME_PONG) + { + EL_LOGW("Received msglink PONG message even though msglink C++ client's don't require it and/or we are a server."); + return; + } + + if (authentication_done) + { + handle_message_post_auth( + msg_type_from_string(msg_type), + jmsg + ); + } + else + { + handle_message_pre_auth( + msg_type_from_string(msg_type), + jmsg + ); + } + } + catch (const nlohmann::json::exception &e) + { + throw malformed_message_error( + "Malformed JSON link message (%s auth): %s\n%s", + authentication_done ? "post" : "pre", + _msg_content.c_str(), + e.what() + ); + } + } + + /** + * @brief called by link interface of server when WS pong has been + * received. + * Causes pong message to be transmitted if required. + */ + void on_pong_received() + { + // no lock required here because this doesn't access any user-defined data + + if (pong_messages_required) + send_pong_message(); + } + + }; + +} // namespace el::msglink diff --git a/include/el/msglink/msglink_protocol.md b/include/el/msglink/msglink_protocol.md new file mode 100644 index 0000000..615fce9 --- /dev/null +++ b/include/el/msglink/msglink_protocol.md @@ -0,0 +1,343 @@ +# Message Link + +Message link (msglink) is a custom networking protocol based on websockets that can be used to easily exchange data, events and commands between multiple systems over the network. + + +## Principles + +msglink tries to conform to these core principals: + +- Simple API: It should be easy to start a msglink server and define client handling functionality without needing to decide between a thousand different transport protocols and other config +- Clean and user-friendly API definition: event definition and use should be as simply as possible leveraging language specific features to abstract away the implementation details. There should be as little repeated boiler-plate as possible. +- Platform independence: the protocol should be usable on any OS or system using multiple programming languages (protocol features should not depend on special language functions) +- Simple state management: The user program shall not be required to manage any state related to the communication such as channel subscriptions or any of that stuff. A user should be able to say "I want this and this and this data" to the library and it must make sure these requests are fulfilled even after reconnects and similar incidents. + + +## Features + +msglink is similar to Socket.IO, except it provides additional functionality extending the simple event system. + +- Strict types and data validation +- Type-Defined events +- Data subscriptions +- Remote function calls + + +## Strict types and data validation + +One of the most annoying and often repetitive coding tasks when it comes to network communication is serialization and deserialization (especially when communicating between different languages and platforms). At this point, JSON has become the defacto standard for most general purpose web APIs and many high-level communication protocols (at least where ever maximum performance and throughput is not the most important point). While JSON is a nice way to encode data both readable with relative ease by humans and computers, it is still a ton of work to encode and decode data in your application from/to the language-internal format. + +Most modern programming languages provide some sort of native or third party support for serializing and deserializing JSON, but the problem with JSON data received via the network is, that it is fully dynamic. You cannot be sure at the time of development what the JSON object will contain. So after parsing, it is required to manually go through the JSON object, checking that all it's fields match the type and restrictions required by your program. Then the data should ideally be extracted into some form of language-specific format like a struct in C/C++. + +Luckily there are libraries that can help us simplify this task. The Python library PyDantic provides a way to elegantly define a JSON property's type, value restrictions and optional (de)serializer functions and enables simple parsing and automatic validation of incoming data. Since everything is represented by classes, static type checkers can see the datatype of properties and provide excellent editor support. In Swift, the Codable protocol is natively supported and provides similar functionality. In some languages like C++ this is not quite as simple to represent but we can still simplify the process. + +msgpack tries to implement and require these type definitions natively in it's implementation libraries, each in the style and with the features supported by the respective programming language. This way, every event has a clearly defined data structure passed along the event. Event listeners can access incoming data in the language-native format and rely on the fact that they receive what they expect. Event emitters on the other hand can pass data in the language-native format and will be forced to only emit valid data for a specific event. + + +## Type-defined events + +Traditionally, events have been identified by a simple string, it's name. There is nothing inherently wrong with this approach, but it introduces additional places to make mistakes. One may want to listen to the same event in multiple places of a program but might make a typo when identifying the event name or forget to update one listener after changing the name. + +Language features such as enums, constants or TS literal types will solve this issue. msglink aims to integrate this as a requirement in it's implementation. This goes hand-in-hand nicely with the previous point, strict types. Every event has to have a defined and validatable data type which also defines the name of the event it is associated with. After defining it once, this event type can be used everywhere in the program (details depend on language implementation) to refer to this specific event, there cannot be typos in the event name and it is impossible emit events with the wrong data structure. + + +## Data subscriptions + +Everything up to this point has just been library implementation specific improvements to the API of Socket.IO, but msglink also provides some additional features that are completely new to solve common problems in a repeatable way. + +Data subscriptions basically allow a communication party to "subscribe" to a certain data source in THE OTHER PARTY's ENVIRONMENT. At first, this may sound similar to event listeners, however there is a key difference. In the usual event system, party A doesn't know what events party B listens to. Whenever a communication party has something to offer, it simply emits an event and the other party decides if it is interested in it. + +Data subscriptions on the other hand, allow one party (we will call it the client although it doesn't matter which one is the client and which one is the server) to tell the other (the server) that it needs some data value (for example a list of all online users). The server will then send the client the requested information and also automatically send an update whenever the data changes in some way on the server. This is especially useful if the server itself needs to get these value updates from some other system and needs to subscribe/listen to them on demand (or if there are too many events to just send all of them all the time). + +These data sources do not need to be part of a static API definition that is hard-coded and known by both parties beforehand. For example, a client might want to know the activity status of user "xyz" and get automatic updates on it. But maybe user "xyz" doesn't even exist. The client can still request the wanted data source from the server, and if the server can provide it it will do so. If the server cannot provide the data source, it will respond with a not-available error or send the data as soon as it is available, depending on what is required. + +The same functionality can be implemented with simple messages, however since this is used so frequently, it gets repetitive and error-prone quite quickly. By implementing this feature in a library, the repetitive parts can be abstracted and we can even use language/framework specific features to make such "remote" data sources even more convenient to use (e.g. React State or Svelt Stores). + + +## Remote function calls + +A common use-case for messaging protocols is a remote procedure calls. This means that one party can trigger the execution of some code (the procedure) by the other party (with some optional input data). This functionality is basically equivalent to an msglink event (events even provide the ability for multiple "procedures"). Sometimes it is required to report some outcome or return some data to the calling party after the procedure has run. This is where msglink remote **function** calls come in. A remote function call consists of one communication party sending some data to the other one and causing some code to run there. Once the other party is finished, it will return the result to the calling party. + +A remote function call can be implemented by emitting an event and running some action in the even listener. At the end of the listener, another event has to be emitted containing the result returned to the client. + +There are a few problems with manually implementing this using events. First of all, in order for the request and response events to be associated with each other, some sort of unique ID must be added by the caller that is then also returned in the response so the caller can associate the result with any particular call. Second, an application is likely to have many different functions to be called, so the additional overhead of defining and emitting separate request and response events (with this ID management) for every function is quite tedious and error prone, as it involves re-implementing the same functionality multiple times. + +msglink avoids this by implementing the base functionality once and providing a language-specific and clean way to define functions in one place with input data, result data and name. This is similar to [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) but provides the additional data validation and automatic parsing functionality described above. + + +## Decision criteria + +Which of the three options provided by msglink (events, data subscriptions, RPCs) to use depends on a few criteria: + +- **value or incident** focus: What's more important? The actual data value or the fact that it changed? + > If the focus is on the value of some datum, a data subscription should be used. It has different syntax to events allowing it to be easily used as a (possibly observable) remote-synced variable. + > + > If the focus is an incident which should cause some action to be performed by the other party, then an event is the way to go. Events also carry data, but are always associated with handler functions, so called listeners, which are called when an event is received. + > + > In short: The focus of events is to run some code when something happens while the focus of a data subscription is to have some data value that can be accessed at any time without worrying about updating it. +- **conditionality**: When and for how long is some data needed and when do events need to be transmitted? + > Both event listeners and data subscriptions offer a way to "enable" and "disable" them during the lifetime of the program. It is strongly encouraged for library implementations to use of language features such as scope and object lifetime to determine when events and data subscription updates are needed in a granular way. This can save on network bandwidth. + > + > When listening to an event, a handler function (listener) is registered which is then called whenever an event is received from the other party. Registering a listener may yield an object or handle representing it. This object can then be used to manage the lifetime of the listener. For example in C++, if an event is only needed inside a class instance, the listener object should be a member of that class and be unregistered whenever the class instance is destroyed (goes out of scope). + > + > When subscribing to some data using data subscriptions, a similar object/handle will be created. Again in C++, it might be used to directly access the data using the arrow operator and manage the lifetime of the data subscription like the event listener object. +- **uniqueness**: Is a piece of data/event unique or are there multiple multiple different ones with the same structure and meaning? + > Uniqueness means, that exactly one of something exists. + > + > In the msglink protocol, events (not event instances) are unique entities. Let's say, there is an event called "device_connect". In the entire application, there is only one such event with a clearly defined data structure associated with it. However, when emitting the event, the data value may be different every time. For example this event might have a "device_id" parameter which uniquely identifies the device that has joined. No matter what the device ID is, every listener for "device_connect" will receive the event. + > + > Sometimes you may only want a listener to be called when the device with a specific ID is connected. You cannot define a different event for each device ID, because it would be very tedious and you probably don't even known at compile time what device IDs exist. Instead this would require some sort of filter, comparing the actual data. This can be done inside the listener function, but that has a big disadvantage: Even though only events with a specific device ID are required, all "device_connect" events are still transmitted over the network. And then you probably need to do the same thing with the "device_disconnect" event. + > + > In such a situation, what you really want is a data subscription which can have subscription parameters. So you might define a data source called "device_connected" which may have a boolean property "is_connected" which can be true or false. Then you define the subscription parameter to have a property "device_id". When this data source is subscribed, a device ID has to be passed to that call. The providing party can then immediately respond saying that it either can or cannot provide the data for the given device ID. If it can, it will then only update the online value for that device ID and all others will not be transmitted over the network. +- **confirmation**: Does some event require any form of confirmation/response/result from the other party? + > When the goal is for one communication party to cause some sort of action by the other party, an event can be used. + > + > However often times the executing party needs send some result data or outcome of the action back to the emitter. In the past, it was necessary to define a separate request and response event and write code for every type of interaction to sync the two up, wait for the response and so on. This is very tedious and repetitive. + > + > With msglink, for such a case a function can be defined instead of an event. A function is basically two events combined, with the only difference being that the listener now returns another object which is sent back to the emitter. This can be integrated nicely with the async programming capability of many programming languages. + > + > Another difference between functions and events is the way that they are handled on the receiving side. Since events have no way of returning data or results to the emitter, there may be many listeners that are notified of the event and can perform actions when that happens. Although each of them may cause various actions, like emitting more events as a response, none of them are responsible for or capable of defining one singular "outcome" or "result" of the event. This is a broadcast theme. + > + > With functions, this is different. Since a function has to have one single definite result to be sent back to the caller (roughly equivalent to emitter for events) after it has been handled, there can only be one handler. A function may be called from many different places, but on the receiving side, there has to be exactly one handler (function). Since it is necessary for a complete transaction to always return a result, it is also not allowed for there to be no handler at all. So functions always have exactly one handler. + + +# Protocol details + +In the following section, the details of the communication protocol will be described. Every communication step will be accompanied with an example of how the corresponding websocket message will look like. + +As an example we are using a system managing multiple hardware devices connected to some computer. A webapp (client) displays information about connected devices and allows managing them by communicating with a server running on that computer. + +The client needs: + +- to be informed when an error occurs
+ **Indicent without response -> event: "error_occurred"** +- a list of devices connected to the computer identified by their ID
+ **unique data -> data subscription (simple): "devices"** +- the power consumption of the device currently displayed in the UI
+ **non-unique data depending on parameter -> data subscription (with parameter): "power_consumption"** +- the ability for the user to disable a device, for example because it needs to much power
+ **Command with response -> RPC: "disable_device"** + +> In msglink there is (almost) no difference between the client and the server except for how the socket connection is established and who sends pings (and transaction IDs which are covered below). Therefore, any example described here could just as well work in the other direction. + + +## The basics + +As explained in the beginning, msglink uses websockets as the underlying communication layer. The websocket protocol already has the concept of messages, which is very convenient. Websockets guarantee that messages transmitted by one communication party are received as a whole and in order (no byte-stream fiddling needed). These messages are the underlying protocol data units (PDUs) used by msglink protocol. + +For now, msglink uses json to encode protocol data and user data in websocket messages. Later versions may introduce binary encoding if that turns out to be necessary for performance reasons. + +### Working message + +Working messages are just the "normal" messages sent back and forth while the connection is open. + +Most messages have 2 base properties: + +```json +{ + "type": "...", // string + "tid": 123, // int + ... +} +``` + +The **```type```** property defines the purpose of the message. There are the following message types: + +- pong +- auth +- auth_ack +- evt_sub +- evt_unsub +- evt_emit +- data_sub +- data_sub_ack +- data_sub_nak +- data_unsub +- data_change +- func_call +- func_err +- func_result + +The **```tid```** property is the transaction ID. The transaction ID is a signed integer number which (within a single session) uniquely identifies the transaction the message belongs to. The "pong" message type doesn't have this transaction ID. + +> A transaction is a (from the perspective of the protocol implementation) complete interaction between the two communication parties. It could be an event, a data subscription or an RPC.
+This is a scheme used by many networking protocols and is required for the communication parties to know what messages belong together when a single transaction requires multiple back-and-forth messages like during an RPC. This is one of those tedious repetitive things that would otherwise need to be reimplemented for every command-response event pair if it was implemented manually using only events. + +Every time a communication party starts a new interaction, it first generates a new transaction ID by using an internal ID counter. To prevent both parties from generating the same transaction ID at the same time, the **server always starts at transaction ID 1 and increments** it for each new transaction it starts (1, 2, 3, 4, ...) while the **client always starts a transaction ID -1 and decrements** from there (-1, -2, -3, -4, ...). Eventually, the two will meet in the middle when the integer overflows, which will take a very long time assuming 64 bit (or even 32 bit) integers. + +The names of properties are intentionally kept as short as possible while still being readable pretty well by humans to reduce message size. + +Messages can have other properties specific to the message type. + + +### Closing message + +When closing the msglink and therefore websocket connection, custom websocket close codes and reasons are used. The following table describes the possible codes and their meaning: + +| Code | Meaning | Notes | +|---|---|---| +| 1000 | Closed by user | Reason string is user defined. +| 3001 | msglink version incompatible | | +| 3002 | link version mismatch | | +| 3003 | Event requirement(s) unsatisfied | | +| 3004 | Data source requirement(s) unsatisfied | | +| 3005 | Function requirement(s) unsatisfied | | +| 3006 | Malformed message | includes both syntactical errors and undecodable data in terms of wrong/missing fields | +| 3007 | Protocol error | | +| 3100 | Undefined link error (unknown exception in link) | | + + +## Heartbeat and Pong message + +By default, TCP sockets and also websockets don't detect unplanned connection loss. When a connection is established, communication parties don't know if the connection is still alive unless they attempt to exchange any data. For this reason, the WebSocket protocol defines specific ping-pong functionality. A WebSocket party can send a ping message with some optional data to which the other party shall respond with a pong message, containing the same data. (This is a special message type defined by WebSocket, which is on a lower level than msglink messages. This has nothing to do with msglink messages.) If the response is not received in a certain time period, the connection is declared dead. + +In msglink, the server is responsible for sending pings. This is because most server libraries support this feature, were as some clients, such as the browser WebSocket API have now way of detecting or interacting with ping-pong messages. +As soon as the msglink connection is established, the server starts to send pings to the client in regular intervals. If a response (WS pong) is not received in time, the connection is terminated on the server side. + +In case of a broke connection however, the client will not be aware of this termination. For this reason, the client listens to the ping messages received (to which it responds with pong, likely implemented by underlying WebSocket library already) and terminates the connection if no ping message is received in a specific time period. + +If a client doesn't support the detection of ping interactions, it has to set the **```no_ping```** flag during authentication (as described [here](#authentication-procedure)). In this case, the server will send a msglink pong message (which is a regular WebSocket communication message, not a control message like pings and pongs) every time it receives a pong from the client. This poses some overhead, but less than would be caused by replacing WS ping and pong message with custom msglink messages entirely. + +```json +{ + "type": "pong" +} +``` + +The pong message does not contain any transaction ID or other additional data. This message is only ever sent from server to client. If a client sends such a message to the server, it has no effect. + + +## Authentication procedure + +When a msglink client first connects to the msglink server both parties send an initial JSON encoded authentication message to the other party containing the following information: + +```json +{ + "type": "auth", + "tid": 1, // should be 1 for server and -1 for client according to definition of tid generation above + "proto_version": [1, 2, 3], + "link_version": 1, + "no_ping": false, // optional boolean + "events": ["error_occurred"], + "data_sources": ["devices", "power_consumption"], + "functions": ["disable_device"] +} +``` + +- **```proto_verison```**: the msglink protocol version (determines whether certain features are supported) +- **```link_verison```**: the user-defined link version (version of the user defined protocol) +- **```no_ping```**: flag that can be set by the client if it doesn't support receiving pong messages from "user" code. Every client *must* respond to WS pings with WS pongs, but in some cases (such as browser API) the user code cannot detect this happening. Such a client can set this flag (_true_) during authentication causing the server to send an extra msglink "pong" message whenever a ping-pong procedure has finished, which can be used by the client to determine the health of the connection. This key can be omitted having the same result as a _false_ value. This key is to be ignored by clients if included by servers in their auth message, as only servers are responsible for conducting ping procedures. +- **```events```**: a list of events the party may emit (it's outgoing events) +- **```data_sources```**: a list of data sources the party can provide (it's outgoing data sources) +- **```functions```**: a list of remote functions the party provides (it's incoming functions) + +After receiving the message from the other party, both parties will check that the protocol versions of the other party are compatible and that the user defined link versions match. If that is not the case, the connection will be closed with code 3001 or 3002. + +> Protocol version compatibility is determined by the party with the higher (= newer) version as that one is assumed to know of and be able to judge compatibility with the lower version. If a party receives an auth message with a higher protocol version than it's own, it skips the version compatibility check. + +The message also contains lists of all the functionality the party can provide to the other one. These lists are used by the receiving party to determine weather they fulfill all it's requirements. If any requirement fails, the connection is immediately closed with the corresponding code described below. This helps to detect simple coding mistakes early and reduce the amount of errors that will occur later during communication. + +- **events**: one party's incoming event list must be a subset of the other's outgoing event list. Fails with code 3003. Fail reasons: + - If one party may want to listen for an event the other party doesn't even know about and will never be able to emit +- **data sources**: one party's data subscription list must be a subset of the other's data source list. Fails with code 3004. Fail reasons: + - If one party may subscribe to a source the other doesn't know about and provide +- **remote function calls**: one party's outgoing (called) function list must be a subset of the other's incoming (callable) function list. Fails with code 3005. Fail reasons: + - If one party may call a function the other doesn't know about and cannot handle + +Obviously these requirements are only checked approximately. The client doesn't know at that point whether the server ever will emit the "error_occurred" event or even if there will ever be a listener for it. The only thing it knows is that both the server and itself know that this event exists and know how to deal with it should that become necessary later. + +If no problems were found, each party sends an authentication acknowledgement message as a response to the other with the respective transaction ID (not a new one) to complete the authentication transaction: + +```json +{ + "type": "auth_ack", + "tid": 1 // now 1 for client, -1 for server +} +``` + +Only after both parties' authentication transactions have been successfully completed, is either party allowed to send further messages. This is defined by one party as both: + +- having sent the auth_ack message in response to the other's auth message +- having received the auth_ack message in response to it's own auth message + + +## Event messages + +If a communication party has a listener for a specific event, it needs to first subscribe to the event before it will receive it over the network. To do so, the event subscribe message is sent: + +```json +{ + "type": "evt_sub", + "tid": ..., // new transaction ID + "name": "..." +} +``` + +- **```name```**: name of the event to be subscribed to + +This assumes the event name is valid and supported by the other party, as that was already negotiated during authentication. Therefore this message is defined to **guarantee** the event will be subscribed after it is received and no response is required. + +In case this message is in fact received for an invalid event, this is due to a library implementation issue. The implementation should log this locally as a warning. + +After receiving this message, the emitting party will inform the listening one when this event type is emitted using the event emit message: + + +```json +{ + "type": "evt_emit", + "tid": ..., // new transaction ID for each emit + "name": "...", + "data": {...} +} +``` + +- **```name```**: name of the emitted event +- **```data```**: a json object containing the data associated with the event. This data will be validated according to the schema defined on the listening party and will cause a local error if it is invalid (error will not be sent to emitting party). Listeners are only called if the data was validated successfully. + +Should one party receive an event message that it hasn't subscribed to, a local warning should be logged. This doesn't cause any harm but wastes bandwidth and is likely due to a library implementation issue which is to be fixed. + +Once all listeners are disabled on the listening party, it can tell the emitting party that the event information is no longer required with the event unsubscribe message: + +```json +{ + "type": "evt_unsub", + "tid": ..., // new transaction ID + "name": "..." +} +``` + +- **```name```**: name of the event to unsubscribe from + +Similar to subscribe, there are no acknowledgement messages for unsubscribe. Unsubscribe will guarantee that no more events with the given name are received. If the unsubscribed event wasn't subscribed before or doesn't even exist, a local warning is logged on the receiving party only. + + +# Notes + +## Naming alternatives + +Note for future me: If msglink doesn't fit for some reason in the future, here are some alternative name ideas: + +- msgio (MessageIO) + + +## Link collection + +- wspp client reconnect: https://github.com/zaphoyd/websocketpp/issues/754#issue-353706390 +- wspp server and websocat client close doesn't work (closing handshake timeout), this person has the same problem I had but got no solutions: https://stackoverflow.com/questions/69447469/do-websocat-handle-closing-handshake-properly-with-a-websocketpp-server + + + +## TODOS + +```cpp + // DONE: next up is moving link and event to el .hpp files. + // TODO: Then add some more macros, a separate event class and more event define functions so a user can decide wether they want events to be just en/de codable and if they should + // be just outgoing/incoming or both. + // Also maybe add the possibility for an event handler as a method of the event class (maybe, not sure if so many options + // are a good idea). + // Then actually add this link support to the msglink server. + // Then implement a corresponding msglink client (reconnects, ...) + // Then implement state-management calls on a link (e.g. on_connect/on_disconnect, possibly a "currently not connected but attempting to reconnect, don't give up jet" state) + // Then add support for data subscriptions (they need more state management (e.g. requests ) in their own classes) + // Then add support for remote function calls. +``` \ No newline at end of file diff --git a/include/el/msglink/networking_architecture.md b/include/el/msglink/networking_architecture.md new file mode 100644 index 0000000..a98e96b --- /dev/null +++ b/include/el/msglink/networking_architecture.md @@ -0,0 +1,144 @@ +# msglink networking architecture + +in addition to defining a protocol and convenient abstraction libraries for using it, msglink libraries also define a number of different networking topologies and convenient support for them in library implementations. + +This page describes the common internal architecture "stack" of msglink implementation libraries as well as different ways multiple communication parties can interact with each other over the network. + + +## Internal architecture + +An application using the msglink protocol is divided into three blocks: + +- **Network Interface**: This block is responsible for opening, maintaining and closing a network connection to the other communication party as well as sending and receiving protocol data units (here messages). It also manages and updates the other blocks higher up in the stack depending on the network connection state. +- **Link Interface**: This is a small block which is used by the link to communicate to the network interface for sending data and controlling the connection based on the protocol. +- **Link**: This block is the responsible for managing the protocol. It receives/sends and decodes/encodes messages from/to the network interface and performs actions according to the msglink protocol. This part is responsible for performing authentication with the other party as soon as a connection is established, checking protocol compatibility, managing event- and data-subscriptions, keeping track of RPC transactions and more. + +## Link configuration + +The user of the msglink library must customize the **link** by defining their application specific events, data subscriptions and remote procedures. They can then use the link (typically an instance of a link class) to emit events or register event handlers, call remote procedures and request/provide data to/from the remote party. + +In the C++ library, this is accomplished by first defining a class/struct for each event, data subscription and remote procedure: + +```cpp +// can be incoming, outgoing or both (bidirectional) +struct didi_event : public el::msglink::bidirectional_event +{ + int height; + std::string shirt_color; + + EL_MSGLINK_DEFINE_BIDIRECTIONAL_EVENT( + didi_event, + height, + shirt_color + ) +}; + +// ... more events and other stuff +``` + +Then, a custom link class has to be created which defines all of the events, data subscriptions and procedures that are part of the application specific "protocol". There, you might also configure some static listener functions called when an e.g. an event is received: + +```cpp + +class my_link : public el::msglink::link +{ + using link::link; // default constructor + + // user-defined link version number that must match on both parties + EL_MSGLINK_LINK_VERSION(1); + + // method to handle didi even + void handle_didi(didi_event &_evt) + { + EL_LOGI( + "Got inout_didi_event with height=%d and shirt_color=%s", + _evt.height, + _evt.shirt_color.c_str() + ); + } + +public: + // object to manage event subscription + std::shared_ptr didi_subscription; + + // override the define method to add protocol definitions + virtual void define() noexcept override + { + // define event and subscribe a listener function to it + // (event type can be inferred from listener function parameter) + didi_subscription = define_event(&my_link::handle_didi); + // without listener, event needs to be named manually: + //define_event(); + + // ... define more events and other stuff + } +}; +``` + +Once the link has been defined, it can the be used when connecting with another communication party. The actual network connection part can vary greatly depending on the use case and is explained in more detail in [Networking topologies](#networking-topologies). + +For the communication to work, both parties need to have a compatible link. The link is compatible if all of the below are true: + +- msglink versions are compatible +- user defined link version (```EL_MSGLINK_LINK_VERSION(...)```) matches +- event, data sub. and procedure requirements match, meaning one party can provide all e.g. events the other needs (see [Authentication Procedure](msglink_protocol.md#authentication-procedure) for details) + + +## Networking topologies + +In the context of this document, the them "networking topology" refers to the relation between client and server parties in a msglink communication session. + +At it's core, msglink is a point-to-point communication protocol. msglink never uses broadcasts or multicasts on a network level. There are always two parties A and B communication with each other using the msglink protocol. + +In the msglink protocol, there is no logical difference between a client and a server. Both communication parties are equal and have equal capabilities (in the bounds defined by the user application). + +However, at some point, a network connection (Websocket, which builds ontop of TCP socket) needs to be established as a communication channel. For that to work, one of the two parties needs to act as a server, listening for a new connection by the other party. It is the job of the **Network Interface** architecture block to manage this connection phase. This network interface block is typically represented by a class. There are multiple network interface blocks that the user of the library can select from to assign a communication party the appropriate role in the connection establishing phase. + +### Networking role selection + +Depending on the application, the user may arbitrarily define either party to be the server or the client by using the corresponding network interface class ```el::msglink::server``` or ```el::msglink::client```. + +In many cases, like a WebApp, the role selection is obvious. In this example, it only makes sense for a program running on the webserver with a public facing IP address to be the msglink server and the browser to be the client.
+In most applications, one party is clearly the "boss" of operations and it therefor makes sense for that one to be the server. + +In a scenario two equivalent devices such as two robots on the same network communicating directly with each other, it might not matter which one is the server. When selecting the before mentioned interface classes, there is only a minimal difference in the library API. + +### Connection loss and reconnects + +In msglink, the protocol an communication state is managed by the **Link** class, while the network connection state is managed by the **Network Interface** class such as ```el::msglink::server``` or ```el::msglink::client```. When a connection is first established, the network interface instance notifies the link instance and it can perform authentication. Once that is done, the two parties might exchange event subscriptions and start communicating. + +But what happens when the connection is lost? msglink intentionally doesn't define any convoluted method for attempting to restore a lost connection and resume communication where it left off. When the connection is closed, it's closed for good and must be restarted from scratch. + +Since the communication is not resumed where it left off, we might as well delete the link instance entirely. As soon as the client detects the connection has closed, it can attempt to reconnect to the server the same way as before. A new link instance would be created and the authentication procedure repeated the same way. But what happens to the event subscriptions and event listeners present before the connection loss? They would just be forgotten entirely and the user application would need to make sure what was required and what listener functions were registered in order to re-subscribe to all the events. This would be very tedious for the user to implement. + +For this reason, in a simple application were there is only one client and server which need to communicate, the **Link** block is completely decoupled from the **Network Interface** block. For the entire lifetime of an application, there will be a single instance of the user-defined link class that is never deleted. The network interface block will simply receive a reference to this instance to control it. Internally in the link class, the user protocol state (subscriptions, ...) is also decoupled from the network and msglink protocol state (authentication, transactions, ...). + +Initially, the link instance is in a disconnected state, were the network interface block has not jet created a connection to the other party. During this time, other parts of the application can already access the link instance and register event listeners or emit events. They will not notice the connection isn't established jet and the link will keep track internally of which events are subscribed and may queue up emitted events (depending on configuration). +As soon as a connection is established and the authentication process is complete, the link will send all the event subscriptions for the currently required events according to it's internal records as well as possibly pushing queued event emissions. + +Any event subscriptions and other state changes made while the connection is established are immediately sent to the other party as well as recorded internally, so the link instance knows at all times what event subscriptions, data subscriptions, etc. it currently needs. + +When the connection is lost, both parties' network connection blocks will reset the link instance's msglink protocol and networking state to a time before authentication. This does not affect user protocol state such as event subscriptions. Form then on, the link will behave like it did before the connection was established. Any access by the rest of the application will be recorded so the current state and missed events can be sent to the other party as soon it reconnects, ensuring the state of the communication subsystem will always be clean and the communication channel behaves as the user application expects. + + +### many-to-one relationship + +In many cases, such as a server for a WebApp, it might be required for a server application to handle connection to multiple independent clients at the same time, dynamically. This is called a many-to-ony relationship. To support this functionality, you can use a special network interface class instead of teh basic server: ```el::msglink::multi_connection_server```. This class dynamically creates link instances for each client that connects to the server as well as calling a "client connected" handler to perform some initialization for the new client. + +This poses a problem when it comes to client state management however. As explained before, the user protocol state is kept within the link instance. In the simple server application only only exactly one connection, there was one global link instance. A server handling many connections needs to initialize links dynamically as soon as a client connects. + +So when a connection is lost, what happens to the link instance? Surely it cannot be kept around forever. Since there are many clients, it is tricky to tell wether a client is trying to reconnect or it's simply a new client connecting, which makes it hard to match a new connection to an existing link. That would require some sort of session identifier, which is currently not supported by msglink (more on that later). + +Luckily we can work around this issue in many cases by changing the design philosophy of the application or just viewing it differently. +When there is a server responsible for *serving* a large number of clients, it is unlikely for the server to *desperately want to get some specific piece of information globally* from *a* specific client. Instead, in most cases the clients probably want's to get notified about events from the server, so the server might not even have any event subscriptions for the client. If the client want's the server to do something, an RPC is probably better suited. And for the cases were client events are listened to by the server, every client probably has it's own event listeners associated to it, which are registered as soon as the link is instantiated when the client connects (probably methods of the link itself). In most cases the server isn't going to say *I now temporarily need to be notified about this event from that client* during runtime. And once a client disconnects, the server probably doesn't care about a specific event from that client, as it is the *client* which *wants* to send the information to the server. + +### Link lifetime + +For all these reasons, the ```el::msglink::multi_connection_server``` doesn't reconnect clients to existing links. The lifetime of a link is from when the connection is first established to when the connection is closed for whatever reason. + +The simple ```el::msglink::server``` class and also the ```el::msglink::client``` class keep the link around for the lifetime of the application, meaning the link is created when the application launches and deleted when it exits. Since these only have one global link instance, they reconnect it to the connection after the connection is closed. + +> ### Note +> The ```el::msglink::multi_connection_server``` is made for multiple ```el::msglink::client```s to connect to it. Even though the server-side link is not persistent, the client still only has one global link instance which behaves as described in the single-connection example. So when a client looses connection to a multi connection server, it still keeps and re-activates it's subscriptions just as expected. + +In the future it might be possible to add support for some sort of session identifier to the msglink authentication procedure that the client (which has a persistent link) remembers after the initial connect. When the multi connection server detects a connection loss without the connection being properly closed beforehand, it can keep it's link instance alive and reconnect it to the next new connection that authenticates with the same session identifier. diff --git a/include/el/msglink/server.hpp b/include/el/msglink/server.hpp new file mode 100644 index 0000000..ee87121 --- /dev/null +++ b/include/el/msglink/server.hpp @@ -0,0 +1,747 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +02.11.23, 22:02 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +msglink server class +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../retcode.hpp" +#include "../logging.hpp" + +#include "internal/wspp.hpp" +#include "internal/context.hpp" +#include "internal/link_interface.hpp" +#include "errors.hpp" +#include "link.hpp" + + +namespace el::msglink +{ + using namespace std::chrono_literals; + + template _LT> + class server; + + template _LT> + class connection_handler; + + /** + * @brief class that is instantiated for every connection. + * This is used inside the server class internally to perform + * actions that are specific to but needed for all open connections. + * + * Methods of this class are only allowed to be called from within + * the msglink class tree, they are not end-user facing. This class expects + * the global class tree guard to be locked for all methods called from outside + * (there are also some asio callbacks inside) + */ + template _LT> + class connection_handler : public link_interface + { + + private: // state + // global class tree context passed from server + ct_context &ctx; + + // the websocket server managing this client connection (passed from msglink server) + wsserver &socket_server; + + // a handle to the connection handled by this client + wspp::connection_hdl m_connection; + + // asio timer used to schedule keep-alive pings + std::shared_ptr m_ping_timer; + + // link instance for handling communication with this client + _LT m_link; + + // set when communication has been canceled to prevent any further actions + soflag m_communication_canceled; + + private: // methods + + /** + * @brief upgrades the connection handle m_connection + * to a full connection ptr (shared ptr to con). + * Throws error if fails because invalid connection. + * This should never happen and is supposed to be + * caught in the main asio loop. + * + * Should never be called from outside server io loop. + * + * @return wsserver::connection_ptr the upgraded connection + */ + wsserver::connection_ptr get_connection() + { + return socket_server.get_con_from_hdl(m_connection); + } + + /** + * @brief schedules a ping to be initiated + * after the configured ping interval. + * If a ping timer is active, it will be canceled. + * + */ + void schedule_ping() + { + auto con = get_connection(); + + // cancel existing timer if one is set + if (m_ping_timer) + m_ping_timer->cancel(); + + m_ping_timer = con->set_timer(1000, std::bind(&connection_handler::handle_ping_timer, this, pl::_1)); + } + + /** + * @brief initiates a websocket ping. + * This is called periodically by timer. + * + * @attention (external entry: asio cb) + */ + void handle_ping_timer(const std::error_code &_ec) + { + auto lock = ctx.get_lock(); + + // if timer was canceled, do nothing. + if (_ec == wspp::transport::error::operation_aborted) // the set_timer method intercepts the handler and changes the code to a non-default asio one + return; + else if (_ec) + throw unexpected_error(_ec); + + // send a ping + get_connection()->ping(""); // no message needed + + // when a ping is received in the pong handler, a new ping will + // be scheduled. + } + + /** + * @brief called by the close_connection() and on_close() method to + * ensure that any async communication procedures are stopped and the link is + * disconnected before entering a potentially invalid + * closing-handshake state. + * This method can be called multiple times and will do nothing after the + * first call. + */ + void cancel_communication() + { + // if already canceled, don't cancel again + if (m_communication_canceled) + return; + + // TODO: invalidate the link (i.e. "disconnect" it from the link iterface + // so it cannot call back to it anymore) + + // cancel ping timer if one is running + if (m_ping_timer) + m_ping_timer->cancel(); + + m_communication_canceled.set(); + } + + /** + * @brief closes the connection with one of the custom + * msglink close codes and logs the event to console + * + * @param _code + */ + void close_with_log(close_code_t _code) + { + EL_LOGI( + "Closing connection with code %d (%s)", + _code, + close_code_name(_code) + ); + get_connection()->close( + (uint16_t)_code, + close_code_name(_code) + ); + } + + /** + * @brief runs a passed lambda containing a link method call + * and handles exceptions thrown by the link, closing the connection + * when that happens. + * When a link throws an incompatible_link_error, the connection is + * closed with the appropriate close code. + * Other exceptions cause the connection to close code for + * undefined error. + * + * @param _lambda code to run with exception handling + */ + void run_with_exception_handling(std::function _lambda) + { + try + { + _lambda(); + } + catch (const incompatible_link_error &e) + { + EL_LOG_EXCEPTION_MSG("Remote link is not compatible", e); + close_with_log(e.code()); + } + catch (const invalid_transaction_error &e) + { + EL_LOG_EXCEPTION_MSG("Invalid transaction", e); + EL_LOGW("Ignoring invalid transaction message"); + } + catch (const malformed_message_error &e) + { + EL_LOG_EXCEPTION_MSG("Received malformed data", e); + close_with_log(close_code_t::MALFORMED_MESSAGE); + } + catch (const protocol_error &e) + { + EL_LOG_EXCEPTION_MSG("Communication doesn not comply with protocol", e); + close_with_log(close_code_t::PROTOCOL_ERROR); + } + catch (const std::exception &e) + { + EL_LOG_EXCEPTION_MSG("Unknown exception in link", e); + close_with_log(close_code_t::UNDEFINED_LINK_ERROR); + } + } + + protected: // methods + /** + * @brief implements the link_interface interface + * to allow the link to send messages through the client + * communication channel. + * + * @param _content message content + */ + virtual void send_message(const std::string &_content) override + { + // ensure no messages go through after cancel, even though link + // shouldn't call this method anymore after cancel anyway. + if (m_communication_canceled) + return; + + EL_LOGD("Outgoing Message: %s", _content.c_str()); + get_connection()->send(_content); + } + + + public: + + // connection handler is supposed to be instantiated in-place exactly once per + // connection in the connection map. It should never be moved or copied. + connection_handler(const connection_handler &) = delete; + connection_handler(connection_handler &&) = delete; + + /** + * @brief called during on_open when new connection + * is established. Used only for initialization. + * + * @param _ctx global class tree context + * @param _socket_server websocket server the connection belongs to + * @param _connection handle to the connection + */ + connection_handler(ct_context &_ctx, wsserver &_socket_server, wspp::connection_hdl _connection) + : ctx(_ctx) + , socket_server(_socket_server) + , m_connection(_connection) + , m_link( + ctx, + true, // is server instance + *this // use this connection handler to communicate + ) + { + EL_LOG_FUNCTION_CALL(); + + // define the link protocol + run_with_exception_handling([&](){ + this->m_link.define(); + }); + } + + /** + * @brief called during on_close when connection is closed or terminated. + * Used to clean up resources like canceling any potential actions. + */ + virtual ~connection_handler() + { + auto lock = ctx.get_soft_lock(); + EL_LOG_FUNCTION_CALL(); + + // cancel ping timer if one is running + if (m_ping_timer) + m_ping_timer->cancel(); + } + + /** + * @brief implements the link_interface interface + * to allow the link to close the connection at any point. + * + * @param _code close status code + * @param _reason readable reason as required by websocket protocol + */ + virtual void close_connection(int _code, std::string _reason) noexcept override + { + EL_LOG_FUNCTION_CALL(); + + // to avoid communication actions during closing handshake + cancel_communication(); + // close the connection gracefully + get_connection()->close(_code, _reason); + + // WARNING: connection_handler instance is destroyed before the terminate() call returns. + // Don't use it here anymore! This must also be regarded by the link. + } + + + /** + * @brief called by server immediately after connection is + * established (and probably this instance has been constructed). + * This is used to initiate any communication actions. + * + */ + void on_open() + { + EL_LOG_FUNCTION_CALL(); + + // start the first ping + schedule_ping(); + + // start communication by notifying the link + // TODO: i.e. "connecting" the link to the interface (change how this works) + run_with_exception_handling([&](){ + this->m_link.on_connection_established(); + }); + } + + /** + * @brief called by server when message arrives for this connection + * + * @param _msg the message to handle + */ + void on_message(wsserver::message_ptr _msg) + { + EL_LOGD("Incoming Message: %s", _msg->get_payload().c_str()); + run_with_exception_handling([&](){ + this->m_link.on_message(_msg->get_payload()); + }); + } + + /** + * @brief called by server when a pong message arrives for this connection. + * This is used to test if the connection is still alive + * + * @param _payload the pong payload (not used) + */ + void on_pong_received(std::string &_payload) + { + // pong arrived in time, all good, connection alive + + // notify link, so pong message can be sent to client if needed + run_with_exception_handling([&](){ + m_link.on_pong_received(); + }); + + // schedule a new ping to be sent a bit later. + schedule_ping(); + } + + /** + * @brief called by server when an expected pong message (given the sent + * ping messages) has not arrived in time. When this happens, + * the connection is considered dead and will be terminated. + * + * @param _expected_payload the expected payload from the ping message + */ + void on_pong_timeout(std::string &_expected_payload) + { + cancel_communication(); + + // terminate connection + get_connection()->terminate(std::make_error_code(std::errc::timed_out)); + + // WARNING: connection_handler instance is destroyed before the terminate() call returns. + // Don't use it here anymore! + } + + /** + * @brief called by the server when the connection has been closed (for any reason, + * whether initiated by client or by server) to stop any potentially still running + * communication procedures. + */ + void on_close() + { + EL_LOG_FUNCTION_CALL(); + + // might already have been called by close_connection() but doesn't matter. + cancel_communication(); + } + + }; + + template _LT> + class server + { + + private: + // global communication class tree context + ct_context ctx; + + // == Configuration + // port to serve on + int port; + + // == State + // the websocket server used for transport + wsserver socket_server; + + // enumeration managing current server state + enum server_state_t + { + UNINITIALIZED = 0, // newly instantiated, not initialized + INITIALIZED = 1, // initialize() successful + RUNNING = 2, // run() called, server still running + FAILED = 3, // run() exited with error + STOPPED = 4 // run() exited cleanly (through stop() or other natural way) + }; + std::atomic server_state { UNINITIALIZED }; + + // set of connections to corresponding connection handler instance + std::map< + wspp::connection_hdl, + connection_handler<_LT>, + std::owner_less + > open_connections; + + private: + + /** + * @brief new websocket connection opened (fully connected) + * This instantiates a connection handler. + * + * @attention (external entry: asio cb) + * @param hdl websocket connection handle + */ + void on_open(wspp::connection_hdl _hdl) + { + auto lock = ctx.get_lock(); + + if (server_state != RUNNING) + return; + + // create new handler instance and save it + auto new_connection = open_connections.emplace( + std::piecewise_construct, // Needed for in-place construct https://en.cppreference.com/w/cpp/utility/piecewise_construct + std::forward_as_tuple(_hdl), + std::forward_as_tuple(ctx, socket_server, _hdl) + ); + + // notify new connection handler to start communication + new_connection.first->second.on_open(); + } + + /** + * @brief message received from a connection. + * This forwards the call to the appropriate connection handler + * or throws if the connection is invalid. + * + * @attention (external entry: asio cb) + * @param _hdl ws connection handle + * @param _msg message that was received + */ + void on_message(wspp::connection_hdl _hdl, wsserver::message_ptr _msg) + { + auto lock = ctx.get_lock(); + + if (server_state != RUNNING) + return; + + // forward message to connection handler + try + { + open_connections.at(_hdl).on_message(_msg); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Received message from unknown/invalid connection."); + } + } + + /** + * @brief websocket connection has been closed, + * Whether gracefully or dropped. This deletes + * the associated connection handler and therefore + * stops any tasks going on with that connection. + * + * @attention (external entry: asio cb) + * @param _hdl ws connection handle that has been closed + */ + void on_close(wspp::connection_hdl _hdl) + { + auto lock = ctx.get_lock(); + + if (server_state != RUNNING) + return; + + if (!open_connections.contains(_hdl)) + { + throw invalid_connection_error("Attempted to close an unknown/invalid connection which doesn't seem to exist."); + } + + // notify connection to stop communication + open_connections.at(_hdl).on_close(); + + // remove closed connection from connection map, deleting the connection handlers + open_connections.erase(_hdl); + } + + /** + * @brief called by wspp when a new connection was attempted but failed + * before it was fully connected. + * + * @attention (external entry: asio cb) + * @param _hdl handle to associated ws connection + */ + void on_fail(wspp::connection_hdl _hdl) + { + //auto lock = ctx.get_lock(); + EL_LOG_FUNCTION_CALL(); + } + + /** + * @brief called by wspp when a pong is received. + * This is forwarded to the connection handler. + * + * @attention (external entry: asio cb) + * @param _hdl handle to associated ws connection + */ + void on_pong_received(wspp::connection_hdl _hdl, std::string _payload) + { + auto lock = ctx.get_lock(); + + if (server_state != RUNNING) + return; + + // forward message to connection handler + try + { + open_connections.at(_hdl).on_pong_received(_payload); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Received pong from unknown/invalid connection."); + } + } + + /** + * @brief called by wspp when a pong message times out. This is + * used by the keepalive system to detect connection loss. + * This call is forwarded to connection handler. + * + * @attention (external entry: asio cb) + * @param _hdl handle to connection where timeout occurred + */ + void on_pong_timeout(wspp::connection_hdl _hdl, std::string _expected_payload) + { + auto lock = ctx.get_lock(); + + if (server_state != RUNNING) + return; + + // forward message to connection handler + try + { + open_connections.at(_hdl).on_pong_timeout(_expected_payload); + } + catch (const std::out_of_range &e) + { + throw invalid_connection_error("Pong timeout on unknown/invalid connection."); + } + } + + public: + + /** + * @brief Construct a new server object + * + * @attention (external entry: public method) + * @param _port TCP port to listen on + */ + server(int _port) + : port(_port) + { + EL_LOG_FUNCTION_CALL(); + } + + // never copy or move a server + server(const server &) = delete; + server(server &&) = delete; + + /** + * @attention (external entry: public method) + */ + ~server() + { + auto lock = ctx.get_soft_lock(); + EL_LOG_FUNCTION_CALL(); + } + + /** + * @brief initializes the server setting up all transport settings + * and preparing the server to run. This MUST be called before run(). + * + * @attention (external entry: public method) + * @throws msglink::initialization_error invalid state to initialize + * @throws msglink::socket_error error while configuring networking + * @throws other std exceptions possible + */ + void initialize() + { + auto lock = ctx.get_lock(); + + if (server_state != UNINITIALIZED) + throw initialization_error("msglink server instance is single use, cannot re-initialize"); + + try + { + // wspp log messages off by default + socket_server.clear_access_channels(wspp::log::alevel::all); + socket_server.clear_error_channels(wspp::log::elevel::all); + // turn on selected logging channels + socket_server.set_access_channels(wspp::log::alevel::disconnect); + socket_server.set_access_channels(wspp::log::alevel::connect); + socket_server.set_access_channels(wspp::log::alevel::fail); + socket_server.set_error_channels(wspp::log::elevel::all); + //socket_server.set_access_channels(wspp::log::alevel::all); + + // initialize asio communication (use external io service from context) + socket_server.init_asio(ctx.io_service.get()); + + // register callback handlers (More handlers: https://docs.websocketpp.org/reference_8handlers.html) + socket_server.set_open_handler(std::bind(&server::on_open, this, pl::_1)); + socket_server.set_message_handler(std::bind(&server::on_message, this, pl::_1, pl::_2)); + socket_server.set_close_handler(std::bind(&server::on_close, this, pl::_1)); + socket_server.set_fail_handler(std::bind(&server::on_fail, this, pl::_1)); + socket_server.set_pong_handler(std::bind(&server::on_pong_received, this, pl::_1, pl::_2)); + socket_server.set_pong_timeout_handler(std::bind(&server::on_pong_timeout, this, pl::_1, pl::_2)); + + // set reuse addr flag to allow faster restart times + socket_server.set_reuse_addr(true); + + } + catch (const wspp::exception &e) + { + throw socket_error(e); + } + + server_state = INITIALIZED; + } + + /** + * @brief runs the server I/O loop (blocking) + * + * @attention (external entry: public method) + * @throws msglink::launch_error couldn't run server because of invalid state (e.g. not initialized) + * @throws msglink::socket_error network communication / websocket error occurred + * @throws other msglink::msglink_error? + * @throws other std exceptions possible + */ + void run() + { + auto lock = ctx.get_lock(); + + if (server_state == UNINITIALIZED) + throw launch_error("called server::run() before server::initialize()"); + else if (server_state != INITIALIZED) + throw launch_error("called server::run() multiple times (msglink server instance is single use, cannot run multiple times)"); + + try + { + // listen on configured port + socket_server.listen(port); + + // start accepting + socket_server.start_accept(); + + server_state = RUNNING; + // free the lock so state can be accessed by callbacks + lock.unlock(); + // run the io loop + socket_server.run(); + // re-acquire ownership after loop exit. This might block until close function is done + lock.lock(); + server_state = STOPPED; + } + catch (const wspp::exception &e) + { + lock.lock(); + server_state = FAILED; + throw socket_error(e); + } + catch (...) + { + lock.lock(); // TODO: may be called on lock exception in which case no second lock should be attempted. + server_state = FAILED; + throw; + } + + } + + /** + * @brief stops the server if it is running, does nothing + * otherwise (if it's not running). + * + * This can be called from any thread. + * + * @attention (external entry: public method) + * @throws msglink::socket_error networking error occurred while stopping server + * @throws other msglink::msglink_error? + */ + void stop() + { + auto lock = ctx.get_lock(); + + // do nothing if server is not running + if (server_state != RUNNING) return; + + try + { + // stop listening for new connections + socket_server.stop_listening(); + + // close all existing connections + for (auto &[hdl, client] : open_connections) + { + // use close_connection method to ensure communication is properly + // stopped, preventing errors + client.close_connection(0, "server stopped"); + } + } + catch (const wspp::exception &e) + { + throw socket_error(e); + } + + // at this point mutex will be unlocked and run function will finish + } + + }; + +} // namespace el \ No newline at end of file diff --git a/include/el/msglink/subscriptions.hpp b/include/el/msglink/subscriptions.hpp new file mode 100644 index 0000000..dd1f29e --- /dev/null +++ b/include/el/msglink/subscriptions.hpp @@ -0,0 +1,225 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +17.12.23, 21:10 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Structures representing various types of subscriptions. +*/ + +#pragma once + +#include +#include +#include + +#include "../flags.hpp" +#include "../logging.hpp" +#include "internal/types.hpp" +#include "internal/context.hpp" + + +namespace el::msglink +{ + /** + * @brief structure representing an event subscription which + * is used to identify and cancel the subscription later. + * This is not a public class. It is only for use withing the communication + * class tree. + */ + class event_subscription + { + protected: + friend class link; + + // type of the lambda used to wrap event handlers + using handler_function_t = std::function; + // type of the lambda used for the cancel callback + using cancel_function_t = std::function; + + // global class tree context + ct_context &ctx; + + // function called when the event is received + handler_function_t handler_function; + // function called to cancel the event (will be a + // lambda created by the link) + cancel_function_t cancel_function; + + // invalidates all potential references and callbacks to the link to + // prevent any calls back to a potentially deallocated link instance. + // This is called by the link destructor, so it is no external entry + void invalidate() + { + cancel_function = nullptr; + handler_function = nullptr; + } + + /** + * @brief Function to be called from an asio callback + * when the event is received. Because this is called by + * an asio callback, this is an external entry. + * + * @attention (external entry: asio callback) + * @param _data + */ + void asio_cb_call_handler(const nlohmann::json &_data) + { + auto lock = ctx.get_lock(); + + if (handler_function != nullptr) + { + auto handler_copy = handler_function; + // Unlock to allow user callback to run without lock, so it can call external tree entries + lock.unlock(); + // possible inconsistency problem when subscription is canceled by another thread right here. + // The function will still be called because of the copy, but user may not expect it. + // At this time, I don't know how to solve this without keeping the tree locked. + handler_copy(_data); + lock.lock(); // re-lock after callback in case more has to be done + } + } + + /** + * @brief Construct a new subscription + * + * @param _ctx global class tree context + * @param _handler_function user code handler function (with decode wrapper) + * @param _cancel_function internal cancellation function + */ + event_subscription( + ct_context &_ctx, + handler_function_t _handler_function, + cancel_function_t _cancel_function + ) + : ctx(_ctx) + , handler_function(_handler_function) + , cancel_function(_cancel_function) + {} + + public: + event_subscription(const event_subscription &) = default; + event_subscription(event_subscription &&) = default; + + /** + * @brief Invalidates the object. + */ + ~event_subscription() + { + auto lock = ctx.get_soft_lock(); + + EL_LOG_FUNCTION_CALL(); + + invalidate(); + } + + /** + * @brief function to cancel the subscription and therefore + * unsubscribe from the event. If already canceled, this does + * nothing. + * + * This is only called from subscription handle and is therefore + * not an external entry. + */ + void cancel() + { + if (cancel_function != nullptr) + { + cancel_function(); + cancel_function = nullptr; // only cancel once + } + } + }; + + /** + * @brief a class which handles subscription lifetime using + * RAII functionality. + * When a subscription is returned from the msglink library + * to user code, it is wrapped by a subscription handle. The handle + * itself is wrapped by a shared_ptr, so there is only ever one handle + * per subscription. + * The subscription is automatically canceled when the lifetime of + * the subscription handle ends, i.e. when no nobody holds a reference + * to it anymore. + * This is to prevent callbacks to class instances which don't exist anymore + * when forgetting to cancel subscriptions in class destructors. + * One can also cancel the subscription manually before this happens using + * the cancel() method though. + * + * @attention The subscription handle is an entirely external, public object. Apart + * from construction, it is never accessed from the communication class tree. + * As such, all functions it calls internally count as external entries. + */ + template + class subscription_hdl + { + friend class link; + + protected: + + // global class tree context + ct_context &ctx; + + // the managed subscription (this instance and the link instance are the only two references of this) + std::shared_ptr<_SUB_T> subscription_ptr; // must be a counted reference (not weak) to ensure ->cancel() is called on subscription + + // no copy or move + subscription_hdl(const subscription_hdl &) = delete; + subscription_hdl(subscription_hdl &&) = delete; + + // only link is allowed to construct instance with valid subscription pointer + subscription_hdl( + ct_context &_ctx, + std::shared_ptr<_SUB_T> _sub_ptr + ) + : ctx(_ctx) + , subscription_ptr(_sub_ptr) + { + EL_LOG_FUNCTION_CALL(); + } + + public: + + /** + * cancels the subscription + */ + ~subscription_hdl() + { + // possibly external entry + auto lock = ctx.get_soft_lock(); + + EL_LOG_FUNCTION_CALL(); + if (subscription_ptr != nullptr) + subscription_ptr->cancel(); + } + + /** + * @brief cancels the subscription even if there + * are still references to the subscription_hdl. + * Usually, this is not required. + * + * @attention (external entry: public method) + */ + void cancel() + { + auto lock = ctx.get_lock(); + + if (subscription_ptr != nullptr) + subscription_ptr->cancel(); + } + }; + + // shortcut for shared pointer to subscription handle which is the object actually returned to user code + template + using subscription_hdl_ptr = std::shared_ptr>; + + /** + * @brief shortcut for shared_ptr to subscription_hdl for event_subscription. + * This type is library user-facing, so this is a short alias. + */ + using event_sub_hdl_ptr = subscription_hdl_ptr; +} // namespace el::msglink diff --git a/include/el/nummap.hpp b/include/el/nummap.hpp new file mode 100644 index 0000000..20d5f80 --- /dev/null +++ b/include/el/nummap.hpp @@ -0,0 +1,46 @@ +/* +ELEKTRON © 2024 - now +Written by melektron +www.elektron.work +18.06.24, 00:07 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +numerical mapping functions +*/ + +#pragma once + +#include "cxxversions.h" + + +namespace el +{ + /** + * @brief linearly maps a number from range [_in_a .. _in_b] to + * range [_out_a .. _out_b]. This may include inversions + * in direction (e.g. mapping 1-4 to 4-1). The number + * does not have to be within and is not clamped to the specified ranges, + * they merely serve as the two points required to identify a linear function. + * + * @tparam _NT number type used (when using small integers, internally used division may cause unusable output) + * @param _x number to map + * @param _in_a first input value (that will be mapped to _out_a) + * @param _in_b second input value (that will be mapped to _out_b) + * @param _out_a first output value (mapped from _in_a) + * @param _out_b second output value (mapped from _in_b) + * @return _NT mapped number + */ + template + _NT map_lin( + _NT _x, + _NT _in_a, + _NT _in_b, + _NT _out_a, + _NT _out_b + ) { + return (_x - _in_a) * (_out_b - _out_a) / (_in_b - _in_a) + _out_a; + } +} // namespace el diff --git a/include/el/retcode.hpp b/include/el/retcode.hpp index 7d17df9..d87ee4c 100644 --- a/include/el/retcode.hpp +++ b/include/el/retcode.hpp @@ -1,5 +1,5 @@ /* -ELEKTRON © 2022 +ELEKTRON © 2022 - now Written by melektron www.elektron.work 27.12.22, 13:44 diff --git a/include/el/rtti_utils.hpp b/include/el/rtti_utils.hpp new file mode 100644 index 0000000..0f98a61 --- /dev/null +++ b/include/el/rtti_utils.hpp @@ -0,0 +1,36 @@ +/* +ELEKTRON © 2023 - now +Written by melektron +www.elektron.work +03.12.23, 23:03 +All rights reserved. + +This source code is licensed under the Apache-2.0 license found in the +LICENSE file in the root directory of this source tree. + +Utilities for runtime type information (RTTI) +*/ + +#pragma once + +#include +#ifdef __GNUC__ +#include +#endif + +namespace el::rtti +{ + inline std::string demangle_if_possible(const char* _typename) + { + +#ifdef __GNUC__ + int status = 0; + char *ex_type_name = abi::__cxa_demangle(_typename, nullptr, nullptr, &status); + std::string output(ex_type_name); + free(ex_type_name); // required by cxxabi + return output; +#else + return _typename +#endif + } +} // namespace el::rtti diff --git a/include/el/struct_proxy.hpp b/include/el/struct_proxy.hpp index 427f330..8f62cc1 100644 --- a/include/el/struct_proxy.hpp +++ b/include/el/struct_proxy.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2023 -Written by Matteo Reiter +ELEKTRON © 2023 - now +Written by melektron www.elektron.work 10.06.23, 23:29 All rights reserved. diff --git a/include/el/strutil.hpp b/include/el/strutil.hpp index 3fa5986..e31541c 100644 --- a/include/el/strutil.hpp +++ b/include/el/strutil.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 07.10.22, 21:28 All rights reserved. @@ -20,6 +20,9 @@ Utility functions operating for strings, mostly STL compatible string types. #include #include +#include "cxxversions.h" + + namespace el::strutil { /** @@ -29,35 +32,41 @@ namespace el::strutil * NOTE: This method uses dynamic memory allocation ("new" operator) and std::unique_ptr. * Usually, the template arguments don't have to be provided but can be deducted from function * arguments. + * Inspiration: https://stackoverflow.com/a/26221725 * - * @tparam _ST string type of the format and return value. The type must be constructable - * from "const char *" (string copy) and must have a .c_str() method to convert to "char *"". * @tparam _Args varadic format argument types * @param _fmt Format string * @param _args Format arguments - * @return _ST newly created string of specified type + * @return newly created string of specified type */ - template - _ST format(const _ST& _fmt, _Args... _args) + template + std::string format(const std::string& _fmt, _Args... _args) { - size_t len = snprintf(nullptr, 0, _fmt.c_str(), _args...) + 1; // extra space for null byte + int len_or_error = std::snprintf(nullptr, 0, _fmt.c_str(), _args...) + 1; // extra space for null byte + + if( len_or_error <= 0 ) + #ifdef __EL_ENABLE_EXCEPTIONS + throw std::runtime_error( "Error during formatting." ); + #else + return ""; + #endif + + auto len = static_cast(len_or_error); + std::unique_ptr _cstr(new char[len]); - snprintf(_cstr.get(), len, _fmt.c_str(), _args...); - return _ST(_cstr.get()); + std::snprintf(_cstr.get(), len, _fmt.c_str(), _args...); + + return std::string(_cstr.get()); } /** * @brief creates a copy of a string with all lowercase letters. * The tolower() C function is used to convert the letters. - * Any string class compatible with the C++ std::string class in terms - * of iteration and uses "char" as the character type can be used. * - * @tparam _ST string type to be used (deducted, typically std::string) * @param instr the input string to convert - * @return _ST copy of the string in lowercase + * @return copy of the string in lowercase */ - template - _ST lowercase(_ST instr) + inline std::string lowercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::tolower(c); }); @@ -68,15 +77,11 @@ namespace el::strutil /** * @brief creates a copy of a string with all lowercase letters. * The tolower() C function is used to convert the letters. - * Any string class compatible with the C++ std::string class in terms - * of iteration and uses "char" as the character type can be used. * - * @tparam _ST string type to be used (deducted, typically std::string) * @param instr the input string to convert - * @return _ST copy of the string in lowercase + * @return copy of the string in lowercase */ - template - _ST uppercase(_ST instr) + inline std::string uppercase(std::string instr) { std::for_each(instr.begin(), instr.end(), [](char &c) { c = ::toupper(c); }); @@ -88,13 +93,11 @@ namespace el::strutil * @brief Reads the entire content of a file and stores it in a string. * @exception This function can trough any exception that the string or ifstream can. * - * @tparam _ST string type, typically std::string (can be deducted) * @param _file The file stream to read from * @param _string The string to store the file contents in. This will overwrite the string. * @return The length of the file (= the number of characters copied to the string) */ - template - size_t read_file_into_string(std::ifstream &_file, _ST &_string) + inline size_t read_file_into_string(std::ifstream &_file, std::string &_string) { // get file length _file.seekg(0, std::ios::end); @@ -110,21 +113,21 @@ namespace el::strutil /** - * @brief stringswitch - a macro based wrapper for if statements - * allowing you to compare std::strings using syntax somewhat similar to - * switch-case statements. As this is purly macro based, there will be - * no namespace annotations unfortunately. + * @brief chain compare - a macro based wrapper for if statements + * allowing you to compare arbitrary types using syntax somewhat similar to + * switch-case statements. As this is purely macro based, there will generate + * if else statements comparing values. * * Limitations: variables created inside the block are local, every case * has to use brackets if it is more than one statement in size */ +#define el_chain_compare(variable) \ + { \ + const auto &__el_strswitch_strtempvar__ = variable; \ + if (false) {} -#define stringswitch(strval) {\ - const std::string &__el_strswitch_strtempvar__ = strval; - -#define scase(strval) if (__el_strswitch_strtempvar__ == strval) - -#define switchend } +#define el_case(value) else if (__el_strswitch_strtempvar__ == value) +#define el_end_compare } }; \ No newline at end of file diff --git a/include/el/types.hpp b/include/el/types.hpp index 8cf57ea..9b57406 100644 --- a/include/el/types.hpp +++ b/include/el/types.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 07.10.22, 22:13 All rights reserved. @@ -70,7 +70,7 @@ namespace el::types std::string to_string() const noexcept { - return strutil::format("(r=%3d, g=%3d, b=%3d)", r, g, b); + return strutil::format("(r=%3d, g=%3d, b=%3d)", r, g, b); } /** @@ -122,7 +122,7 @@ namespace el::types std::string to_string() const noexcept { - return strutil::format("(r=%3lf, g=%3lf, b=%3lf)", r, g, b); + return strutil::format("(r=%3lf, g=%3lf, b=%3lf)", r, g, b); } /** @@ -149,5 +149,8 @@ namespace el::types } }; + // destructures RGB colors into three function parameters + #define EL_RGB_DESTRUCTURE(c) (c).r, (c).g, (c).b + using mac_t = uint64_t; }; \ No newline at end of file diff --git a/include/el/universal.hpp b/include/el/universal.hpp index b3205f1..bb88873 100644 --- a/include/el/universal.hpp +++ b/include/el/universal.hpp @@ -1,6 +1,6 @@ /* -ELEKTRON © 2022 -Written by Matteo Reiter +ELEKTRON © 2022 - now +Written by melektron www.elektron.work 06.10.22, 21:50 All rights reserved.