diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d32951a9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +Copyright (c) 2018 Machine Zone, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 8cd3e0f3..4ad7efe5 100644 --- a/README.md +++ b/README.md @@ -1 +1,163 @@ -# IXWebSocket +# General + +## Introduction + +[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex +communication channels over a single TCP connection. This library provides a C++ library for Websocket communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient). + +## Examples + +The examples folder countains a simple chat program, using a node.js broadcast server. + +Here is what the API looks like. + +``` +ix::WebSocket webSocket; + +std::string url("ws://localhost:8080/"); +webSocket.configure(url); + +// Setup a callback to be fired when a message or an event (open, close, error) is received +webSocket.setOnMessageCallback( + [](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error) + { + if (messageType == ix::WebSocket_MessageType_Message) + { + std::cout << str << std::endl; + } +}); + +// Now that our callback is setup, we can start our background thread and receive messages +webSocket.start(); + +// Send a message to the server +webSocket.send("hello world"); + +// ... finally ... + +// Stop the connection +webSocket:stop() +``` + +## Implementation details + +### TLS/SSL + +Connections can be optionally secured and encrypted with TLS/SSL when using a wss:// endpoint, or using normal un-encrypted socket with ws:// endpoints. AppleSSL is used on iOS and OpenSSL is used on Android. + +### Polling and background thread work + +No manual polling to fetch data is required. Data is sent and received instantly by using a background thread for receiving data and the select [system](http://man7.org/linux/man-pages/man2/select.2.html) call to be notified by the OS of incoming data. No timeout is used for select so that the background thread is only woken up when data is available, to optimize battery life. This is also the recommended way of using select according to the select tutorial, section [select law](https://linux.die.net/man/2/select_tut). Read and Writes to the socket are non blocking. Data is sent right away and not enqueued by writing directly to the socket, which is [possible](https://stackoverflow.com/questions/1981372/are-parallel-calls-to-send-recv-on-the-same-socket-valid) since system socket implementations allow concurrent read/writes. However concurrent writes need to be protected with mutex. + +### Automatic reconnection + +If the remote end (server) breaks the connection, the code will try to perpetually reconnect, by using an exponential backoff strategy, capped at one retry every 10 seconds. + +## Limitations + +* There is no per message compression support. That could be useful for retrieving large messages, but could also be implemented at the application level. However that would conflict with auto-serialiasation. +* There is no text support for sending data, only the binary protocol is supported. Sending json or text over the binary protocol works well. +* Automatic reconnection works at the TCP socket level, and will detect remote end disconnects. However, if the device/computer network become unreachable (by turning off wifi), it is quite hard to reliably and timely detect it at the socket level using `recv` and `send` error codes. [Here](https://stackoverflow.com/questions/14782143/linux-socket-how-to-detect-disconnected-network-in-a-client-program) is a good discussion on the subject. This behavior is consistent with other runtimes such as node.js. One way to detect a disconnected device with low level C code is to do a name resolution with DNS but this can be expensive. Mobile devices have good and reliable API to do that. + +## Examples + +1. Bring up a terminal and jump to the examples folder. +2. Compile the example C++ code. `sh build.sh` +3. Install node.js from [here](https://nodejs.org/en/download/). +4. Type `npm install` to install the node.js dependencies. Then `node broadcast-server.js` to run the server. +5. Bring up a second terminal. `env USER=bob ./cmd_websocket_chat` +6. Bring up a third terminal. `env USER=bill ./cmd_websocket_chat` +7. Start typing things in any of those terminals. Hopefully you should see your message being received on the other end. + +## C++ code organization + +Here's a simplistic diagram which explains how the code is structured in term of class/modules. + +``` ++-----------------------+ +| | Start the receiving Background thread. Auto reconnection. Simple websocket Ping. +| IXWebSocket | Interface used by C++ test clients. No IX dependencies. +| | ++-----------------------+ +| | +| IXWebSocketTransport | Low level websocket code, framing, managing raw socket. Adapted from easywsclient. +| | ++-----------------------+ +| | +| IXWebSocket | ws:// Unencrypted Socket handler +| IXWebSocketAppleSSL | wss:// TLS encrypted Socket AppleSSL handler. Used on iOS and macOS +| IXWebSocketOpenSSL | wss:// TLS encrypted Socket OpenSSL handler. Used on Android and Linux +| | Can be used on macOS too. ++-----------------------+ +``` + +## Advanced usage + +### Sending messages + +`websocket:send("foo")` will send a message. + +If the connection was closed and sending failed, the return value will be set to false. + +### ReadyState + +`getReadyState()` returns the state of the connection. There are 4 possible states. + +1. WebSocket_ReadyState_Connecting - The connection is not yet open. +2. WebSocket_ReadyState_Open - The connection is open and ready to communicate. +3. WebSocket_ReadyState_Closing - The connection is in the process of closing. +4. WebSocket_MessageType_Close - The connection is closed or couldn't be opened. + +### Open and Close notifications + +The onMessage event will be fired when the connection is opened or closed. This is similar to the [Javascript browser API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), which has `open` and `close` events notification that can be registered with the browser `addEventListener`. + +``` +webSocket.setOnMessageCallback( + [this](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error) + { + if (messageType == ix::WebSocket_MessageType_Open) + { + puts("send greetings"); + } + else if (messageType == ix::WebSocket_MessageType_Close) + { + puts("disconnected"); + } + } +); +``` + +### Error notification + +A message will be fired when there is an error with the connection. The message type will be `ix::WebSocket_MessageType_Error`. Multiple fields will be available on the event to describe the error. + +``` +webSocket.setOnMessageCallback( + [this](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error) + { + if (messageType == ix::WebSocket_MessageType_Error) + { + ss << "cmd_websocket_chat: Error ! " << error.reason << std::endl; + ss << "#retries: " << event.retries << std::endl; + ss << "Wait time(ms): " << event.waitTime << std::endl; + ss << "HTTP Status: " << event.httpStatus << std::endl; + } + } +); +``` + +### start, stop + +1. `websocket.start()` connect to the remote server and starts the message receiving background thread. +2. `websocket.stop()` disconnect from the remote server and closes the background thread. + +### Configuring the remote url + +The url can be set and queried after a websocket object has been created. You will have to call `stop` and `start` if you want to disconnect and connect to that new url. + +``` +std::string url = 'wss://example.com' +websocket.configure(url); +assert(websocket:getUrl() == url) +``` diff --git a/examples/broadcast-server.js b/examples/broadcast-server.js new file mode 100644 index 00000000..35d98455 --- /dev/null +++ b/examples/broadcast-server.js @@ -0,0 +1,28 @@ +/* + * cmd_websocket_chat.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved. + */ +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); + +// Broadcast to all. +wss.broadcast = function broadcast(data) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); +}; + +wss.on('connection', function connection(ws) { + ws.on('message', function incoming(data) { + // Broadcast to everyone else. + wss.clients.forEach(function each(client) { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); + }); +}); diff --git a/examples/build.sh b/examples/build.sh new file mode 100644 index 00000000..5abed6d2 --- /dev/null +++ b/examples/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Author: Benjamin Sergeant +# Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved. +# + +clang++ --std=c++11 --stdlib=libc++ \ + ../ixwebsocket/IXSocket.cpp \ + ../ixwebsocket/IXWebSocketTransport.cpp \ + ../ixwebsocket/IXSocketAppleSSL.cpp \ + ../ixwebsocket/IXWebSocket.cpp \ + cmd_websocket_chat.cpp \ + -o cmd_websocket_chat \ + -framework Security \ + -framework Foundation diff --git a/examples/cmd_websocket_chat.cpp b/examples/cmd_websocket_chat.cpp new file mode 100644 index 00000000..4289502c --- /dev/null +++ b/examples/cmd_websocket_chat.cpp @@ -0,0 +1,190 @@ +/* + * cmd_websocket_chat.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved. + */ + +// +// Simple chat program that talks to the node.js server at +// websocket_chat_server/broacast-server.js +// + +#include +#include +#include +#include "../ixwebsocket/IXWebSocket.h" + +#include "nlohmann/json.hpp" + +// for convenience +using json = nlohmann::json; + +using namespace ix; + +namespace +{ + void log(const std::string& msg) + { + std::cout << msg << std::endl; + } + + class WebSocketChat + { + public: + WebSocketChat(const std::string& user); + + void subscribe(const std::string& channel); + void start(); + void stop(); + bool isReady() const; + + void sendMessage(const std::string& text); + size_t getReceivedMessagesCount() const; + + std::string encodeMessage(const std::string& text); + std::pair decodeMessage(const std::string& str); + + private: + std::string _user; + + ix::WebSocket _webSocket; + + std::queue _receivedQueue; + }; + + WebSocketChat::WebSocketChat(const std::string& user) : + _user(user) + { + ; + } + + size_t WebSocketChat::getReceivedMessagesCount() const + { + return _receivedQueue.size(); + } + + bool WebSocketChat::isReady() const + { + return _webSocket.getReadyState() == ix::WebSocket_ReadyState_Open; + } + + void WebSocketChat::stop() + { + _webSocket.stop(); + } + + void WebSocketChat::start() + { + std::string url("ws://localhost:8080/"); + _webSocket.configure(url); + + std::stringstream ss; + log(std::string("Connecting to url: ") + url); + + _webSocket.setOnMessageCallback( + [this](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error) + { + std::stringstream ss; + if (messageType == ix::WebSocket_MessageType_Open) + { + ss << "cmd_websocket_chat: user " + << _user + << " Connected !"; + log(ss.str()); + } + else if (messageType == ix::WebSocket_MessageType_Close) + { + ss << "cmd_websocket_chat: user " + << _user + << " disconnected !"; + log(ss.str()); + } + else if (messageType == ix::WebSocket_MessageType_Message) + { + auto result = decodeMessage(str); + + // Our "chat" / "broacast" node.js server does not send us + // the messages we send, so we don't have to filter it out. + + // store text + _receivedQueue.push(result.second); + + ss << std::endl + << result.first << " > " << result.second + << std::endl + << _user << " > "; + log(ss.str()); + } + else if (messageType == ix::WebSocket_MessageType_Error) + { + ss << "cmd_websocket_chat: Error ! " << error.reason; + log(ss.str()); + } + else + { + ss << "Invalid ix::WebSocketMessageType"; + log(ss.str()); + } + }); + + _webSocket.start(); + } + + std::pair WebSocketChat::decodeMessage(const std::string& str) + { + auto j = json::parse(str); + + std::string msg_user = j["user"]; + std::string msg_text = j["text"]; + + return std::pair(msg_user, msg_text); + } + + std::string WebSocketChat::encodeMessage(const std::string& text) + { + json j; + j["user"] = _user; + j["text"] = text; + + std::string output = j.dump(); + return output; + } + + void WebSocketChat::sendMessage(const std::string& text) + { + _webSocket.send(encodeMessage(text)); + } + + void interactiveMain() + { + std::string user(getenv("USER")); + + WebSocketChat webSocketChat(user); + + std::cout << "Type Ctrl-D to exit prompt..." << std::endl; + webSocketChat.start(); + + while (true) + { + std::string text; + std::cout << user << " > " << std::flush; + std::getline(std::cin, text); + + if (!std::cin) + { + break; + } + + webSocketChat.sendMessage(text); + } + + std::cout << std::endl; + webSocketChat.stop(); + } +} + +int main() +{ + interactiveMain(); + return 0; +} diff --git a/examples/nlohmann/json.hpp b/examples/nlohmann/json.hpp new file mode 100644 index 00000000..3d7967bb --- /dev/null +++ b/examples/nlohmann/json.hpp @@ -0,0 +1,18878 @@ +/* + __ _____ _____ _____ + __| | __| | | | JSON for Modern C++ +| | |__ | | | | | | version 3.2.0 +|_____|_____|_____|_|___| https://github.com/nlohmann/json + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2013-2018 Niels Lohmann . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef NLOHMANN_JSON_HPP +#define NLOHMANN_JSON_HPP + +#define NLOHMANN_JSON_VERSION_MAJOR 3 +#define NLOHMANN_JSON_VERSION_MINOR 2 +#define NLOHMANN_JSON_VERSION_PATCH 0 + +#include // all_of, find, for_each +#include // assert +#include // and, not, or +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#include // istream, ostream +#include // iterator_traits, random_access_iterator_tag +#include // accumulate +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap + +// #include +#ifndef NLOHMANN_JSON_FWD_HPP +#define NLOHMANN_JSON_FWD_HPP + +#include // int64_t, uint64_t +#include // map +#include // allocator +#include // string +#include // vector + +/*! +@brief namespace for Niels Lohmann +@see https://github.com/nlohmann +@since version 1.0.0 +*/ +namespace nlohmann +{ +/*! +@brief default JSONSerializer template argument + +This serializer ignores the template arguments and uses ADL +([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) +for serialization. +*/ +template +struct adl_serializer; + +template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer> +class basic_json; + +/*! +@brief JSON Pointer + +A JSON pointer defines a string syntax for identifying a specific value +within a JSON document. It can be used with functions `at` and +`operator[]`. Furthermore, JSON pointers are the base for JSON patches. + +@sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + +@since version 2.0.0 +*/ +template +class json_pointer; + +/*! +@brief default JSON class + +This type is the default specialization of the @ref basic_json class which +uses the standard template types. + +@since version 1.0.0 +*/ +using json = basic_json<>; +} + +#endif + +// #include + + +// This file contains all internal macro definitions +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40900 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdocumentation" +#endif + +// allow for portable deprecation warnings +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #define JSON_DEPRECATED __attribute__((deprecated)) +#elif defined(_MSC_VER) + #define JSON_DEPRECATED __declspec(deprecated) +#else + #define JSON_DEPRECATED +#endif + +// allow to disable exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// manual branch prediction +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #define JSON_LIKELY(x) __builtin_expect(!!(x), 1) + #define JSON_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else + #define JSON_LIKELY(x) x + #define JSON_UNLIKELY(x) x +#endif + +// C++ language standard detection +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 +#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 +#endif + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// #include + + +#include // not +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type + +namespace nlohmann +{ +namespace detail +{ +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +template +using uncvref_t = typename std::remove_cv::type>::type; + +// implementation of C++14 index_sequence and affiliates +// source: https://stackoverflow.com/a/32223343 +template +struct index_sequence +{ + using type = index_sequence; + using value_type = std::size_t; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +template +struct merge_and_renumber; + +template +struct merge_and_renumber, index_sequence> + : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; + +template +struct make_index_sequence + : merge_and_renumber < typename make_index_sequence < N / 2 >::type, + typename make_index_sequence < N - N / 2 >::type > {}; + +template<> struct make_index_sequence<0> : index_sequence<> {}; +template<> struct make_index_sequence<1> : index_sequence<0> {}; + +template +using index_sequence_for = make_index_sequence; + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static constexpr T value{}; +}; + +template +constexpr T static_const::value; +} +} + +// #include + + +#include // not +#include // numeric_limits +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval + +// #include + +// #include + +// #include + + +#include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; +} +} + + +// http://en.cppreference.com/w/cpp/experimental/is_detected +namespace nlohmann +{ +namespace detail +{ +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + void operator=(nonesuch const&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template