2018-12-31 20:04:05 +01:00
|
|
|
//
|
2024-01-01 01:07:21 +01:00
|
|
|
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024
|
2018-12-31 20:04:05 +01:00
|
|
|
//
|
|
|
|
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
|
|
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
//
|
|
|
|
#include "td/utils/port/ServerSocketFd.h"
|
|
|
|
|
|
|
|
#include "td/utils/port/config.h"
|
|
|
|
|
2019-02-12 22:26:36 +01:00
|
|
|
#include "td/utils/common.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/logging.h"
|
2020-06-15 02:50:38 +02:00
|
|
|
#include "td/utils/port/detail/skip_eintr.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/port/IPAddress.h"
|
2018-09-10 03:08:15 +02:00
|
|
|
#include "td/utils/port/PollFlags.h"
|
2021-05-17 14:21:11 +02:00
|
|
|
#include "td/utils/SliceBuilder.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
|
|
|
|
#if TD_PORT_POSIX
|
2020-06-07 17:14:52 +02:00
|
|
|
#include <cerrno>
|
2018-12-31 20:04:05 +01:00
|
|
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <netinet/in.h>
|
|
|
|
#include <netinet/tcp.h>
|
|
|
|
#include <sys/socket.h>
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
#if TD_PORT_WINDOWS
|
2018-09-11 16:43:43 +02:00
|
|
|
#include "td/utils/port/detail/Iocp.h"
|
2022-07-14 14:27:06 +02:00
|
|
|
#include "td/utils/port/Mutex.h"
|
2018-08-13 19:15:09 +02:00
|
|
|
#include "td/utils/VectorQueue.h"
|
|
|
|
#endif
|
|
|
|
|
2018-09-10 03:08:15 +02:00
|
|
|
#include <atomic>
|
2018-09-07 02:41:21 +02:00
|
|
|
#include <cstring>
|
|
|
|
|
2018-12-31 20:04:05 +01:00
|
|
|
namespace td {
|
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
namespace detail {
|
|
|
|
#if TD_PORT_WINDOWS
|
2021-07-04 04:58:54 +02:00
|
|
|
class ServerSocketFdImpl final : private Iocp::Callback {
|
2018-08-13 19:15:09 +02:00
|
|
|
public:
|
2018-09-07 02:41:21 +02:00
|
|
|
ServerSocketFdImpl(NativeFd fd, int socket_family) : info_(std::move(fd)), socket_family_(socket_family) {
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " create ServerSocketFd";
|
2018-09-11 16:43:43 +02:00
|
|
|
Iocp::get()->subscribe(get_native_fd(), this);
|
2018-08-13 19:15:09 +02:00
|
|
|
notify_iocp_read();
|
|
|
|
}
|
|
|
|
void close() {
|
|
|
|
notify_iocp_close();
|
|
|
|
}
|
|
|
|
PollableFdInfo &get_poll_info() {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
const PollableFdInfo &get_poll_info() const {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
const NativeFd &get_native_fd() const {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_.native_fd();
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
Result<SocketFd> accept() {
|
|
|
|
auto lock = lock_.lock();
|
|
|
|
if (accepted_.empty()) {
|
|
|
|
get_poll_info().clear_flags(PollFlags::Read());
|
|
|
|
return Status::Error(-1, "Operation would block");
|
|
|
|
}
|
|
|
|
return accepted_.pop();
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
Status get_pending_error() {
|
|
|
|
Status res;
|
|
|
|
{
|
|
|
|
auto lock = lock_.lock();
|
|
|
|
if (!pending_errors_.empty()) {
|
|
|
|
res = pending_errors_.pop();
|
|
|
|
}
|
|
|
|
if (res.is_ok()) {
|
|
|
|
get_poll_info().clear_flags(PollFlags::Error());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
private:
|
2018-09-07 02:41:21 +02:00
|
|
|
PollableFdInfo info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
|
2022-07-14 14:27:06 +02:00
|
|
|
Mutex lock_;
|
2018-08-13 19:15:09 +02:00
|
|
|
VectorQueue<SocketFd> accepted_;
|
|
|
|
VectorQueue<Status> pending_errors_;
|
|
|
|
static constexpr size_t MAX_ADDR_SIZE = sizeof(sockaddr_in6) + 16;
|
|
|
|
char addr_buf_[MAX_ADDR_SIZE * 2];
|
|
|
|
|
|
|
|
bool close_flag_{false};
|
|
|
|
std::atomic<int> refcnt_{1};
|
|
|
|
bool is_read_active_{false};
|
2018-09-10 19:49:54 +02:00
|
|
|
WSAOVERLAPPED read_overlapped_;
|
2018-08-13 19:15:09 +02:00
|
|
|
|
|
|
|
char close_overlapped_;
|
|
|
|
|
|
|
|
NativeFd accept_socket_;
|
|
|
|
int socket_family_;
|
|
|
|
|
|
|
|
void on_close() {
|
|
|
|
close_flag_ = true;
|
2018-09-07 02:41:21 +02:00
|
|
|
info_.set_native_fd({});
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
void on_read() {
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " on_read";
|
2018-08-13 19:15:09 +02:00
|
|
|
if (is_read_active_) {
|
|
|
|
is_read_active_ = false;
|
2019-07-06 13:29:15 +02:00
|
|
|
auto r_socket = [&]() -> Result<SocketFd> {
|
|
|
|
auto from = get_native_fd().socket();
|
|
|
|
auto status = setsockopt(accept_socket_.socket(), SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
|
|
|
|
reinterpret_cast<const char *>(&from), sizeof(from));
|
|
|
|
if (status != 0) {
|
|
|
|
return OS_SOCKET_ERROR("Failed to set SO_UPDATE_ACCEPT_CONTEXT options");
|
|
|
|
}
|
|
|
|
return SocketFd::from_native_fd(std::move(accept_socket_));
|
|
|
|
}();
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " finish accept";
|
2018-08-13 19:15:09 +02:00
|
|
|
if (r_socket.is_error()) {
|
|
|
|
return on_error(r_socket.move_as_error());
|
|
|
|
}
|
|
|
|
{
|
|
|
|
auto lock = lock_.lock();
|
|
|
|
accepted_.push(r_socket.move_as_ok());
|
|
|
|
}
|
|
|
|
get_poll_info().add_flags_from_poll(PollFlags::Read());
|
|
|
|
}
|
|
|
|
loop_read();
|
|
|
|
}
|
|
|
|
void loop_read() {
|
|
|
|
CHECK(!is_read_active_);
|
|
|
|
accept_socket_ = NativeFd(socket(socket_family_, SOCK_STREAM, 0));
|
|
|
|
std::memset(&read_overlapped_, 0, sizeof(read_overlapped_));
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " start accept";
|
2018-09-10 19:49:54 +02:00
|
|
|
BOOL status = AcceptEx(get_native_fd().socket(), accept_socket_.socket(), addr_buf_, 0, MAX_ADDR_SIZE,
|
2018-08-13 19:15:09 +02:00
|
|
|
MAX_ADDR_SIZE, nullptr, &read_overlapped_);
|
2018-09-10 19:49:54 +02:00
|
|
|
if (status == TRUE || check_status("Failed to accept connection")) {
|
2018-08-13 19:15:09 +02:00
|
|
|
inc_refcnt();
|
|
|
|
is_read_active_ = true;
|
|
|
|
}
|
|
|
|
}
|
2018-09-10 19:49:54 +02:00
|
|
|
bool check_status(Slice message) {
|
2018-09-07 02:41:21 +02:00
|
|
|
auto last_error = WSAGetLastError();
|
2018-08-13 19:15:09 +02:00
|
|
|
if (last_error == ERROR_IO_PENDING) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
on_error(OS_SOCKET_ERROR(message));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
bool dec_refcnt() {
|
|
|
|
if (--refcnt_ == 0) {
|
|
|
|
delete this;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
void inc_refcnt() {
|
|
|
|
CHECK(refcnt_ != 0);
|
|
|
|
refcnt_++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void on_error(Status status) {
|
|
|
|
{
|
|
|
|
auto lock = lock_.lock();
|
|
|
|
pending_errors_.push(std::move(status));
|
|
|
|
}
|
|
|
|
get_poll_info().add_flags_from_poll(PollFlags::Error());
|
|
|
|
}
|
|
|
|
|
2021-07-03 22:51:36 +02:00
|
|
|
void on_iocp(Result<size_t> r_size, WSAOVERLAPPED *overlapped) final {
|
2018-08-13 19:15:09 +02:00
|
|
|
// called from other thread
|
|
|
|
if (dec_refcnt() || close_flag_) {
|
2018-12-17 17:12:47 +01:00
|
|
|
VLOG(fd) << "Ignore IOCP (server socket is closing)";
|
2018-08-13 19:15:09 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (r_size.is_error()) {
|
2018-12-17 17:12:47 +01:00
|
|
|
return on_error(get_socket_pending_error(get_native_fd(), overlapped, r_size.move_as_error()));
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (overlapped == nullptr) {
|
|
|
|
return on_read();
|
|
|
|
}
|
|
|
|
if (overlapped == &read_overlapped_) {
|
|
|
|
return on_read();
|
|
|
|
}
|
|
|
|
|
2018-09-10 19:49:54 +02:00
|
|
|
if (overlapped == reinterpret_cast<WSAOVERLAPPED *>(&close_overlapped_)) {
|
2018-08-13 19:15:09 +02:00
|
|
|
return on_close();
|
|
|
|
}
|
|
|
|
UNREACHABLE();
|
|
|
|
}
|
|
|
|
void notify_iocp_read() {
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " notify_read";
|
2018-08-13 19:15:09 +02:00
|
|
|
inc_refcnt();
|
2018-09-11 16:43:43 +02:00
|
|
|
Iocp::get()->post(0, this, nullptr);
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
void notify_iocp_close() {
|
2018-09-11 18:44:37 +02:00
|
|
|
VLOG(fd) << get_native_fd() << " notify_close";
|
2018-09-11 16:43:43 +02:00
|
|
|
Iocp::get()->post(0, this, reinterpret_cast<WSAOVERLAPPED *>(&close_overlapped_));
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
void ServerSocketFdImplDeleter::operator()(ServerSocketFdImpl *impl) {
|
|
|
|
impl->close();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2018-08-13 19:15:09 +02:00
|
|
|
#elif TD_PORT_POSIX
|
|
|
|
class ServerSocketFdImpl {
|
|
|
|
public:
|
2018-09-07 02:41:21 +02:00
|
|
|
explicit ServerSocketFdImpl(NativeFd fd) : info_(std::move(fd)) {
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
PollableFdInfo &get_poll_info() {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
const PollableFdInfo &get_poll_info() const {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
const NativeFd &get_native_fd() const {
|
2018-09-07 02:41:21 +02:00
|
|
|
return info_.native_fd();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2018-08-13 19:15:09 +02:00
|
|
|
Result<SocketFd> accept() {
|
|
|
|
sockaddr_storage addr;
|
|
|
|
socklen_t addr_len = sizeof(addr);
|
2018-09-11 15:27:20 +02:00
|
|
|
int native_fd = get_native_fd().socket();
|
2018-09-10 01:16:42 +02:00
|
|
|
int r_fd = detail::skip_eintr([&] { return ::accept(native_fd, reinterpret_cast<sockaddr *>(&addr), &addr_len); });
|
2018-08-13 19:15:09 +02:00
|
|
|
auto accept_errno = errno;
|
|
|
|
if (r_fd >= 0) {
|
|
|
|
return SocketFd::from_native_fd(NativeFd(r_fd));
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
if (accept_errno == EAGAIN
|
2018-12-31 20:04:05 +01:00
|
|
|
#if EAGAIN != EWOULDBLOCK
|
2018-08-13 19:15:09 +02:00
|
|
|
|| accept_errno == EWOULDBLOCK
|
2018-12-31 20:04:05 +01:00
|
|
|
#endif
|
2018-08-13 19:15:09 +02:00
|
|
|
) {
|
|
|
|
get_poll_info().clear_flags(PollFlags::Read());
|
|
|
|
return Status::Error(-1, "Operation would block");
|
|
|
|
}
|
|
|
|
|
2018-10-07 01:47:50 +02:00
|
|
|
auto error = Status::PosixError(accept_errno, PSLICE() << "Accept from " << get_native_fd() << " has failed");
|
2018-08-13 19:15:09 +02:00
|
|
|
switch (accept_errno) {
|
|
|
|
case EBADF:
|
|
|
|
case EFAULT:
|
|
|
|
case EINVAL:
|
|
|
|
case ENOTSOCK:
|
|
|
|
case EOPNOTSUPP:
|
|
|
|
LOG(FATAL) << error;
|
|
|
|
UNREACHABLE();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
LOG(ERROR) << error;
|
|
|
|
// fallthrough
|
|
|
|
case EMFILE:
|
|
|
|
case ENFILE:
|
|
|
|
case ECONNABORTED: //???
|
|
|
|
get_poll_info().clear_flags(PollFlags::Read());
|
|
|
|
get_poll_info().add_flags(PollFlags::Close());
|
|
|
|
return std::move(error);
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2018-08-13 19:15:09 +02:00
|
|
|
|
|
|
|
Status get_pending_error() {
|
2020-07-21 17:29:39 +02:00
|
|
|
if (!get_poll_info().get_flags_local().has_pending_error()) {
|
2018-08-13 19:15:09 +02:00
|
|
|
return Status::OK();
|
|
|
|
}
|
|
|
|
TRY_STATUS(detail::get_socket_pending_error(get_native_fd()));
|
|
|
|
get_poll_info().clear_flags(PollFlags::Error());
|
|
|
|
return Status::OK();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2018-09-07 02:41:21 +02:00
|
|
|
PollableFdInfo info_;
|
2018-08-13 19:15:09 +02:00
|
|
|
};
|
|
|
|
void ServerSocketFdImplDeleter::operator()(ServerSocketFdImpl *impl) {
|
|
|
|
delete impl;
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
#endif
|
2018-08-13 19:15:09 +02:00
|
|
|
} // namespace detail
|
|
|
|
|
|
|
|
ServerSocketFd::ServerSocketFd() = default;
|
2021-10-18 13:36:15 +02:00
|
|
|
ServerSocketFd::ServerSocketFd(ServerSocketFd &&) noexcept = default;
|
|
|
|
ServerSocketFd &ServerSocketFd::operator=(ServerSocketFd &&) noexcept = default;
|
2018-08-13 19:15:09 +02:00
|
|
|
ServerSocketFd::~ServerSocketFd() = default;
|
2018-09-27 03:19:03 +02:00
|
|
|
ServerSocketFd::ServerSocketFd(unique_ptr<detail::ServerSocketFdImpl> impl) : impl_(impl.release()) {
|
2018-08-13 19:15:09 +02:00
|
|
|
}
|
|
|
|
PollableFdInfo &ServerSocketFd::get_poll_info() {
|
|
|
|
return impl_->get_poll_info();
|
|
|
|
}
|
|
|
|
|
|
|
|
const PollableFdInfo &ServerSocketFd::get_poll_info() const {
|
|
|
|
return impl_->get_poll_info();
|
|
|
|
}
|
|
|
|
|
|
|
|
Status ServerSocketFd::get_pending_error() {
|
|
|
|
return impl_->get_pending_error();
|
|
|
|
}
|
|
|
|
|
|
|
|
const NativeFd &ServerSocketFd::get_native_fd() const {
|
|
|
|
return impl_->get_native_fd();
|
|
|
|
}
|
|
|
|
|
|
|
|
Result<SocketFd> ServerSocketFd::accept() {
|
|
|
|
return impl_->accept();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void ServerSocketFd::close() {
|
2018-08-13 19:15:09 +02:00
|
|
|
impl_.reset();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ServerSocketFd::empty() const {
|
2018-08-13 19:15:09 +02:00
|
|
|
return !impl_;
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
Result<ServerSocketFd> ServerSocketFd::open(int32 port, CSlice addr) {
|
2020-11-09 22:31:56 +01:00
|
|
|
if (port <= 0 || port >= (1 << 16)) {
|
|
|
|
return Status::Error(PSLICE() << "Invalid server port " << port << " specified");
|
|
|
|
}
|
|
|
|
|
|
|
|
TRY_RESULT(address, IPAddress::get_ip_address(addr));
|
|
|
|
address.set_port(port);
|
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
NativeFd fd{socket(address.get_address_family(), SOCK_STREAM, 0)};
|
|
|
|
if (!fd) {
|
2018-12-31 20:04:05 +01:00
|
|
|
return OS_SOCKET_ERROR("Failed to create a socket");
|
|
|
|
}
|
|
|
|
|
2018-09-10 16:05:12 +02:00
|
|
|
TRY_STATUS(fd.set_is_blocking_unsafe(false));
|
2018-08-13 19:15:09 +02:00
|
|
|
auto sock = fd.socket();
|
2018-12-31 20:04:05 +01:00
|
|
|
|
|
|
|
linger ling = {0, 0};
|
|
|
|
#if TD_PORT_POSIX
|
|
|
|
int flags = 1;
|
|
|
|
#ifdef SO_REUSEPORT
|
2018-08-13 19:15:09 +02:00
|
|
|
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char *>(&flags), sizeof(flags));
|
2018-12-31 20:04:05 +01:00
|
|
|
#endif
|
|
|
|
#elif TD_PORT_WINDOWS
|
2020-11-10 21:08:54 +01:00
|
|
|
BOOL flags = FALSE;
|
|
|
|
if (address.is_ipv6()) {
|
|
|
|
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&flags), sizeof(flags));
|
|
|
|
}
|
|
|
|
flags = TRUE;
|
2018-12-31 20:04:05 +01:00
|
|
|
#endif
|
2018-08-13 19:15:09 +02:00
|
|
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&flags), sizeof(flags));
|
|
|
|
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast<const char *>(&flags), sizeof(flags));
|
|
|
|
setsockopt(sock, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char *>(&ling), sizeof(ling));
|
|
|
|
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char *>(&flags), sizeof(flags));
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
int e_bind = bind(sock, address.get_sockaddr(), static_cast<socklen_t>(address.get_sockaddr_len()));
|
2018-12-31 20:04:05 +01:00
|
|
|
if (e_bind != 0) {
|
|
|
|
return OS_SOCKET_ERROR("Failed to bind a socket");
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: magic constant
|
2018-08-13 19:15:09 +02:00
|
|
|
int e_listen = listen(sock, 8192);
|
2018-12-31 20:04:05 +01:00
|
|
|
if (e_listen != 0) {
|
|
|
|
return OS_SOCKET_ERROR("Failed to listen on a socket");
|
|
|
|
}
|
|
|
|
|
|
|
|
#if TD_PORT_POSIX
|
2018-09-27 03:19:03 +02:00
|
|
|
auto impl = make_unique<detail::ServerSocketFdImpl>(std::move(fd));
|
2018-12-31 20:04:05 +01:00
|
|
|
#elif TD_PORT_WINDOWS
|
2018-09-27 03:19:03 +02:00
|
|
|
auto impl = make_unique<detail::ServerSocketFdImpl>(std::move(fd), address.get_address_family());
|
2018-12-31 20:04:05 +01:00
|
|
|
#endif
|
|
|
|
|
2018-08-13 19:15:09 +02:00
|
|
|
return ServerSocketFd(std::move(impl));
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace td
|