Skip to content

Commit f1d58ad

Browse files
authored
Implement a cross-platform mmap class in src/lang/io (#2156)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 19a1d89 commit f1d58ad

File tree

13 files changed

+294
-15
lines changed

13 files changed

+294
-15
lines changed

src/lang/io/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME io SOURCES io.cc)
1+
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME io
2+
PRIVATE_HEADERS error.h fileview.h
3+
SOURCES io.cc io_fileview.cc)
24

35
if(SOURCEMETA_CORE_INSTALL)
46
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME io)

src/lang/io/include/sourcemeta/core/io.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
#include <sourcemeta/core/io_export.h>
66
#endif
77

8+
// NOLINTBEGIN(misc-include-cleaner)
9+
#include <sourcemeta/core/io_error.h>
10+
#include <sourcemeta/core/io_fileview.h>
11+
// NOLINTEND(misc-include-cleaner)
12+
813
#include <cassert> // assert
914
#include <filesystem> // std::filesystem
1015
#include <fstream> // std::basic_ifstream
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#ifndef SOURCEMETA_CORE_IO_ERROR_H_
2+
#define SOURCEMETA_CORE_IO_ERROR_H_
3+
4+
#ifndef SOURCEMETA_CORE_IO_EXPORT
5+
#include <sourcemeta/core/io_export.h>
6+
#endif
7+
8+
#include <exception> // std::exception
9+
#include <filesystem> // std::filesystem::path
10+
#include <string> // std::string
11+
#include <string_view> // std::string_view
12+
#include <utility> // std::move
13+
14+
namespace sourcemeta::core {
15+
16+
// Exporting symbols that depends on the standard C++ library is considered
17+
// safe.
18+
// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
19+
#if defined(_MSC_VER)
20+
#pragma warning(disable : 4251 4275)
21+
#endif
22+
23+
/// @ingroup io
24+
/// An error that represents a failure to memory-map a file
25+
class SOURCEMETA_CORE_IO_EXPORT FileViewError : public std::exception {
26+
public:
27+
FileViewError(std::filesystem::path path, const char *message)
28+
: path_{std::move(path)}, message_{message} {}
29+
FileViewError(std::filesystem::path path, std::string message) = delete;
30+
FileViewError(std::filesystem::path path, std::string &&message) = delete;
31+
FileViewError(std::filesystem::path path, std::string_view message) = delete;
32+
33+
[[nodiscard]] auto what() const noexcept -> const char * override {
34+
return this->message_;
35+
}
36+
37+
[[nodiscard]] auto path() const noexcept -> const std::filesystem::path & {
38+
return this->path_;
39+
}
40+
41+
private:
42+
std::filesystem::path path_;
43+
const char *message_;
44+
};
45+
46+
#if defined(_MSC_VER)
47+
#pragma warning(default : 4251 4275)
48+
#endif
49+
50+
} // namespace sourcemeta::core
51+
52+
#endif
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#ifndef SOURCEMETA_CORE_IO_FILEVIEW_H_
2+
#define SOURCEMETA_CORE_IO_FILEVIEW_H_
3+
4+
#ifndef SOURCEMETA_CORE_IO_EXPORT
5+
#include <sourcemeta/core/io_export.h>
6+
#endif
7+
8+
#include <cassert> // assert
9+
#include <cstddef> // std::size_t
10+
#include <cstdint> // std::uint8_t
11+
#include <filesystem> // std::filesystem::path
12+
13+
namespace sourcemeta::core {
14+
15+
/// @ingroup io
16+
/// A read-only memory-mapped file. For example:
17+
///
18+
/// ```cpp
19+
/// #include <sourcemeta/core/io.h>
20+
/// #include <cassert>
21+
///
22+
/// struct Header {
23+
/// std::uint32_t magic;
24+
/// std::uint32_t version;
25+
/// };
26+
///
27+
/// sourcemeta::core::FileView view{"/path/to/file.bin"};
28+
/// const auto *header = view.as<Header>();
29+
/// assert(header->magic == 0x12345678);
30+
/// ```
31+
class SOURCEMETA_CORE_IO_EXPORT FileView {
32+
public:
33+
FileView(const std::filesystem::path &path);
34+
~FileView();
35+
36+
// Disable copying and moving
37+
FileView(const FileView &) = delete;
38+
FileView(FileView &&) = delete;
39+
auto operator=(const FileView &) -> FileView & = delete;
40+
auto operator=(FileView &&) -> FileView & = delete;
41+
42+
/// The size of the memory-mapped data in bytes
43+
[[nodiscard]] auto size() const noexcept -> std::size_t;
44+
45+
/// Interpret the memory-mapped data as a pointer to T at the given offset.
46+
template <typename T>
47+
[[nodiscard]] auto as(const std::size_t offset = 0) const noexcept
48+
-> const T * {
49+
assert(offset + sizeof(T) <= this->size_);
50+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
51+
return reinterpret_cast<const T *>(this->data_ + offset);
52+
}
53+
54+
private:
55+
const std::uint8_t *data_{nullptr};
56+
std::size_t size_{0};
57+
#if defined(_WIN32)
58+
void *file_handle_{nullptr};
59+
void *mapping_handle_{nullptr};
60+
#else
61+
int file_descriptor_{-1};
62+
#endif
63+
};
64+
65+
} // namespace sourcemeta::core
66+
67+
#endif

src/lang/io/io_fileview.cc

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#include <sourcemeta/core/io_error.h>
2+
#include <sourcemeta/core/io_fileview.h>
3+
4+
#if defined(_WIN32)
5+
#define WIN32_LEAN_AND_MEAN
6+
#include <windows.h>
7+
#else
8+
#include <fcntl.h> // open, O_RDONLY
9+
#include <sys/mman.h> // mmap, munmap
10+
#include <sys/stat.h> // fstat
11+
#include <unistd.h> // close
12+
#endif
13+
14+
namespace sourcemeta::core {
15+
16+
#if defined(_WIN32)
17+
18+
FileView::FileView(const std::filesystem::path &path) {
19+
this->file_handle_ =
20+
CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
21+
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
22+
if (this->file_handle_ == INVALID_HANDLE_VALUE) {
23+
throw FileViewError(path, "Could not open the file");
24+
}
25+
26+
LARGE_INTEGER file_size;
27+
if (GetFileSizeEx(this->file_handle_, &file_size) == 0) {
28+
CloseHandle(this->file_handle_);
29+
throw FileViewError(path, "Could not determine the file size");
30+
}
31+
this->size_ = static_cast<std::size_t>(file_size.QuadPart);
32+
33+
this->mapping_handle_ = CreateFileMappingW(this->file_handle_, nullptr,
34+
PAGE_READONLY, 0, 0, nullptr);
35+
if (this->mapping_handle_ == nullptr) {
36+
CloseHandle(this->file_handle_);
37+
throw FileViewError(path, "Could not create a file mapping");
38+
}
39+
40+
this->data_ = static_cast<const std::uint8_t *>(
41+
MapViewOfFile(this->mapping_handle_, FILE_MAP_READ, 0, 0, 0));
42+
if (this->data_ == nullptr) {
43+
CloseHandle(this->mapping_handle_);
44+
CloseHandle(this->file_handle_);
45+
throw FileViewError(path, "Could not map the file into memory");
46+
}
47+
}
48+
49+
FileView::~FileView() {
50+
if (this->data_ != nullptr) {
51+
UnmapViewOfFile(this->data_);
52+
}
53+
54+
if (this->mapping_handle_ != nullptr) {
55+
CloseHandle(this->mapping_handle_);
56+
}
57+
58+
if (this->file_handle_ != nullptr &&
59+
this->file_handle_ != INVALID_HANDLE_VALUE) {
60+
CloseHandle(this->file_handle_);
61+
}
62+
}
63+
64+
#else
65+
66+
FileView::FileView(const std::filesystem::path &path) {
67+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
68+
this->file_descriptor_ = open(path.c_str(), O_RDONLY);
69+
if (this->file_descriptor_ == -1) {
70+
throw FileViewError(path, "Could not open the file");
71+
}
72+
73+
struct stat file_stat;
74+
if (fstat(this->file_descriptor_, &file_stat) != 0) {
75+
close(this->file_descriptor_);
76+
throw FileViewError(path, "Could not determine the file size");
77+
}
78+
this->size_ = static_cast<std::size_t>(file_stat.st_size);
79+
80+
void *mapped = mmap(nullptr, this->size_, PROT_READ, MAP_PRIVATE,
81+
this->file_descriptor_, 0);
82+
if (mapped == MAP_FAILED) {
83+
close(this->file_descriptor_);
84+
throw FileViewError(path, "Could not map the file into memory");
85+
}
86+
87+
this->data_ = static_cast<const std::uint8_t *>(mapped);
88+
}
89+
90+
FileView::~FileView() {
91+
if (this->data_ != nullptr && this->size_ > 0) {
92+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
93+
munmap(const_cast<std::uint8_t *>(this->data_), this->size_);
94+
}
95+
96+
if (this->file_descriptor_ != -1) {
97+
close(this->file_descriptor_);
98+
}
99+
}
100+
101+
#endif
102+
103+
auto FileView::size() const noexcept -> std::size_t { return this->size_; }
104+
105+
} // namespace sourcemeta::core

test/io/CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME io
44
io_flush_test.cc
55
io_weakly_canonical_test.cc
66
io_starts_with_test.cc
7-
io_read_file_test.cc)
7+
io_read_file_test.cc
8+
io_fileview_test.cc)
89

910
target_link_libraries(sourcemeta_core_io_unit
1011
PRIVATE sourcemeta::core::io)
1112
target_compile_definitions(sourcemeta_core_io_unit
12-
PRIVATE TEST_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}")
13+
PRIVATE STUBS_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}/stubs")

test/io/io_canonical_test.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
TEST(IO_canonical, test_txt) {
66
const auto path{sourcemeta::core::canonical(
7-
std::filesystem::path{TEST_DIRECTORY} / ".." / "io" / "test.txt")};
8-
EXPECT_EQ(path, std::filesystem::path{TEST_DIRECTORY} / "test.txt");
7+
std::filesystem::path{STUBS_DIRECTORY} / "test.txt")};
8+
EXPECT_EQ(path, std::filesystem::path{STUBS_DIRECTORY} / "test.txt");
99
}
1010

1111
TEST(IO_canonical, not_exists) {
1212
EXPECT_THROW(sourcemeta::core::canonical(
13-
std::filesystem::path{TEST_DIRECTORY} / "foo.txt"),
13+
std::filesystem::path{STUBS_DIRECTORY} / "foo.txt"),
1414
std::filesystem::filesystem_error);
1515
}

test/io/io_fileview_test.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <sourcemeta/core/io.h>
4+
5+
#include <cstdint> // std::uint32_t
6+
7+
TEST(IO_FileView, size) {
8+
const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} /
9+
"fileview.bin"};
10+
EXPECT_EQ(view.size(), 20);
11+
}
12+
13+
TEST(IO_FileView, as_header) {
14+
struct Header {
15+
std::uint32_t magic;
16+
std::uint32_t version;
17+
std::uint32_t count;
18+
};
19+
20+
const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} /
21+
"fileview.bin"};
22+
const auto *header = view.as<Header>();
23+
24+
EXPECT_EQ(header->magic, 0x56574946);
25+
EXPECT_EQ(header->version, 1);
26+
EXPECT_EQ(header->count, 42);
27+
}
28+
29+
TEST(IO_FileView, as_with_offset) {
30+
struct Data {
31+
std::uint32_t value1;
32+
std::uint32_t value2;
33+
};
34+
35+
const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} /
36+
"fileview.bin"};
37+
const auto *data = view.as<Data>(12);
38+
39+
EXPECT_EQ(data->value1, 0xDEADBEEF);
40+
EXPECT_EQ(data->value2, 0xCAFEBABE);
41+
}
42+
43+
TEST(IO_FileView, file_not_found) {
44+
EXPECT_THROW(sourcemeta::core::FileView(
45+
std::filesystem::path{STUBS_DIRECTORY} / "nonexistent.bin"),
46+
sourcemeta::core::FileViewError);
47+
}

test/io/io_flush_test.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
#include <sourcemeta/core/io.h>
44

55
TEST(IO_flush, test_txt) {
6-
const auto path{std::filesystem::path{TEST_DIRECTORY} / "test.txt"};
6+
const auto path{std::filesystem::path{STUBS_DIRECTORY} / "test.txt"};
77
sourcemeta::core::flush(path);
88
SUCCEED();
99
}
1010

1111
TEST(IO_flush, not_exists) {
12-
const auto path{std::filesystem::path{TEST_DIRECTORY} / "foo.txt"};
12+
const auto path{std::filesystem::path{STUBS_DIRECTORY} / "foo.txt"};
1313

1414
try {
1515
sourcemeta::core::flush(path);

test/io/io_read_file_test.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
TEST(IO_read_file, text_file) {
99
auto stream{sourcemeta::core::read_file(
10-
std::filesystem::path{TEST_DIRECTORY} / "test.txt")};
10+
std::filesystem::path{STUBS_DIRECTORY} / "test.txt")};
1111
std::ostringstream contents;
1212
contents << stream.rdbuf();
1313
auto result{contents.str()};
@@ -19,10 +19,10 @@ TEST(IO_read_file, text_file) {
1919

2020
TEST(IO_read_file, directory) {
2121
try {
22-
sourcemeta::core::read_file(std::filesystem::path{TEST_DIRECTORY});
22+
sourcemeta::core::read_file(std::filesystem::path{STUBS_DIRECTORY});
2323
} catch (const std::filesystem::filesystem_error &error) {
2424
EXPECT_EQ(error.code(), std::errc::is_a_directory);
25-
EXPECT_EQ(error.path1(), std::filesystem::path{TEST_DIRECTORY});
25+
EXPECT_EQ(error.path1(), std::filesystem::path{STUBS_DIRECTORY});
2626
} catch (...) {
2727
FAIL() << "The parse function was expected to throw a filesystem error";
2828
}

0 commit comments

Comments
 (0)