I am working on a game engine that should read (among others) compressed files and decompress them into memory. Where the compressed file should be memory mapped.
For that I implemented a simple memory mapped file with a minimal API
In my code, no exceptions are thrown. For hard, unrecoverable errors, i simply call std::terminate, otherwise I make use of raid::result<T>.
raid::result<T> is a simple std::expected<T, raid::raid_error_code> where raid::raid_error_code is just an enum class for every possible error in my engine.
//specific/mapped_file.h
#ifndef GAME_SRC_SPECIFIC_MAPPED_FILE_H
#define GAME_SRC_SPECIFIC_MAPPED_FILE_H
#include "common/error.h"
#include <filesystem>
#include <cstddef>
#include "common/unowned_ptr.h"
namespace raid {
struct mapped_file;
struct mapped_file_deleter {
void operator()(mapped_file*);
};
using mapped_file_ptr = std::unique_ptr<mapped_file,mapped_file_deleter>;
result<mapped_file_ptr> open_memfile(const std::filesystem::path& p);
std::span<const std::byte> get_data(unowned_ptr<const mapped_file> file);
}
#endif
I make use of an opaque data type here because the engine is supposed to be run on linux and windows. An implementation for linux is as follows:
//specific/linux/mapped_file.cpp
#include "specific/mapped_file.h"
#include "common/error.h"
#include "common/log.h"
#include "common/unowned_ptr.h"
#include <cstddef>
#include <filesystem>
#include <span>
#include <sys/mman.h>
#include <system_error>
#include <unistd.h>
#include <fcntl.h>
namespace raid {
struct mapped_file {
int fd;
std::span<std::byte> data;
};
result<mapped_file_ptr> open_memfile(const std::filesystem::path& p) {
std::error_code ec;
if(!std::filesystem::exists(p)) {
log_error("File does not exist");
return make_error(raid_error_code::file_not_found);
}
auto file_size = std::filesystem::file_size(p, ec);
if(ec) {
log_error("Could not open file for memory mapping: ", ec.message());
return make_error(raid_error_code::file_access_error);
}
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
auto fd = open(p.c_str(), O_RDONLY);
if(fd == -1) {
log_error("Could not open file", p.c_str());
return make_error(raid_error_code::file_descriptor_failed);
}
log_debug("Opened file", p.c_str(), "FD: ", fd);
auto ptr = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(ptr == MAP_FAILED) {
log_error("Could not memory map file ", p.c_str());
close(fd);
return make_error(raid_error_code::file_memory_map_failed);
}
log_debug("Memory mapped File", p.c_str());
return mapped_file_ptr(new mapped_file{
.fd = fd,
.data = std::span{(std::byte*)ptr,file_size},
});
}
void mapped_file_deleter::operator()(mapped_file* f) {
munmap(f->data.data(), f->data.size_bytes());
log_debug("munmap", f->fd);
close(f->fd);
log_debug("closed FD:", f->fd);
delete f;
}
std::span<const std::byte> get_data(unowned_ptr<const mapped_file> file) {
return file->data;
}
}
I am aware of the extra indirection due to it being an opaque data type.
Example usage, using flatbuffers:
auto gameflow_file = open_memfile("gameflow.bin");
if(!gameflow_file) {
return make_error(gameflow_file.error());
}
auto gameflow_data = get_data((*gameflow_file).get());
auto* gf = serialization::Getgameflow_data(gameflow_data.data());
boost::interprocessmemory mapping? That's cross-platform, FTW. \$\endgroup\$