9

I know I can use Win32 APIs for accessing files within my own local data folder (eg, see this answered question) but I need to access files outside of my app (eg, from the Pictures Library) and the libraries I'm trying to use are all based on Win32 file HANDLEs and / or they rely on using relative filenames.

Since the only way to get at files in the Pictures Library (or to get files / folders returned from a picker) is via StorageFile objects, how can I re-use my existing code? Do I have to re-write it all to be asynchronous and rely on the WinRT storage APIs?

Community
  • 1
  • 1
Peter Torr - MSFT
  • 11,824
  • 3
  • 18
  • 51

1 Answers1

21

Starting in the "Anniversary Update" (aka "RS1" or build 10.0.14393) you are able to get a Win32 HANDLE from a StorageItem (file or folder) and to create new named files (returning a HANDLE) from within a StorageFolder. You do this using the new IStorageFolderHandleAccess and IStorageItemHandleAccess APIs.

Note: These APIs have been accidentally placed inside the WINAPI_PARTITION_DESKTOP partition (they're not desktop-specific; they're available to UWPs). This will be addressed in future SDK updates.

To use one of these new COM interfaces, you simply QI the StorageFile or StorageFolder for the interface. If the interface isn't supported, it means your app is running on a down-level OS (or perhaps the Storage Item isn't actually backed by a real file, but is rather a pseudo-file). You can use these interfaces from C++ (C++/CX or WRL) or from C#.

Here's a simple example of using a FolderPicker to have the user pick a location on their disk (which returns a brokered StorageFolder object) and then using Win32 APIs ReadFile and WriteFile to read and write a file from that location.

As noted above, we have to copy the declarations for the interface into our own code because the real SDK versions are in the wrong API partition. (I would advise against modifying the SDK files to solve the problem). So first up is our own header file, eg StorageHandleAccess.h, that copies the declarations from the SDK file WindowsStorageCOM.h:

#pragma once

// These are copied from WindowsStorageCOM.h
// You can remove this header file once the real file has been updated
// to fix the WINAPI_PARTITION_DESKTOP block

typedef interface IOplockBreakingHandler IOplockBreakingHandler;
typedef interface IStorageItemHandleAccess IStorageItemHandleAccess;
typedef interface IStorageFolderHandleAccess IStorageFolderHandleAccess;

#ifdef __cplusplus
extern "C" {
#endif 

  typedef /* [v1_enum] */
    enum HANDLE_OPTIONS
  {
    HO_NONE = 0,
    HO_OPEN_REQUIRING_OPLOCK = 0x40000,
    HO_DELETE_ON_CLOSE = 0x4000000,
    HO_SEQUENTIAL_SCAN = 0x8000000,
    HO_RANDOM_ACCESS = 0x10000000,
    HO_NO_BUFFERING = 0x20000000,
    HO_OVERLAPPED = 0x40000000,
    HO_WRITE_THROUGH = 0x80000000
  }     HANDLE_OPTIONS;

  DEFINE_ENUM_FLAG_OPERATORS(HANDLE_OPTIONS);
  typedef /* [v1_enum] */
    enum HANDLE_ACCESS_OPTIONS
  {
    HAO_NONE = 0,
    HAO_READ_ATTRIBUTES = 0x80,
    HAO_READ = 0x120089,
    HAO_WRITE = 0x120116,
    HAO_DELETE = 0x10000
  }     HANDLE_ACCESS_OPTIONS;

  DEFINE_ENUM_FLAG_OPERATORS(HANDLE_ACCESS_OPTIONS);
  typedef /* [v1_enum] */
    enum HANDLE_SHARING_OPTIONS
  {
    HSO_SHARE_NONE = 0,
    HSO_SHARE_READ = 0x1,
    HSO_SHARE_WRITE = 0x2,
    HSO_SHARE_DELETE = 0x4
  }     HANDLE_SHARING_OPTIONS;

  DEFINE_ENUM_FLAG_OPERATORS(HANDLE_SHARING_OPTIONS);
  typedef /* [v1_enum] */
    enum HANDLE_CREATION_OPTIONS
  {
    HCO_CREATE_NEW = 0x1,
    HCO_CREATE_ALWAYS = 0x2,
    HCO_OPEN_EXISTING = 0x3,
    HCO_OPEN_ALWAYS = 0x4,
    HCO_TRUNCATE_EXISTING = 0x5
  }     HANDLE_CREATION_OPTIONS;


  EXTERN_C const IID IID_IOplockBreakingHandler;
  MIDL_INTERFACE("826ABE3D-3ACD-47D3-84F2-88AAEDCF6304")
    IOplockBreakingHandler : public IUnknown
  {
  public:
      virtual HRESULT STDMETHODCALLTYPE OplockBreaking(void) = 0;

  };

  EXTERN_C const IID IID_IStorageItemHandleAccess;
  MIDL_INTERFACE("5CA296B2-2C25-4D22-B785-B885C8201E6A")
    IStorageItemHandleAccess : public IUnknown
  {
  public:
      virtual HRESULT STDMETHODCALLTYPE Create(
        /* [in] */ HANDLE_ACCESS_OPTIONS accessOptions,
        /* [in] */ HANDLE_SHARING_OPTIONS sharingOptions,
        /* [in] */ HANDLE_OPTIONS options,
        /* [optional][in] */ __RPC__in_opt IOplockBreakingHandler *oplockBreakingHandler,
        /* [system_handle][retval][out] */ __RPC__deref_out_opt HANDLE *interopHandle) = 0;

  };

  EXTERN_C const IID IID_IStorageFolderHandleAccess;
  MIDL_INTERFACE("DF19938F-5462-48A0-BE65-D2A3271A08D6")
    IStorageFolderHandleAccess : public IUnknown
  {
  public:
      virtual HRESULT STDMETHODCALLTYPE Create(
        /* [string][in] */ __RPC__in_string LPCWSTR fileName,
        /* [in] */ HANDLE_CREATION_OPTIONS creationOptions,
        /* [in] */ HANDLE_ACCESS_OPTIONS accessOptions,
        /* [in] */ HANDLE_SHARING_OPTIONS sharingOptions,
        /* [in] */ HANDLE_OPTIONS options,
        /* [optional][in] */ __RPC__in_opt IOplockBreakingHandler *oplockBreakingHandler,
        /* [system_handle][retval][out] */ __RPC__deref_out_opt HANDLE *interopHandle) = 0;

  };
#ifdef __cplusplus
}
#endif

Next up is a simple usage of the API. This example takes a StorageFolder, a filename, and a creation flag (open or create) and tries to open (or create) the named file, reads (or writes) some text from (to) the file, and writes some output to the Debug console.

The code isn't particularly useful in a real-world setting, but illustrates how to use the API. This can be used in a blank C++ XAML project to replace the MainPage.xaml.cpp file (you should only need to update the namespace):

#include "pch.h"
#include "MainPage.xaml.h"
#include <ppltasks.h>

// TODO: Replace with your namespace
#error Replace this with your real namespace
using namespace FileHandleFromStorageFolder;

// Uncomment out this line and delete the next line once the SDK is fixed
//#include <WindowsStorageCOM.h>
#include "StorageHandleAccess.h"

// For ComPtr<>
#include <wrl\client.h>

// For HandleT<>
#include <wrl\wrappers\corewrappers.h>

__declspec(noreturn) inline void ThrowWithHRESULT(HRESULT hr, const wchar_t* message)
{
  using namespace Platform;
  throw ref new Exception(hr, ref new String(message));
}

__declspec(noreturn) inline void ThrowWithGetLastError(const wchar_t* message)
{
  using namespace Platform;
  throw ref new Exception(HRESULT_FROM_WIN32(GetLastError()), ref new String(message));
}

// Test is a simple test function. Pass in one of the HANDLE_CREATION_OPTIONS values
// (eg, HCO_CREATE_ALWAYS or HCO_OPEN_ALWAYS) and the function will try and either
// write to the file (if it's empty) or read from it (if it's not).
void Test(Windows::Storage::StorageFolder^ folder, const wchar_t* filename, HANDLE_CREATION_OPTIONS options)
{
  using namespace Microsoft::WRL;
  using namespace Microsoft::WRL::Wrappers;

  // Get an IUnknown from the ref class, and then QI for IStorageFolderHandleAccess
  ComPtr<IUnknown> abiPointer(reinterpret_cast<IUnknown*>(folder));
  ComPtr<IStorageFolderHandleAccess> handleAccess;
  HRESULT hr = abiPointer.As(&handleAccess);
  if (FAILED(hr))
    ThrowWithHRESULT(hr, L"Can't QI");

  // Standard RAII wrapper for HANDLEs that represent files
  HandleT<HandleTraits::FileHandleTraits>win32fileHandle;

  // This is roughly equivalent of calling CreateFile2 
  hr = handleAccess->Create(filename, options,
    HANDLE_ACCESS_OPTIONS::HAO_WRITE | HANDLE_ACCESS_OPTIONS::HAO_READ,
    HANDLE_SHARING_OPTIONS::HSO_SHARE_NONE, HANDLE_OPTIONS::HO_NONE, nullptr,
    win32fileHandle.GetAddressOf());
  if (FAILED(hr))
    ThrowWithHRESULT(hr, L"Can't access file");

  // From here, it's standard Win32 code - nothing WinRT specific at all
  LARGE_INTEGER size{ 0 };
  if (FALSE == GetFileSizeEx(win32fileHandle.Get(), &size))
    ThrowWithGetLastError(L"Can't get file size");

  static const DWORD BUFFER_SIZE = 500;
  char buffer[BUFFER_SIZE];
  DWORD bytesUsed{ 0 };

  if (size.QuadPart == 0)
  {
    const static auto str = "Hello, world\r\n";
    if (FALSE == WriteFile(win32fileHandle.Get(), str, strlen(str), &bytesUsed, nullptr))
      ThrowWithGetLastError(L"Can't write to file");

    sprintf_s(buffer, ARRAYSIZE(buffer), "Wrote %d bytes to file\r\n", bytesUsed);
    OutputDebugStringA(buffer);
  }
  else
  {
    if (FALSE == ReadFile(win32fileHandle.Get(), buffer, ARRAYSIZE(buffer) - 1, &bytesUsed, nullptr))
      ThrowWithGetLastError(L"Can't read from file");

    buffer[bytesUsed] = 0;
    OutputDebugStringA(buffer);
  }
}

// Trivial driver that gets a StorageFolder and then creates a file
// inside it, writes some text, then re-opens it to read text.
void TestWrapper()
{
  using namespace Windows::Storage;
  using namespace Windows::Storage::Pickers;

  auto picker = ref new FolderPicker();
  picker->FileTypeFilter->Append(L".txt");
  picker->SuggestedStartLocation = PickerLocationId::Desktop;
  concurrency::create_task(picker->PickSingleFolderAsync()).then([]
  (StorageFolder^ folder)
  {
    if (folder != nullptr)
    {
      // Create and then read back a simple file
      Test(folder, L"win32handletest.txt", HANDLE_CREATION_OPTIONS::HCO_CREATE_ALWAYS);
      Test(folder, L"win32handletest.txt", HANDLE_CREATION_OPTIONS::HCO_OPEN_ALWAYS);
    }
  }
  );
}

MainPage::MainPage()
{
  InitializeComponent();
  TestWrapper();
}
Peter Torr - MSFT
  • 11,824
  • 3
  • 18
  • 51
  • I have one question: Is this applicable for ANY folders picked by folder picker? or only folders that enabled by AppX manifest? In my understanding, File access of UWP app is brokered by OS process if the file is outside of app specific folder. But, your code seems that achieve to do the native access. – Mamoru Satoh Mar 15 '17 at 08:38
  • 1
    Yes, this will work for any file or folder that you get from the system. Access has already been brokered and this API simply gets the underlying handle. – Peter Torr - MSFT Mar 15 '17 at 14:22
  • 1
    Wow, I could repro it with your code - it works with ANY folders that picked by picker. It seems that I should change my understanding about the UWP sandbox. It's a hidden gem of AU SDK :) Thank you for sharing the info. – Mamoru Satoh Mar 15 '17 at 16:44
  • I which we could get handle to existing folders with this method. Passing name of an existing (sub)folder to the IStorageFolderHandleAccess::Create method returns E_ACCESSDENIED unless the folder is in application data. – Mehrzad Chehraz Jun 12 '17 at 15:58
  • @PeterTorr-MSFT, a lot of third party libraries rely on being passed a file path (and then they internally use fopen). Is there any mechanism for allowing this, after a user has picked a file? e.g. pick a file then use libpng / libjpeg to open it via path char*? – Mark Ingram Jun 15 '18 at 15:55
  • The basics are in the platform but currently it requires the library to rebuild with new APIs. We're hoping to improve that though. – Peter Torr - MSFT Jun 15 '18 at 21:20
  • @PeterTorr-MSFT Are only Read, Write, Delete and Read Attribute flags supported for handle access?? Is there any way to get only Set Attribute access, I am providing my users to be able to change Read Only flag attribute in UWP app. – Soumya Mahunt Jan 19 '21 at 03:44
  • One option is to use `CreateFileFromApp` which should support Al modes. – Peter Torr - MSFT Jan 21 '21 at 08:25
  • @PeterTorr-MSFT I tried using `CreateFileFromApp`, while most cases it worked, I wasn't able to get handle for restricted filetypes (.bat, .cmd etc.) even if my app already has access to the file (file opened from picker). But if I use the above method I was able to get handle for these types of files. Is this intended behavior or a bug?? – Soumya Mahunt May 01 '21 at 12:03
  • I can't tell if you're saying it works or not but `StorageFile` should hide those files but `CreateFileFromApp` should not. – Peter Torr - MSFT May 06 '21 at 22:19