/////////////////////////////////////////////////////////// /// Copyright 2019 Alexy Pellegrini /// /// 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 NOT_ENOUGH_STANDARDS_PROCESS #define NOT_ENOUGH_STANDARDS_PROCESS #if defined(_WIN32) #define NES_WIN32_PROCESS #ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #elif defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)) #define NES_POSIX_PROCESS #include #include #include #include #include #else #error "Not enough standards does not support this environment." #endif #if __has_include("pipe.hpp") #define NES_PROCESS_PIPE_EXTENSION #include "pipe.hpp" #endif #include #include #include #include #include #include #include #include #if defined(NES_WIN32_PROCESS) namespace nes { class process; namespace impl { enum class id_t : DWORD{}; constexpr bool operator==(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) == static_cast(rhs); } constexpr bool operator!=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) != static_cast(rhs); } constexpr bool operator<(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) < static_cast(rhs); } constexpr bool operator<=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) <= static_cast(rhs); } constexpr bool operator>(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) > static_cast(rhs); } constexpr bool operator>=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) >= static_cast(rhs); } struct auto_handle { public: constexpr auto_handle() = default; auto_handle(HANDLE h) :m_handle{h} { } ~auto_handle() { if(m_handle != INVALID_HANDLE_VALUE) CloseHandle(m_handle); } auto_handle(const auto_handle&) = delete; auto_handle& operator=(const auto_handle&) = delete; auto_handle(auto_handle&& other) noexcept :m_handle{std::exchange(other.m_handle, INVALID_HANDLE_VALUE)} { } auto_handle& operator=(auto_handle&& other) noexcept { m_handle = std::exchange(other.m_handle, m_handle); return *this; } HANDLE release() noexcept { return std::exchange(m_handle, INVALID_HANDLE_VALUE); } operator HANDLE() const noexcept { return m_handle; } HANDLE* operator&() noexcept { return &m_handle; } const HANDLE* operator&() const noexcept { return &m_handle; } operator bool() const noexcept { return m_handle != INVALID_HANDLE_VALUE; } private: HANDLE m_handle{INVALID_HANDLE_VALUE}; }; } enum class process_options : std::uint32_t { none = 0x00, #ifdef NES_PROCESS_PIPE_EXTENSION grab_stdout = 0x10, grab_stderr = 0x20, grab_stdin = 0x40 #endif }; constexpr process_options operator&(process_options left, process_options right) noexcept { return static_cast(static_cast(left) & static_cast(right)); } constexpr process_options& operator&=(process_options& left, process_options right) noexcept { left = left & right; return left; } constexpr process_options operator|(process_options left, process_options right) noexcept { return static_cast(static_cast(left) | static_cast(right)); } constexpr process_options& operator|=(process_options& left, process_options right) noexcept { left = left | right; return left; } constexpr process_options operator^(process_options left, process_options right) noexcept { return static_cast(static_cast(left) ^ static_cast(right)); } constexpr process_options& operator^=(process_options& left, process_options right) noexcept { left = left ^ right; return left; } constexpr process_options operator~(process_options value) noexcept { return static_cast(~static_cast(value)); } class process { public: using native_handle_type = HANDLE; using return_code_type = DWORD; using id = impl::id_t; public: constexpr process() noexcept = default; explicit process(const std::string& path, const std::string& working_directory) :process{path, {}, working_directory, {}}{} explicit process(const std::string& path, process_options options) :process{path, {}, {}, options}{} explicit process(const std::string& path, const std::vector& args, process_options options) :process{path, args, {}, options}{} explicit process(const std::string& path, const std::string& working_directory, process_options options) :process{path, {}, working_directory, options}{} explicit process(const std::string& path, std::vector args = std::vector{}, const std::string& working_directory = std::string{}, process_options options [[maybe_unused]] = process_options{}) { assert(!std::empty(path) && "nes::process::process called with empty path."); SECURITY_ATTRIBUTES security_attributes{}; security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES); security_attributes.bInheritHandle = TRUE; security_attributes.lpSecurityDescriptor = nullptr; #ifdef NES_PROCESS_PIPE_EXTENSION impl::auto_handle stdin_rd{}; impl::auto_handle stdout_rd{}; impl::auto_handle stderr_rd{}; impl::auto_handle stdin_wr{}; impl::auto_handle stdout_wr{}; impl::auto_handle stderr_wr{}; if(static_cast(options & process_options::grab_stdin)) if(!CreatePipe(&stdin_rd, &stdin_wr, &security_attributes, 0) || !SetHandleInformation(stdin_wr, HANDLE_FLAG_INHERIT, 0)) throw std::runtime_error{"Failed to create stdin pipe. " + get_error_message()}; if(static_cast(options & process_options::grab_stdout)) if(!CreatePipe(&stdout_rd, &stdout_wr, &security_attributes, 0) || !SetHandleInformation(stdout_rd, HANDLE_FLAG_INHERIT, 0)) throw std::runtime_error{"Failed to create stdout pipe. " + get_error_message()}; if(static_cast(options & process_options::grab_stderr)) if(!CreatePipe(&stderr_rd, &stderr_wr, &security_attributes, 0) || !SetHandleInformation(stderr_rd, HANDLE_FLAG_INHERIT, 0)) throw std::runtime_error{"Failed to create stderr pipe. " + get_error_message()}; #endif args.insert(std::begin(args), path); auto format_arg = [](const std::wstring& arg) -> std::wstring { if(arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) return arg; std::wstring out{L"\""}; for(auto it = std::cbegin(arg); it != std::cend(arg); ++it) { if(*it == L'\\') { std::size_t count{1}; while(++it != std::cend(arg) && *it == L'\\') ++count; if(it == std::cend(arg)) { out.append(count * 2, L'\\'); break; } else if(*it == L'\"') { out.append(count * 2 + 1, L'\\'); out.push_back(L'\"'); } else { out.append(count, L'\\'); out.push_back(*it); } } else if(*it == L'\"') { out.push_back(L'\\'); out.push_back(*it); } else { out.push_back(*it); } } out.push_back(L'\"'); return out; }; std::wstring args_str{}; for(auto&& arg : args) args_str += format_arg(to_wide(arg)) + L" "; const std::wstring native_working_directory{to_wide(working_directory)}; const std::wstring native_path{to_wide(path)}; STARTUPINFOW startup_info{}; startup_info.cb = sizeof(STARTUPINFOW); #ifdef NES_PROCESS_PIPE_EXTENSION startup_info.hStdInput = stdin_rd; startup_info.hStdOutput = stdout_wr; startup_info.hStdError = stderr_wr; if(static_cast(options) != 0) startup_info.dwFlags = STARTF_USESTDHANDLES; #endif PROCESS_INFORMATION process_info{}; if(!CreateProcessW(std::data(native_path), null_or_data(args_str), nullptr, nullptr, TRUE, 0, nullptr, null_or_data(native_working_directory), &startup_info, &process_info)) throw std::runtime_error{"Failed to create process. " + get_error_message()}; m_id = static_cast(process_info.dwProcessId); m_handle = process_info.hProcess; m_thread_handle = process_info.hThread; #ifdef NES_PROCESS_PIPE_EXTENSION if(static_cast(options & process_options::grab_stdin)) { pipe_streambuf buffer{stdin_wr.release(), std::ios_base::out}; m_stdin_stream.reset(new pipe_ostream{std::move(buffer)}); } if(static_cast(options & process_options::grab_stdout)) { pipe_streambuf buffer{stdout_rd.release(), std::ios_base::in}; m_stdout_stream.reset(new pipe_istream{std::move(buffer)}); } if(static_cast(options & process_options::grab_stderr)) { pipe_streambuf buffer{stderr_rd.release(), std::ios_base::in}; m_stderr_stream.reset(new pipe_istream{std::move(buffer)}); } #endif } ~process() { assert(!joinable() && "nes::process::~process() called with joinable() returning true."); if(joinable()) std::terminate(); } process(const process&) = delete; process& operator=(const process&) = delete; process(process&& other) noexcept :m_id{std::exchange(other.m_id, id{})} ,m_return_code{std::exchange(other.m_return_code, return_code_type{})} ,m_handle{std::move(other.m_handle)} ,m_thread_handle{std::move(other.m_thread_handle)} #ifdef NES_PROCESS_PIPE_EXTENSION ,m_stdin_stream{std::move(other.m_stdin_stream)} ,m_stdout_stream{std::move(other.m_stdout_stream)} ,m_stderr_stream{std::move(other.m_stderr_stream)} #endif { } process& operator=(process&& other) noexcept { if(joinable()) std::terminate(); m_id = std::exchange(other.m_id, m_id); m_return_code = std::exchange(other.m_return_code, m_return_code); m_handle = std::move(other.m_handle); m_thread_handle = std::move(other.m_thread_handle); #ifdef NES_PROCESS_PIPE_EXTENSION m_stdin_stream = std::move(other.m_stdin_stream); m_stdout_stream = std::move(other.m_stdout_stream); m_stderr_stream = std::move(other.m_stderr_stream); #endif return *this; } void join() { assert(joinable() && "nes::process::join() called with joinable() returning false."); if(WaitForSingleObject(m_handle, INFINITE)) throw std::runtime_error{"Failed to join the process. " + get_error_message()}; if(!GetExitCodeProcess(m_handle, reinterpret_cast(&m_return_code))) throw std::runtime_error{"Failed to get the return code of the process. " + get_error_message()}; close_process(); } bool joinable() const noexcept { return m_handle; } bool active() const { if(!m_handle) return false; DWORD result = WaitForSingleObject(m_handle, 0); if(result == WAIT_FAILED) throw std::runtime_error{"Failed to get the state of the process. " + get_error_message()}; return result == WAIT_TIMEOUT; } void detach() { assert(joinable() && "nes::process::detach() called with joinable() returning false."); close_process(); } bool kill() { assert(joinable() && "nes::process::kill() called with joinable() returning false."); if(!TerminateProcess(m_handle, 1)) return false; join(); return true; } return_code_type return_code() const noexcept { assert(!joinable() && "nes::process::return_code() called with joinable() returning true."); return m_return_code; } native_handle_type native_handle() const noexcept { return m_handle; } id get_id() const noexcept { return m_id; } #ifdef NES_PROCESS_PIPE_EXTENSION pipe_ostream& stdin_stream() noexcept { return *m_stdin_stream; } pipe_istream& stdout_stream() noexcept { return *m_stdout_stream; } pipe_istream& stderr_stream() noexcept { return *m_stderr_stream; } #endif private: std::wstring to_wide(const std::string& path) { assert(std::size(path) < 0x7FFFFFFFu && "Wrong path."); if(std::empty(path)) return {}; std::wstring out_path{}; out_path.resize(static_cast(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, std::data(path), static_cast(std::size(path)), nullptr, 0))); if(!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, std::data(path), static_cast(std::size(path)), std::data(out_path), static_cast(std::size(out_path)))) throw std::runtime_error{"Failed to convert the path to wide."}; return out_path; } std::string get_error_message() const { return "#" + std::to_string(GetLastError()); } void close_process() { m_id = id{}; CloseHandle(m_handle.release()); CloseHandle(m_thread_handle.release()); } wchar_t* null_or_data(std::wstring& str) { return std::empty(str) ? nullptr : std::data(str); } const wchar_t* null_or_data(const std::wstring& str) { return std::empty(str) ? nullptr : std::data(str); } private: id m_id{}; return_code_type m_return_code{}; impl::auto_handle m_handle{}; impl::auto_handle m_thread_handle{}; #ifdef NES_PROCESS_PIPE_EXTENSION std::unique_ptr m_stdin_stream{}; std::unique_ptr m_stdout_stream{}; std::unique_ptr m_stderr_stream{}; #endif }; namespace this_process { inline process::id get_id() noexcept { return process::id{GetCurrentProcessId()}; } inline std::string working_directory() { const DWORD size{GetCurrentDirectoryW(0, nullptr)}; std::wstring native_path{}; native_path.resize(static_cast(size)); GetCurrentDirectoryW(size, std::data(native_path)); native_path.pop_back(); //Because GetCurrentDirectoryW adds a null terminator std::transform(std::begin(native_path), std::end(native_path), std::begin(native_path), [](wchar_t c){return c == L'\\' ? L'/' : c;}); std::string path{}; path.resize(static_cast(WideCharToMultiByte(CP_UTF8, 0, std::data(native_path), static_cast(std::size(native_path)), nullptr, 0, nullptr, nullptr))); if(!WideCharToMultiByte(CP_UTF8, 0, std::data(native_path), static_cast(std::size(native_path)), std::data(path), static_cast(std::size(path)), nullptr, nullptr)) throw std::runtime_error{"Failed to convert the path to UTF-8."}; return path; } inline bool change_working_directory(const std::string& path) { std::wstring native_path{}; native_path.resize(static_cast(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, std::data(path), static_cast(std::size(path)), nullptr, 0))); if(!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, std::data(path), static_cast(std::size(path)), std::data(native_path), static_cast(std::size(native_path)))) throw std::runtime_error{"Failed to convert the path to wide."}; return SetCurrentDirectoryW(std::data(native_path)); } } } template std::basic_ostream& operator<<(std::basic_ostream& os, nes::process::id id) { return os << static_cast(id); } namespace std { template<> struct hash { using argument_type = nes::process::id; using result_type = std::size_t; result_type operator()(const argument_type& s) const noexcept { return std::hash{}(static_cast(s)); } }; } #elif defined(NES_POSIX_PROCESS) namespace nes { class process; namespace impl { enum class id_t : pid_t{}; constexpr bool operator==(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) == static_cast(rhs); } constexpr bool operator!=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) != static_cast(rhs); } constexpr bool operator<(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) < static_cast(rhs); } constexpr bool operator<=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) <= static_cast(rhs); } constexpr bool operator>(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) > static_cast(rhs); } constexpr bool operator>=(id_t lhs, id_t rhs) noexcept { return static_cast(lhs) >= static_cast(rhs); } #ifdef NES_PROCESS_PIPE_EXTENSION struct auto_handle { constexpr auto_handle() = default; auto_handle(int h) :m_handle{h} { } ~auto_handle() { if(m_handle) close(m_handle); } auto_handle(const auto_handle&) = delete; auto_handle& operator=(const auto_handle&) = delete; auto_handle(auto_handle&& other) noexcept :m_handle{std::exchange(other.m_handle, -1)} { } auto_handle& operator=(auto_handle&& other) noexcept { m_handle = std::exchange(other.m_handle, m_handle); return *this; } int release() noexcept { return std::exchange(m_handle, -1); } operator int() noexcept { return m_handle; } operator int() const noexcept { return m_handle; } int* operator&() noexcept { return &m_handle; } const int* operator&() const noexcept { return &m_handle; } operator bool() const noexcept { return m_handle != -1; } int m_handle{-1}; }; #endif } enum class process_options : std::uint32_t { none = 0x00, #ifdef NES_PROCESS_PIPE_EXTENSION grab_stdout = 0x10, grab_stderr = 0x20, grab_stdin = 0x40 #endif }; constexpr process_options operator&(process_options left, process_options right) noexcept { return static_cast(static_cast(left) & static_cast(right)); } constexpr process_options& operator&=(process_options& left, process_options right) noexcept { left = left & right; return left; } constexpr process_options operator|(process_options left, process_options right) noexcept { return static_cast(static_cast(left) | static_cast(right)); } constexpr process_options& operator|=(process_options& left, process_options right) noexcept { left = left | right; return left; } constexpr process_options operator^(process_options left, process_options right) noexcept { return static_cast(static_cast(left) ^ static_cast(right)); } constexpr process_options& operator^=(process_options& left, process_options right) noexcept { left = left ^ right; return left; } constexpr process_options operator~(process_options value) noexcept { return static_cast(~static_cast(value)); } class process { public: using native_handle_type = pid_t; using return_code_type = int; using id = impl::id_t; public: constexpr process() noexcept = default; explicit process(const std::string& path, const std::string& working_directory) :process{path, {}, working_directory, {}}{} explicit process(const std::string& path, process_options options) :process{path, {}, {}, options}{} explicit process(const std::string& path, const std::vector& args, process_options options) :process{path, args, {}, options}{} explicit process(const std::string& path, const std::string& working_directory, process_options options) :process{path, {}, working_directory, options}{} explicit process(const std::string& path, std::vector args = std::vector{}, const std::string& working_directory = std::string{}, process_options options [[maybe_unused]] = process_options{}) { assert(!std::empty(path) && "nes::process::process called with empty path."); args.insert(std::begin(args), path); std::vector native_args{}; native_args.resize(std::size(args) + 1); for(std::size_t i{}; i < std::size(args); ++i) native_args[i] = std::data(args[i]); #ifdef NES_PROCESS_PIPE_EXTENSION impl::auto_handle stdin_fd[2]{}; impl::auto_handle stdout_fd[2]{}; impl::auto_handle stderr_fd[2]{}; if(static_cast(options & process_options::grab_stdin) && pipe(reinterpret_cast(stdin_fd))) throw std::runtime_error{"Failed to create stdin pipe. " + std::string{strerror(errno)}}; if(static_cast(options & process_options::grab_stdout) && pipe(reinterpret_cast(stdout_fd))) throw std::runtime_error{"Failed to create stdout pipe. " + std::string{strerror(errno)}}; if(static_cast(options & process_options::grab_stderr) && pipe(reinterpret_cast(stderr_fd))) throw std::runtime_error{"Failed to create stderr pipe. " + std::string{strerror(errno)}}; const bool standard_streams{static_cast(options & process_options::grab_stdin) || static_cast(options & process_options::grab_stdout) || static_cast(options & process_options::grab_stderr)}; #else constexpr bool standard_streams{false}; #endif const pid_t id{fork()}; if(id < 0) { throw std::runtime_error{"Failed to create process. " + std::string{strerror(errno)}}; } else if(id == 0) { #ifdef NES_PROCESS_PIPE_EXTENSION if(static_cast(options & process_options::grab_stdin)) if(dup2(stdin_fd[0], 0) == -1) _exit(EXIT_FAILURE); if(static_cast(options & process_options::grab_stdout)) if(dup2(stdout_fd[1], 1) == -1) _exit(EXIT_FAILURE); if(static_cast(options & process_options::grab_stderr)) if(dup2(stderr_fd[1], 2) == -1) _exit(EXIT_FAILURE); #endif if(!std::empty(working_directory)) if(chdir(std::data(working_directory))) _exit(EXIT_FAILURE); execv(std::data(path), std::data(native_args)); _exit(EXIT_FAILURE); } m_id = id; #ifdef NES_PROCESS_PIPE_EXTENSION if(static_cast(options & process_options::grab_stdin)) m_stdin_stream.reset(new pipe_ostream{pipe_streambuf{stdin_fd[1].release(), std::ios_base::out}}); if(static_cast(options & process_options::grab_stdout)) m_stdout_stream.reset(new pipe_istream{pipe_streambuf{stdout_fd[0].release(), std::ios_base::in}}); if(static_cast(options & process_options::grab_stderr)) m_stderr_stream.reset(new pipe_istream{pipe_streambuf{stderr_fd[0].release(), std::ios_base::in}}); #endif } ~process() { assert(!joinable() && "nes::process::~process() called with joinable() returning true."); if(joinable()) std::terminate(); } process(const process&) = delete; process& operator=(const process&) = delete; process(process&& other) noexcept :m_id{std::exchange(other.m_id, -1)} ,m_return_code{std::exchange(other.m_return_code, return_code_type{})} #ifdef NES_PROCESS_PIPE_EXTENSION ,m_stdin_stream{std::move(other.m_stdin_stream)} ,m_stdout_stream{std::move(other.m_stdout_stream)} ,m_stderr_stream{std::move(other.m_stderr_stream)} #endif { } process& operator=(process&& other) noexcept { if(joinable()) std::terminate(); m_id = std::exchange(other.m_id, m_id); m_return_code = std::exchange(other.m_return_code, m_return_code); #ifdef NES_PROCESS_PIPE_EXTENSION m_stdin_stream = std::move(other.m_stdin_stream); m_stdout_stream = std::move(other.m_stdout_stream); m_stderr_stream = std::move(other.m_stderr_stream); #endif return *this; } void join() { assert(joinable() && "nes::process::join() called with joinable() returning false."); int return_code{}; if(waitpid(m_id, &return_code, 0) == -1) throw std::runtime_error{"Failed to join the process. " + std::string{strerror(errno)}}; m_id = -1; m_return_code = WEXITSTATUS(return_code); } bool joinable() const noexcept { return m_id != -1; } bool active() const { return ::kill(m_id, 0) != ESRCH; } void detach() { assert(joinable() && "nes::process::detach() called with joinable() returning false."); m_id = -1; } bool kill() { assert(joinable() && "nes::process::kill() called with joinable() returning false."); if(::kill(m_id, SIGTERM)) return false; join(); return true; } return_code_type return_code() const noexcept { assert(!joinable() && "nes::process::return_code() called with joinable() returning true."); return m_return_code; } native_handle_type native_handle() const noexcept { return m_id; } id get_id() const noexcept { return static_cast(m_id); } #ifdef NES_PROCESS_PIPE_EXTENSION pipe_ostream& stdin_stream() noexcept { return *m_stdin_stream; } pipe_istream& stdout_stream() noexcept { return *m_stdout_stream; } pipe_istream& stderr_stream() noexcept { return *m_stderr_stream; } #endif private: native_handle_type m_id{}; return_code_type m_return_code{}; #ifdef NES_PROCESS_PIPE_EXTENSION std::unique_ptr m_stdin_stream{}; std::unique_ptr m_stdout_stream{}; std::unique_ptr m_stderr_stream{}; #endif }; namespace this_process { inline process::id get_id() noexcept { return process::id{getpid()}; } inline std::string working_directory() { std::string path{}; path.resize(256); while(!getcwd(std::data(path), std::size(path))) { if(errno == ERANGE) path.resize(std::size(path) * 2); else throw std::runtime_error{"Failed to get the current working directory. " + std::string{strerror(errno)}}; } path.resize(path.find_first_of('\0')); return path; } inline bool change_working_directory(const std::string& path) { return chdir(std::data(path)) == 0; } } } template std::basic_ostream& operator<<(std::basic_ostream& os, nes::process::id id) { return os << static_cast(id); } namespace std { template<> struct hash { using argument_type = nes::process::id; using result_type = std::size_t; result_type operator()(const argument_type& s) const noexcept { return std::hash{}(static_cast(s)); } }; } #endif #endif