mirror of
https://github.com/izzy2lost/dolphin.git
synced 2026-06-19 01:16:48 -07:00
Common: Add a DirectIOFile class that allows for copies which are entirely thread safe.
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
// Copyright 2025 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Common/DirectIOFile.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "Common/Buffer.h"
|
||||
#include "Common/CommonFuncs.h"
|
||||
#include "Common/MathUtil.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#else
|
||||
#include <string>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef ANDROID
|
||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||
|
||||
#include "Common/Lazy.h"
|
||||
#endif
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#endif
|
||||
|
||||
#include "Common/Assert.h"
|
||||
|
||||
namespace File
|
||||
{
|
||||
DirectIOFile::DirectIOFile() = default;
|
||||
|
||||
DirectIOFile::~DirectIOFile()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
DirectIOFile::DirectIOFile(const DirectIOFile& other)
|
||||
{
|
||||
*this = other.Duplicate();
|
||||
}
|
||||
|
||||
DirectIOFile& DirectIOFile::operator=(const DirectIOFile& other)
|
||||
{
|
||||
return *this = other.Duplicate();
|
||||
}
|
||||
|
||||
DirectIOFile::DirectIOFile(DirectIOFile&& other)
|
||||
{
|
||||
Swap(other);
|
||||
}
|
||||
|
||||
DirectIOFile& DirectIOFile::operator=(DirectIOFile&& other)
|
||||
{
|
||||
Close();
|
||||
Swap(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DirectIOFile::DirectIOFile(const std::string& path, AccessMode access_mode, OpenMode open_mode)
|
||||
{
|
||||
Open(path, access_mode, open_mode);
|
||||
}
|
||||
|
||||
bool DirectIOFile::Open(const std::string& path, AccessMode access_mode, OpenMode open_mode)
|
||||
{
|
||||
ASSERT(!IsOpen());
|
||||
|
||||
if (open_mode == OpenMode::Default)
|
||||
open_mode = (access_mode == AccessMode::Write) ? OpenMode::Truncate : OpenMode::Existing;
|
||||
|
||||
// This is not a sensible combination. Fail here to not rely on OS-specific behaviors.
|
||||
if (access_mode == AccessMode::Read && open_mode == OpenMode::Truncate)
|
||||
return false;
|
||||
|
||||
#if defined(_WIN32)
|
||||
DWORD desired_access = GENERIC_READ | GENERIC_WRITE;
|
||||
if (access_mode == AccessMode::Read)
|
||||
desired_access = GENERIC_READ;
|
||||
else if (access_mode == AccessMode::Write)
|
||||
desired_access = GENERIC_WRITE;
|
||||
|
||||
// Allow deleting and renaming through our handle.
|
||||
desired_access |= DELETE;
|
||||
|
||||
// All sharing is allowed to more closely match default behavior on other OSes.
|
||||
constexpr DWORD share_mode = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE;
|
||||
|
||||
DWORD creation_disposition = OPEN_ALWAYS;
|
||||
if (open_mode == OpenMode::Truncate)
|
||||
creation_disposition = CREATE_ALWAYS;
|
||||
else if (open_mode == OpenMode::Create)
|
||||
creation_disposition = CREATE_NEW;
|
||||
else if (open_mode == OpenMode::Existing)
|
||||
creation_disposition = OPEN_EXISTING;
|
||||
|
||||
m_handle = CreateFile(UTF8ToTStr(path).c_str(), desired_access, share_mode, nullptr,
|
||||
creation_disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (!IsOpen())
|
||||
WARN_LOG_FMT(COMMON, "CreateFile: {}", Common::GetLastErrorString());
|
||||
|
||||
#else
|
||||
#if defined(ANDROID)
|
||||
if (IsPathAndroidContent(path))
|
||||
{
|
||||
// Android documentation says that "w" may or may not truncate.
|
||||
// In case it does, we'll use "rw" when we don't want truncation.
|
||||
// This wrongly enables read access when we don't need it.
|
||||
if (access_mode == AccessMode::Write && open_mode != OpenMode::Truncate)
|
||||
access_mode = AccessMode::ReadAndWrite;
|
||||
|
||||
std::string open_mode_str = "rw";
|
||||
if (access_mode == AccessMode::Read)
|
||||
open_mode_str = "r";
|
||||
else if (access_mode == AccessMode::Write)
|
||||
open_mode_str = "w";
|
||||
|
||||
// FYI: File::Exists can be slow on Android.
|
||||
Common::Lazy<bool> file_exists{[&] { return Exists(path); }};
|
||||
|
||||
// A few features are emulated in a non-atomic manner.
|
||||
if (open_mode == OpenMode::Existing)
|
||||
{
|
||||
if (access_mode != AccessMode::Read && !*file_exists)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (open_mode == OpenMode::Truncate)
|
||||
open_mode_str += 't';
|
||||
else if (open_mode == OpenMode::Create && *file_exists)
|
||||
return false;
|
||||
|
||||
// Modes other than `Existing` may create a file, but "r" won't do that automatically.
|
||||
if (access_mode == AccessMode::Read && !*file_exists)
|
||||
CreateEmptyFile(path);
|
||||
}
|
||||
|
||||
m_fd = OpenAndroidContent(path, open_mode_str);
|
||||
|
||||
return IsOpen();
|
||||
}
|
||||
#endif
|
||||
int flags = O_RDWR;
|
||||
if (access_mode == AccessMode::Read)
|
||||
flags = O_RDONLY;
|
||||
else if (access_mode == AccessMode::Write)
|
||||
flags = O_WRONLY;
|
||||
|
||||
if (open_mode == OpenMode::Truncate)
|
||||
flags |= O_CREAT | O_TRUNC;
|
||||
else if (open_mode == OpenMode::Create)
|
||||
flags |= O_CREAT | O_EXCL;
|
||||
else if (open_mode != OpenMode::Existing)
|
||||
flags |= O_CREAT;
|
||||
|
||||
m_fd = open(path.c_str(), flags, 0666);
|
||||
|
||||
#endif
|
||||
|
||||
return IsOpen();
|
||||
}
|
||||
|
||||
bool DirectIOFile::Close()
|
||||
{
|
||||
if (!IsOpen())
|
||||
return false;
|
||||
|
||||
m_current_offset = 0;
|
||||
|
||||
#if defined(_WIN32)
|
||||
return CloseHandle(std::exchange(m_handle, INVALID_HANDLE_VALUE)) != 0;
|
||||
#else
|
||||
return close(std::exchange(m_fd, -1)) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DirectIOFile::IsOpen() const
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
return m_handle != INVALID_HANDLE_VALUE;
|
||||
#else
|
||||
return m_fd != -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
template <auto* TransferFunc>
|
||||
static bool OverlappedTransfer(HANDLE handle, u64 offset, auto* data_ptr, u64 size)
|
||||
{
|
||||
// ReadFile/WriteFile take a 32bit size so we must loop to handle our 64bit size.
|
||||
while (true)
|
||||
{
|
||||
OVERLAPPED overlapped{};
|
||||
overlapped.Offset = DWORD(offset);
|
||||
overlapped.OffsetHigh = DWORD(offset >> 32);
|
||||
|
||||
DWORD bytes_transferred{};
|
||||
if (TransferFunc(handle, data_ptr, MathUtil::SaturatingCast<DWORD>(size), &bytes_transferred,
|
||||
&overlapped) == 0)
|
||||
{
|
||||
ERROR_LOG_FMT(COMMON, "OverlappedTransfer: {}", Common::GetLastErrorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
size -= bytes_transferred;
|
||||
|
||||
if (size == 0)
|
||||
return true;
|
||||
|
||||
offset += bytes_transferred;
|
||||
data_ptr += bytes_transferred;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool DirectIOFile::OffsetRead(u64 offset, u8* out_ptr, u64 size)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
return OverlappedTransfer<ReadFile>(m_handle, offset, out_ptr, size);
|
||||
#else
|
||||
return pread(m_fd, out_ptr, size, off_t(offset)) == ssize_t(size);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DirectIOFile::OffsetWrite(u64 offset, const u8* in_ptr, u64 size)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
return OverlappedTransfer<WriteFile>(m_handle, offset, in_ptr, size);
|
||||
#else
|
||||
return pwrite(m_fd, in_ptr, size, off_t(offset)) == ssize_t(size);
|
||||
#endif
|
||||
}
|
||||
|
||||
u64 DirectIOFile::GetSize() const
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
LARGE_INTEGER result{};
|
||||
if (GetFileSizeEx(m_handle, &result) != 0)
|
||||
return result.QuadPart;
|
||||
#else
|
||||
struct stat st{};
|
||||
if (fstat(m_fd, &st) == 0)
|
||||
return st.st_size;
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool DirectIOFile::Seek(s64 offset, SeekOrigin origin)
|
||||
{
|
||||
if (!IsOpen())
|
||||
return false;
|
||||
|
||||
u64 reference_pos = 0;
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin::Current:
|
||||
reference_pos = m_current_offset;
|
||||
break;
|
||||
case SeekOrigin::End:
|
||||
reference_pos = GetSize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Don't let our current offset underflow.
|
||||
if (offset < 0 && u64(-offset) > reference_pos)
|
||||
return false;
|
||||
|
||||
m_current_offset = reference_pos + offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DirectIOFile::Flush()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
return FlushFileBuffers(m_handle) != 0;
|
||||
#else
|
||||
return fsync(m_fd) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DirectIOFile::Swap(DirectIOFile& other)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
std::swap(m_handle, other.m_handle);
|
||||
#else
|
||||
std::swap(m_fd, other.m_fd);
|
||||
#endif
|
||||
std::swap(m_current_offset, other.m_current_offset);
|
||||
}
|
||||
|
||||
DirectIOFile DirectIOFile::Duplicate() const
|
||||
{
|
||||
DirectIOFile result;
|
||||
|
||||
if (!IsOpen())
|
||||
return result;
|
||||
|
||||
#if defined(_WIN32)
|
||||
const auto current_process = GetCurrentProcess();
|
||||
if (DuplicateHandle(current_process, m_handle, current_process, &result.m_handle, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS) == 0)
|
||||
{
|
||||
ERROR_LOG_FMT(COMMON, "DuplicateHandle: {}", Common::GetLastErrorString());
|
||||
}
|
||||
#else
|
||||
result.m_fd = dup(m_fd);
|
||||
#endif
|
||||
|
||||
ASSERT(result.IsOpen());
|
||||
|
||||
result.m_current_offset = m_current_offset;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Resize(DirectIOFile& file, u64 size)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
// This operation is not "atomic", but it's the only thing we're using the file pointer for.
|
||||
// Concurrent `Resize` would need some external synchronization to prevent race regardless.
|
||||
const LARGE_INTEGER distance{.QuadPart = LONGLONG(size)};
|
||||
return (SetFilePointerEx(file.GetHandle(), distance, nullptr, FILE_BEGIN) != 0) &&
|
||||
(SetEndOfFile(file.GetHandle()) != 0);
|
||||
#else
|
||||
return ftruncate(file.GetHandle(), off_t(size)) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Rename(DirectIOFile& file, const std::string& source_path [[maybe_unused]],
|
||||
const std::string& destination_path)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const auto dest_name = UTF8ToWString(destination_path);
|
||||
const auto dest_name_byte_size = DWORD(dest_name.size() * sizeof(WCHAR));
|
||||
FILE_RENAME_INFO info{
|
||||
.ReplaceIfExists = TRUE,
|
||||
.FileNameLength = dest_name_byte_size, // The size in bytes, not including null termination.
|
||||
};
|
||||
constexpr auto filename_struct_offset = offsetof(FILE_RENAME_INFO, FileName);
|
||||
Common::UniqueBuffer<u8> buffer(filename_struct_offset + dest_name_byte_size + sizeof(WCHAR));
|
||||
std::memcpy(buffer.data(), &info, filename_struct_offset);
|
||||
std::memcpy(buffer.data() + filename_struct_offset, dest_name.c_str(),
|
||||
dest_name_byte_size + sizeof(WCHAR));
|
||||
return SetFileInformationByHandle(file.GetHandle(), FileRenameInfo, buffer.data(),
|
||||
DWORD(buffer.size())) != 0;
|
||||
#else
|
||||
return file.IsOpen() && Rename(source_path, destination_path);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Delete(DirectIOFile& file, const std::string& filename)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
FILE_DISPOSITION_INFO info{.DeleteFile = TRUE};
|
||||
return SetFileInformationByHandle(file.GetHandle(), FileDispositionInfo, &info, sizeof(info)) !=
|
||||
0;
|
||||
#else
|
||||
return file.IsOpen() && Delete(filename, IfAbsentBehavior::NoConsoleWarning);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace File
|
||||
Reference in New Issue
Block a user