From b7cc124070514c591388845b4b972788193b7954 Mon Sep 17 00:00:00 2001 From: krzys-h Date: Sun, 6 Jan 2019 14:49:26 +0100 Subject: [PATCH] (UWP) Base StorageFile implementation --- griffin/griffin.c | 2 + .../include/vfs/vfs_implementation.h | 8 + .../vfs/vfs_implementation_uwp.cpp | 605 ++++++++++++++++++ .../RetroArch-msvc2017-UWP.vcxproj | 3 + .../RetroArch-msvc2017-UWP.vcxproj.filters | 6 + 5 files changed, 624 insertions(+) create mode 100644 libretro-common/vfs/vfs_implementation_uwp.cpp diff --git a/griffin/griffin.c b/griffin/griffin.c index d4ebac5a2b..d2c609c86f 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -926,7 +926,9 @@ FILE #include "../libretro-common/streams/file_stream_transforms.c" #include "../libretro-common/streams/interface_stream.c" #include "../libretro-common/streams/memory_stream.c" +#ifndef __WINRT__ #include "../libretro-common/vfs/vfs_implementation.c" +#endif #include "../list_special.c" #include "../libretro-common/string/stdstring.c" #include "../libretro-common/file/nbio/nbio_stdio.c" diff --git a/libretro-common/include/vfs/vfs_implementation.h b/libretro-common/include/vfs/vfs_implementation.h index afc2e22213..9f49f2640d 100644 --- a/libretro-common/include/vfs/vfs_implementation.h +++ b/libretro-common/include/vfs/vfs_implementation.h @@ -44,6 +44,10 @@ typedef struct retro_vfs_dir_handle libretro_vfs_implementation_dir; typedef struct libretro_vfs_implementation_dir libretro_vfs_implementation_dir; #endif +#ifdef __cplusplus +extern "C" { +#endif + libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, unsigned mode, unsigned hints); int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream); @@ -84,4 +88,8 @@ bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *dirstream); int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *dirstream); +#ifdef __cplusplus +} +#endif + #endif diff --git a/libretro-common/vfs/vfs_implementation_uwp.cpp b/libretro-common/vfs/vfs_implementation_uwp.cpp new file mode 100644 index 0000000000..5293a13f41 --- /dev/null +++ b/libretro-common/vfs/vfs_implementation_uwp.cpp @@ -0,0 +1,605 @@ +/* Copyright (C) 2018-2019 The RetroArch team +* +* --------------------------------------------------------------------------------------- +* The following license statement only applies to this file (vfs_implementation_uwp.cpp). +* --------------------------------------------------------------------------------------- +* +* 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. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace Windows::Storage::FileProperties; + +#ifdef RARCH_INTERNAL +#ifndef VFS_FRONTEND +#define VFS_FRONTEND +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + /* Dear Microsoft + * I really appreciate all the effort you took to not provide any + * synchronous file APIs and block all attempts to synchronously + * wait for the results of async tasks for "smooth user experience", + * but I'm not going to run and rewrite all RetroArch cores for + * async I/O. I hope you like this hack I made instead. + */ + template + T RunAsync(std::function()> func) + { + bool finished = false; + Platform::Exception^ exception = nullptr; + T result; + func().then([&](concurrency::task t) { + try + { + result = t.get(); + } + catch (Platform::Exception^ exception_) + { + exception = exception_; + } + finished = true; + }); + + while (!finished) + Windows::UI::Core::CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent); + + if (exception != nullptr) + throw exception; + return result; + } + + template + T RunAsyncAndCatchErrors(std::function()> func, T valueOnError) + { + try + { + return RunAsync(func); + } + catch (Platform::Exception^ e) + { + return valueOnError; + } + } + + + void windowsize_path(wchar_t* path) + { + /* UWP deals with paths containing / instead of \ way worse than normal Windows */ + /* and RetroArch may sometimes mix them (e.g. on archive extraction) */ + if (!path) + return; + while (*path) + { + if (*path == '/') + *path = '\\'; + ++path; + } + } +} + +#ifdef VFS_FRONTEND +struct retro_vfs_file_handle +#else +struct libretro_vfs_implementation_file +#endif +{ + IRandomAccessStream^ fp; + char* orig_path; +}; + +libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, unsigned mode, unsigned hints) +{ + if (!path || !*path) + return NULL; + + if (!path_is_absolute(path)) + { + RARCH_WARN("Something tried to access files from current directory ('%s'). This is not allowed on UWP.\n", path); + return NULL; + } + + if (path_char_is_slash(path[strlen(path) - 1])) + { + RARCH_WARN("Trying to open a directory as file?! ('%s')\n", path); + return NULL; + } + + char* dirpath = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_basedir(dirpath, path, PATH_MAX_LENGTH); + wchar_t *dirpath_wide = utf8_to_utf16_string_alloc(dirpath); + windowsize_path(dirpath_wide); + Platform::String^ dirpath_str = ref new Platform::String(dirpath_wide); + free(dirpath_wide); + free(dirpath); + + char* filename = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(filename, path, PATH_MAX_LENGTH); + wchar_t *filename_wide = utf8_to_utf16_string_alloc(filename); + Platform::String^ filename_str = ref new Platform::String(filename_wide); + free(filename_wide); + free(filename); + + retro_assert(!dirpath_str->IsEmpty() && !filename_str->IsEmpty()); + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(dirpath_str)).then([&](StorageFolder^ dir) { + if (mode == RETRO_VFS_FILE_ACCESS_READ) + return dir->GetFileAsync(filename_str); + else + return dir->CreateFileAsync(filename_str, (mode & RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING) != 0 ? + CreationCollisionOption::OpenIfExists : CreationCollisionOption::ReplaceExisting); + }).then([&](StorageFile^ file) { + FileAccessMode accessMode = mode == RETRO_VFS_FILE_ACCESS_READ ? + FileAccessMode::Read : FileAccessMode::ReadWrite; + return file->OpenAsync(accessMode); + }).then([&](IRandomAccessStream^ fpstream) { + libretro_vfs_implementation_file *stream = (libretro_vfs_implementation_file*)calloc(1, sizeof(*stream)); + if (!stream) + return (libretro_vfs_implementation_file*)NULL; + + stream->orig_path = strdup(path); + stream->fp = fpstream; + stream->fp->Seek(0); + return stream; + }); + }, NULL); +} + +int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + + /* Apparently, this is how you close a file in WinRT */ + /* Yes, really */ + stream->fp = nullptr; + + return 0; +} + +int retro_vfs_file_error_impl(libretro_vfs_implementation_file *stream) +{ + return false; /* TODO */ +} + +int64_t retro_vfs_file_size_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return 0; + return stream->fp->Size; +} + +int64_t retro_vfs_file_truncate_impl(libretro_vfs_implementation_file *stream, int64_t length) +{ + if (!stream || !stream->fp) + return -1; + stream->fp->Size = length; + return 0; +} + +int64_t retro_vfs_file_tell_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + return stream->fp->Position; +} + +int64_t retro_vfs_file_seek_impl(libretro_vfs_implementation_file *stream, int64_t offset, int seek_position) +{ + if (!stream || !stream->fp) + return -1; + + switch (seek_position) + { + case RETRO_VFS_SEEK_POSITION_START: + stream->fp->Seek(offset); + break; + + case RETRO_VFS_SEEK_POSITION_CURRENT: + stream->fp->Seek(stream->fp->Position + offset); + break; + + case RETRO_VFS_SEEK_POSITION_END: + stream->fp->Seek(stream->fp->Size - offset); + break; + } + + return 0; +} + +/* This is some pure magic and I have absolutely no idea how it works */ +/* Wraps a raw buffer into a WinRT object */ +/* https://stackoverflow.com/questions/10520335/how-to-wrap-a-char-buffer-in-a-winrt-ibuffer-in-c */ +class NativeBuffer : + public Microsoft::WRL::RuntimeClass, + ABI::Windows::Storage::Streams::IBuffer, + Windows::Storage::Streams::IBufferByteAccess> +{ +public: + virtual ~NativeBuffer() + { + } + + HRESULT __stdcall RuntimeClassInitialize(byte *buffer, uint32_t capacity, uint32_t length) + { + m_buffer = buffer; + m_capacity = capacity; + m_length = length; + return S_OK; + } + + HRESULT __stdcall Buffer(byte **value) + { + if (m_buffer == nullptr) + return E_INVALIDARG; + *value = m_buffer; + return S_OK; + } + + HRESULT __stdcall get_Capacity(uint32_t *value) + { + *value = m_capacity; + return S_OK; + } + + HRESULT __stdcall get_Length(uint32_t *value) + { + *value = m_length; + return S_OK; + } + + HRESULT __stdcall put_Length(uint32_t value) + { + if (value > m_capacity) + return E_INVALIDARG; + m_length = value; + return S_OK; + } + +private: + byte *m_buffer; + uint32_t m_capacity; + uint32_t m_length; +}; + +IBuffer^ CreateNativeBuffer(void* buf, uint32_t capacity, uint32_t length) +{ + Microsoft::WRL::ComPtr nativeBuffer; + Microsoft::WRL::Details::MakeAndInitialize(&nativeBuffer, (byte *)buf, capacity, length); + auto iinspectable = (IInspectable *)reinterpret_cast(nativeBuffer.Get()); + IBuffer ^buffer = reinterpret_cast(iinspectable); + return buffer; +} + +int64_t retro_vfs_file_read_impl(libretro_vfs_implementation_file *stream, void *s, uint64_t len) +{ + if (!stream || !stream->fp || !s) + return -1; + + IBuffer^ buffer = CreateNativeBuffer(s, len, 0); + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(stream->fp->ReadAsync(buffer, buffer->Capacity, InputStreamOptions::None)).then([&](IBuffer^ buf) { + retro_assert(buf == buffer); + return (int64_t)buffer->Length; + }); + }, -1); +} + +int64_t retro_vfs_file_write_impl(libretro_vfs_implementation_file *stream, const void *s, uint64_t len) +{ + if (!stream || !stream->fp || !s) + return -1; + + IBuffer^ buffer = CreateNativeBuffer(const_cast(s), len, len); + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(stream->fp->WriteAsync(buffer)).then([&](unsigned int written) { + return (int64_t)written; + }); + }, -1); +} + +int retro_vfs_file_flush_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(stream->fp->FlushAsync()).then([&](bool this_value_is_not_even_documented_wtf) { + /* The bool value may be reporting an error or something, but just leave it alone for now */ + /* https://github.com/MicrosoftDocs/winrt-api/issues/841 */ + return 0; + }); + }, -1); +} + +int retro_vfs_file_remove_impl(const char *path) +{ + if (!path || !*path) + return -1; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFile::GetFileFromPathAsync(path_str)).then([&](StorageFile^ file) { + return file->DeleteAsync(StorageDeleteOption::PermanentDelete); + }).then([&]() { + return 0; + }); + }, -1); +} + +/* TODO: this may not work if trying to move a directory */ +int retro_vfs_file_rename_impl(const char *old_path, const char *new_path) +{ + if (!old_path || !*old_path || !new_path || !*new_path) + return -1; + + wchar_t* old_path_wide = utf8_to_utf16_string_alloc(old_path); + Platform::String^ old_path_str = ref new Platform::String(old_path_wide); + free(old_path_wide); + + char* new_dir_path = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_basedir(new_dir_path, new_path, PATH_MAX_LENGTH); + wchar_t *new_dir_path_wide = utf8_to_utf16_string_alloc(new_dir_path); + windowsize_path(new_dir_path_wide); + Platform::String^ new_dir_path_str = ref new Platform::String(new_dir_path_wide); + free(new_dir_path_wide); + free(new_dir_path); + + char* new_file_name = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(new_file_name, new_path, PATH_MAX_LENGTH); + wchar_t *new_file_name_wide = utf8_to_utf16_string_alloc(new_file_name); + Platform::String^ new_file_name_str = ref new Platform::String(new_file_name_wide); + free(new_file_name_wide); + free(new_file_name); + + retro_assert(!old_path_str->IsEmpty() && !new_dir_path_str->IsEmpty() && !new_file_name_str->IsEmpty()); + + return RunAsyncAndCatchErrors([&]() { + concurrency::task old_file_task = concurrency::create_task(StorageFile::GetFileFromPathAsync(old_path_str)); + concurrency::task new_dir_task = concurrency::create_task(StorageFolder::GetFolderFromPathAsync(new_dir_path_str)); + return concurrency::create_task([&] { + /* Run these two tasks in parallel */ + /* TODO: There may be some cleaner way to express this */ + concurrency::task_group group; + group.run([&] { return old_file_task; }); + group.run([&] { return new_dir_task; }); + group.wait(); + }).then([&]() { + return old_file_task.get()->MoveAsync(new_dir_task.get(), new_file_name_str, NameCollisionOption::ReplaceExisting); + }).then([&]() { + return 0; + }); + }, -1); +} + +const char *retro_vfs_file_get_path_impl(libretro_vfs_implementation_file *stream) +{ + /* should never happen, do something noisy so caller can be fixed */ + if (!stream) + abort(); + return stream->orig_path; +} + + +/* This is ugly, but I can't figure out a better way and there may be no better way... */ +static IStorageItem^ GetFileOrFolderFromPath(Platform::String^ path) +{ + if (!path || path->IsEmpty()) + return nullptr; + + if (*(path->End() - 1) == '\\') + { + /* Ends with a slash, so it's definitely a directory */ + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); + }, nullptr); + } + else + { + /* No final slash - probably a file (since RetroArch usually slash-terminates dirs), but there is a chance it's a directory */ + IStorageItem^ item; + item = RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFile::GetFileFromPathAsync(path)); + }, nullptr); + if (!item) + { + item = RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); + }, nullptr); + } + return item; + } +} + +int retro_vfs_stat_impl(const char *path, int32_t *size) +{ + if (!path || !*path) + return 0; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + IStorageItem^ item = GetFileOrFolderFromPath(path_str); + if (!item) + return 0; + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(item->GetBasicPropertiesAsync()).then([&](BasicProperties^ properties) { + if (size) + *size = properties->Size; + return item->IsOfType(StorageItemTypes::Folder) ? RETRO_VFS_STAT_IS_VALID | RETRO_VFS_STAT_IS_DIRECTORY : RETRO_VFS_STAT_IS_VALID; + }); + }, 0); +} + +int retro_vfs_mkdir_impl(const char *dir) +{ + if (!dir || !*dir) + return -1; + + char* dir_local = strdup(dir); + /* If the path ends with a slash, we have to remove it for basename to work */ + char* tmp = dir_local + strlen(dir_local) - 1; + if (path_char_is_slash(*tmp)) + *tmp = 0; + + char* dir_name = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(dir_name, dir_local, PATH_MAX_LENGTH); + wchar_t *dir_name_wide = utf8_to_utf16_string_alloc(dir_name); + Platform::String^ dir_name_str = ref new Platform::String(dir_name_wide); + free(dir_name_wide); + free(dir_name); + + char* parent_path = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_parent_dir(parent_path, dir_local, PATH_MAX_LENGTH); + wchar_t *parent_path_wide = utf8_to_utf16_string_alloc(parent_path); + windowsize_path(parent_path_wide); + Platform::String^ parent_path_str = ref new Platform::String(parent_path_wide); + free(parent_path_wide); + free(parent_path); + + retro_assert(!dir_name_str->IsEmpty() && !parent_path_str->IsEmpty()); + + free(dir_local); + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(parent_path_str)).then([&](StorageFolder^ parent) { + return parent->CreateFolderAsync(dir_name_str); + }).then([&](concurrency::task new_dir) { + try + { + new_dir.get(); + } + catch (Platform::COMException^ e) + { + if (e->HResult == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) + return -2; + throw; + } + return 0; + }); + }, -1); +} + +#ifdef VFS_FRONTEND +struct retro_vfs_dir_handle +#else +struct libretro_vfs_implementation_dir +#endif +{ + IVectorView^ directory; + IIterator^ entry; + char *entry_name; +}; + +libretro_vfs_implementation_dir *retro_vfs_opendir_impl(const char *name, bool include_hidden) +{ + libretro_vfs_implementation_dir *rdir; + + if (!name || !*name) + return NULL; + + rdir = (libretro_vfs_implementation_dir*)calloc(1, sizeof(*rdir)); + if (!rdir) + return NULL; + + wchar_t *name_wide = utf8_to_utf16_string_alloc(name); + windowsize_path(name_wide); + Platform::String^ name_str = ref new Platform::String(name_wide); + free(name_wide); + + rdir->directory = RunAsyncAndCatchErrors^>([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(name_str)).then([&](StorageFolder^ folder) { + return folder->GetItemsAsync(); + }); + }, nullptr); + + if (rdir->directory) + return rdir; + + free(rdir); + return NULL; +} + +bool retro_vfs_readdir_impl(libretro_vfs_implementation_dir *rdir) +{ + if (!rdir->entry) + { + rdir->entry = rdir->directory->First(); + return rdir->entry->HasCurrent; + } + else + { + return rdir->entry->MoveNext(); + } +} + +const char *retro_vfs_dirent_get_name_impl(libretro_vfs_implementation_dir *rdir) +{ + if (rdir->entry_name) + free(rdir->entry_name); + rdir->entry_name = utf16_to_utf8_string_alloc(rdir->entry->Current->Name->Data()); + return rdir->entry_name; +} + +bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *rdir) +{ + return rdir->entry->Current->IsOfType(StorageItemTypes::Folder); +} + +int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *rdir) +{ + if (!rdir) + return -1; + + if (rdir->entry_name) + free(rdir->entry_name); + rdir->entry = nullptr; + rdir->directory = nullptr; + + free(rdir); + return 0; +} diff --git a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj index 1f21ca7a6c..4d8b42ab77 100644 --- a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj +++ b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj @@ -282,6 +282,9 @@ + + + diff --git a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters index 3fcb574ea4..dd9507e079 100644 --- a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters +++ b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters @@ -10,6 +10,9 @@ {c3155604-6d38-494a-bfe0-861cef871cb2} + + {d41660c5-7f5b-442c-b5d7-03e6e9af8172} + @@ -40,6 +43,9 @@ griffin + + libretro-common-uwp +