diff --git a/include/mio/detail/mmap_impl.hpp b/include/mio/detail/mmap_impl.hpp index ea6e802..9695cfb 100644 --- a/include/mio/detail/mmap_impl.hpp +++ b/include/mio/detail/mmap_impl.hpp @@ -3,6 +3,7 @@ #include #include +#include #ifdef _WIN32 # ifndef WIN32_LEAN_AND_MEAN @@ -71,25 +72,10 @@ public: mmap& operator=(mmap&&); ~mmap(); - /** - * On *nix systems is_open and is_mapped are the same and don't actually say if - * the file itself is open or closed, they only refer to the mapping. This is - * because a mapping remains valid (as long as it's not unmapped) even if another - * entity closes the file which is being mapped. - * - * On Windows, however, in order to map a file, both an active file handle and a - * mapping handle is required, so is_open checks for a valid file handle, while - * is_mapped checks for a valid mapping handle. - */ bool is_open() const noexcept; bool is_mapped() const noexcept; bool empty() const noexcept { return length() == 0; } - /** - * size/length returns the logical length (i.e. the one user requested), while - * mapped_length returns the actual mapped length, which is usually a multiple of - * the OS' page size. - */ size_type size() const noexcept { return length_; } size_type length() const noexcept { return length_; } size_type mapped_length() const noexcept { return mapped_length_; } @@ -116,8 +102,6 @@ public: reference operator[](const size_type i) noexcept { return data_[i]; } const_reference operator[](const size_type i) const noexcept { return data_[i]; } - void map(const std::string& path, const size_type offset, const size_type length, - const access_mode mode, std::error_code& error); void map(const handle_type handle, const size_type offset, const size_type length, const access_mode mode, std::error_code& error); void unmap(); @@ -131,9 +115,6 @@ public: private: - static handle_type open_file(const std::string& path, - const access_mode mode, std::error_code& error); - pointer get_mapping_start() noexcept; /** NOTE: file_handle_ must be valid. */ @@ -145,6 +126,10 @@ private: void verify_file_handle(std::error_code& error) const noexcept; }; +template +mmap::handle_type open_file(const Path& path, + const mmap::access_mode mode, std::error_code& error); + } // namespace detail } // namespace mio diff --git a/include/mio/detail/mmap_impl.ipp b/include/mio/detail/mmap_impl.ipp index 3e708f7..b72331c 100644 --- a/include/mio/detail/mmap_impl.ipp +++ b/include/mio/detail/mmap_impl.ipp @@ -4,12 +4,15 @@ #include "mmap_impl.hpp" #include +#include #ifndef _WIN32 # include # include # include # include +# include +# include #endif namespace mio { @@ -105,45 +108,6 @@ inline mmap& mmap::operator=(mmap&& other) return *this; } -inline void mmap::map(const std::string& path, const size_type offset, - const size_type length, const access_mode mode, std::error_code& error) -{ - if(!path.empty() && !is_open()) - { - error.clear(); - const auto handle = open_file(path, mode, error); - if(error) { return; } - map(handle, offset, length, mode, error); - } -} - -inline mmap::handle_type mmap::open_file(const std::string& path, - const mmap::access_mode mode, std::error_code& error) -{ -#if defined(_WIN32) - const auto handle = ::CreateFile(path.c_str(), - mode == mmap::access_mode::read_only - ? GENERIC_READ : GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - 0, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - 0); - if(handle == INVALID_HANDLE_VALUE) - { - error = last_error(); - } -#else - const auto handle = ::open(path.c_str(), - mode == mmap::access_mode::read_only ? O_RDONLY : O_RDWR); - if(handle == -1) - { - error = last_error(); - } -#endif - return handle; -} - inline void mmap::map(const handle_type handle, const size_type offset, const size_type length, const access_mode mode, std::error_code& error) { @@ -269,13 +233,13 @@ inline void mmap::unmap() inline mmap::size_type mmap::query_file_size(std::error_code& error) noexcept { #ifdef _WIN32 - PLARGE_INTEGER file_size; + LARGE_INTEGER file_size; if(::GetFileSizeEx(file_handle_, &file_size) == 0) { error = last_error(); return 0; } - return file_size; + return static_cast(file_size.QuadPart); #else struct stat sbuf; if(::fstat(file_handle_, &sbuf) == -1) @@ -345,6 +309,53 @@ inline bool operator!=(const mmap& a, const mmap& b) return !(a == b); } +template< + typename String, + typename = decltype(std::declval().data()), + typename = typename std::enable_if::value>::type +> const char* c_str(const String& path) +{ + return path.data(); +} + +template< + typename String, + typename = typename std::enable_if< + std::is_same::type>::value + >::type +> const char* c_str(String path) +{ + return path; +} + +template +mmap::handle_type open_file(const Path& path, + const mmap::access_mode mode, std::error_code& error) +{ +#if defined(_WIN32) + const auto handle = ::CreateFile(c_str(path), + mode == mmap::access_mode::read_only + ? GENERIC_READ : GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + 0, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + 0); + if(handle == INVALID_HANDLE_VALUE) + { + error = last_error(); + } +#else + const auto handle = ::open(c_str(path), + mode == mmap::access_mode::read_only ? O_RDONLY : O_RDWR); + if(handle == -1) + { + error = last_error(); + } +#endif + return handle; +} + } // namespace detail } // namespace mio diff --git a/include/mio/mmap.hpp b/include/mio/mmap.hpp index da1dbde..5f45dc9 100644 --- a/include/mio/mmap.hpp +++ b/include/mio/mmap.hpp @@ -2,6 +2,7 @@ #define MIO_MMAP_HEADER #include "detail/mmap_impl.hpp" +#include namespace mio { @@ -10,14 +11,11 @@ namespace mio { * offsets that are aligned with the operating system's page granularity, this is taken * care of within the class in both cases. * - * Both classes have std::unique_ptr<> semantics, thus only a single entity may own - * a mapping to a file at any given time. + * Both classes have single-ownership semantics, and transferring ownership may be + * accomplished by moving the mmap instance. * * Remapping a file is possible, but unmap must be called before that. * - * For now, both classes may only be used with an existing open file by providing the - * file's handle. - * * Both classes' destructors unmap the file. However, mmap_sink's destructor does not * sync the mapped file view to disk, this has to be done manually with a call tosink. */ @@ -45,13 +43,18 @@ public: using handle_type = impl_type::handle_type; mmap_source() = default; - mmap_source(const std::string& path, const size_type offset, const size_type length) - { - std::error_code error; - map(path, offset, length, error); - if(error) { throw error; } - } + /** + * `handle` must be a valid file handle, which is then used to memory map the + * requested region. Upon failure a `std::error_code` is thrown, detailing the + * cause of the error, and the object remains in an unmapped state. + * + * When specifying `offset`, there is no need to worry about providing + * a value that is aligned with the operating system's page allocation granularity. + * This is adjusted by the implementation such that the first requested byte (as + * returned by `data` or `begin`), so long as `offset` is valid, will be at `offset` + * from the start of the file. + */ mmap_source(const handle_type handle, const size_type offset, const size_type length) { std::error_code error; @@ -60,54 +63,92 @@ public: } /** - * On *nix systems is_open and is_mapped are the same and don't actually say if + * `mmap_source` has single-ownership semantics, so transferring ownership may only + * be accomplished by moving the instance. + */ + mmap_source(const mmap_source&) = delete; + mmap_source(mmap_source&&) = default; + + /** The destructor invokes unmap. */ + ~mmap_source() = default; + + /** + * On UNIX systems `is_open` and `is_mapped` are the same and don't actually say if * the file itself is open or closed, they only refer to the mapping. This is * because a mapping remains valid (as long as it's not unmapped) even if another * entity closes the file which is being mapped. * * On Windows, however, in order to map a file, both an active file handle and a - * mapping handle is required, so is_open checks for a valid file handle, while - * is_mapped checks for a valid mapping handle. + * mapping handle is required, so `is_open` checks for a valid file handle, while + * `is_mapped` checks for a valid mapping handle. */ bool is_open() const noexcept { return impl_.is_open(); } bool is_mapped() const noexcept { return impl_.is_mapped(); } bool empty() const noexcept { return impl_.empty(); } /** - * size/length returns the logical length (i.e. the one user requested), while - * mapped_length returns the actual mapped length, which is usually a multiple of - * the OS' page size. + * `size` and `length` both return the logical length (i.e. the one user requested), + * while `mapped_length` returns the actual mapped length, which is usually a + * multiple of the underlying operating system's page allocation granularity. */ size_type size() const noexcept { return impl_.size(); } size_type length() const noexcept { return impl_.length(); } size_type mapped_length() const noexcept { return impl_.mapped_length(); } + /** + * Returns a pointer to the first requested byte, or `nullptr` if no memory mapping + * exists. + */ const_pointer data() const noexcept { return impl_.data(); } + /** + * Returns an iterator to the first requested byte, if a valid memory mapping + * exists, otherwise this function call is equivalent to invoking `end`. + */ const_iterator begin() const noexcept { return impl_.begin(); } const_iterator cbegin() const noexcept { return impl_.cbegin(); } + + /** Returns an iterator one past the last requested byte. */ const_iterator end() const noexcept { return impl_.end(); } const_iterator cend() const noexcept { return impl_.cend(); } const_reverse_iterator rbegin() const noexcept { return impl_.rbegin(); } const_reverse_iterator crbegin() const noexcept { return impl_.crbegin(); } + const_reverse_iterator rend() const noexcept { return impl_.rend(); } const_reverse_iterator crend() const noexcept { return impl_.crend(); } + /** + * Returns a reference to the `i`th byte from the first requested byte (as returned + * by `data`). If this is invoked when no valid memory mapping has been established + * prior to this call, undefined behaviour ensues. + */ const_reference operator[](const size_type i) const noexcept { return impl_[i]; } - void map(const std::string& path, const size_type offset, - const size_type length, std::error_code& error) - { - impl_.map(path, offset, length, impl_type::access_mode::read_only, error); - } - + /** + * Establishes a read-only memory mapping. + * + * `handle` must be a valid file handle, which is then used to memory map the + * requested region. Upon failure, `error` is set to indicate the reason and the + * object remains in an unmapped state. + * + * When specifying `offset`, there is no need to worry about providing + * a value that is aligned with the operating system's page allocation granularity. + * This is adjusted by the implementation such that the first requested byte (as + * returned by `data` or `begin`), so long as `offset` is valid, will be at `offset` + * from the start of the file. + */ void map(const handle_type handle, const size_type offset, const size_type length, std::error_code& error) { impl_.map(handle, offset, length, impl_type::access_mode::read_only, error); } + /** + * If a valid memory mapping has been established prior to this call, this call + * instructs the kernel to unmap the memory region and dissasociate this object + * from the file. + */ void unmap() { impl_.unmap(); } void swap(mmap_source& other) { impl_.swap(other.impl_); } @@ -146,13 +187,18 @@ public: using handle_type = impl_type::handle_type; mmap_sink() = default; - mmap_sink(const std::string& path, const size_type offset, const size_type length) - { - std::error_code error; - map(path, offset, length, error); - if(error) { throw error; } - } + /** + * `handle` must be a valid file handle, which is then used to memory map the + * requested region. Upon failure a `std::error_code` is thrown, detailing the + * cause of the error, and the object remains in an unmapped state. + * + * When specifying `offset`, there is no need to worry about providing + * a value that is aligned with the operating system's page allocation granularity. + * This is adjusted by the implementation such that the first requested byte (as + * returned by `data` or `begin`), so long as `offset` is valid, will be at `offset` + * from the start of the file. + */ mmap_sink(const handle_type handle, const size_type offset, const size_type length) { std::error_code error; @@ -161,35 +207,58 @@ public: } /** - * On *nix systems is_open and is_mapped are the same and don't actually say if + * `mmap_sink` has single-ownership semantics, so transferring ownership may only + * be accomplished by moving the instance. + */ + mmap_sink(const mmap_sink&) = delete; + mmap_sink(mmap_sink&&) = default; + + /** + * The destructor invokes unmap, but does NOT invoke `sync`. Thus, if changes have + * been made to the mapped region, `sync` needs to be called in order to persist + * any writes to disk. + */ + ~mmap_sink() = default; + + /** + * On UNIX systems `is_open` and `is_mapped` are the same and don't actually say if * the file itself is open or closed, they only refer to the mapping. This is * because a mapping remains valid (as long as it's not unmapped) even if another * entity closes the file which is being mapped. * * On Windows, however, in order to map a file, both an active file handle and a - * mapping handle is required, so is_open checks for a valid file handle, while - * is_mapped checks for a valid mapping handle. + * mapping handle is required, so `is_open` checks for a valid file handle, while + * `is_mapped` checks for a valid mapping handle. */ bool is_open() const noexcept { return impl_.is_open(); } bool is_mapped() const noexcept { return impl_.is_mapped(); } bool empty() const noexcept { return impl_.empty(); } /** - * size/length returns the logical length (i.e. the one user requested), while - * mapped_length returns the actual mapped length, which is usually a multiple of - * the OS' page size. + * `size` and `length` both return the logical length (i.e. the one user requested), + * while `mapped_length` returns the actual mapped length, which is usually a + * multiple of the underlying operating system's page allocation granularity. */ size_type size() const noexcept { return impl_.size(); } size_type length() const noexcept { return impl_.length(); } size_type mapped_length() const noexcept { return impl_.mapped_length(); } + /** + * Returns a pointer to the first requested byte, or `nullptr` if no memory mapping + * exists. + */ pointer data() noexcept { return impl_.data(); } const_pointer data() const noexcept { return impl_.data(); } + /** + * Returns an iterator to the first requested byte, if a valid memory mapping + * exists, otherwise this function call is equivalent to invoking `end`. + */ iterator begin() noexcept { return impl_.begin(); } const_iterator begin() const noexcept { return impl_.begin(); } const_iterator cbegin() const noexcept { return impl_.cbegin(); } + /** Returns an iterator one past the last requested byte. */ iterator end() noexcept { return impl_.end(); } const_iterator end() const noexcept { return impl_.end(); } const_iterator cend() const noexcept { return impl_.cend(); } @@ -202,21 +271,38 @@ public: const_reverse_iterator rend() const noexcept { return impl_.rend(); } const_reverse_iterator crend() const noexcept { return impl_.crend(); } + /** + * Returns a reference to the `i`th byte from the first requested byte (as returned + * by `data`). If this is invoked when no valid memory mapping has been established + * prior to this call, undefined behaviour ensues. + */ reference operator[](const size_type i) noexcept { return impl_[i]; } const_reference operator[](const size_type i) const noexcept { return impl_[i]; } - void map(const std::string& path, const size_type offset, - const size_type length, std::error_code& error) - { - impl_.map(path, offset, length, impl_type::access_mode::read_only, error); - } - + /** + * Establishes a read-only memory mapping. + * + * `handle` must be a valid file handle, which is then used to memory map the + * requested region. Upon failure, `error` is set to indicate the reason and the + * object remains in an unmapped state. + * + * When specifying `offset`, there is no need to worry about providing + * a value that is aligned with the operating system's page allocation granularity. + * This is adjusted by the implementation such that the first requested byte (as + * returned by `data` or `begin`), so long as `offset` is valid, will be at `offset` + * from the start of the file. + */ void map(const handle_type handle, const size_type offset, const size_type length, std::error_code& error) { impl_.map(handle, offset, length, impl_type::access_mode::read_write, error); } + /** + * If a valid memory mapping has been established prior to this call, this call + * instructs the kernel to unmap the memory region and dissasociate this object + * from the file. + */ void unmap() { impl_.unmap(); } /** Flushes the memory mapped page to disk. */ @@ -235,6 +321,43 @@ public: } }; +/** + * Since `mmap_source` works on the file descriptor/handle level of abstraction, a + * factory method is provided for the case when a file needs to be mapped using a file + * path. + * + * Path may be `std::string`, `std::string_view`, `const char*`, + * `std::filesystem::path`, `std::vector`, or similar. + */ +template +mmap_source make_mmap_source(const Path& path, mmap_source::size_type offset, + mmap_source::size_type length, std::error_code& error) +{ + const auto handle = detail::open_file(path, + detail::mmap::access_mode::read_only, error); + mmap_source mmap; + if(!error) { mmap.map(handle, offset, length, error); } + return mmap; +} + +/** + * Since `mmap_sink` works on the file descriptor/handle level of abstraction, a factory + * method is provided for the case when a file needs to be mapped using a file path. + * + * Path may be `std::string`, `std::string_view`, `const char*`, + * `std::filesystem::path`, `std::vector`, or similar. + */ +template +mmap_sink make_mmap_sink(const Path& path, mmap_sink::size_type offset, + mmap_sink::size_type length, std::error_code& error) +{ + const auto handle = detail::open_file(path, + detail::mmap::access_mode::read_write, error); + mmap_sink mmap; + if(!error) { mmap.map(handle, offset, length, error); } + return mmap; +} + } // namespace mio #endif // MIO_MMAP_HEADER diff --git a/test/test.cpp b/test/test.cpp index ca24ab1..4839c67 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -6,9 +6,9 @@ #include #include -int main(int argc, char** argv) +int main() { - const char* test_file_name = argc >= 2 ? argv[1] : "test-file"; + const char* test_file_name = "test-file"; std::string buffer(0x4000 - 250, 'M'); @@ -17,8 +17,7 @@ int main(int argc, char** argv) file.close(); std::error_code error; - mio::mmap_source file_view; - file_view.map(test_file_name, 0, buffer.size(), error); + mio::mmap_source file_view = mio::make_mmap_source(test_file_name, 0, buffer.size(), error); if(error) { const auto& errmsg = error.message(); @@ -39,4 +38,10 @@ int main(int argc, char** argv) assert(0); } } + + // see if mapping an invalid file results in an error + mio::make_mmap_source("garbage-that-hopefully-doesn't exist", 0, 0, error); + assert(error); + + std::printf("all tests passed!\n"); }