FileSavePicker on IL2CPP build

Hello guys!
need your help,
I want to open FileSavePicker on Windows Store build, made with il2cpp scripting backend. I know that it is currently unavailable to do right from Unity, and should be possible in 5.6,
but we definitely can do that from DLL or so, but I have not succeed with that.

My last approach was to create Win32 Api DLL, with method kinda like that:

#include <windows.h>
#include <shobjidl.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
        COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        IFileOpenDialog *pFileOpen;

        // Create the FileOpenDialog object.
        hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
                IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));

        if (SUCCEEDED(hr))
        {
            // Show the Open dialog box.
            hr = pFileOpen->Show(NULL);

            // Get the file name from the dialog box.
            if (SUCCEEDED(hr))
            {
                IShellItem *pItem;
                hr = pFileOpen->GetResult(&pItem);
                if (SUCCEEDED(hr))
                {
                    PWSTR pszFilePath;
                    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                    // Display the file name to the user.
                    if (SUCCEEDED(hr))
                    {
                        MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
                        CoTaskMemFree(pszFilePath);
                    }
                    pItem->Release();
                }
            }
            pFileOpen->Release();
        }
        CoUninitialize();
    }
    return 0;
}

This one works fine if we call that method from C# on Standalone or in Editor, but not on WSA, so Im wondering why it is not working.

Does WSA has another way to call those windows, or it is Unity that blocks it? Does anybody know?

Thanks for help!

Hey,

On WSA, CoCreateInstance is not available and trying to activate objects using it will fail. The API you’re trying to use is for desktop applications only:

On WSA, you have to use another API - one from Windows.Storage.Pickers namespace:

Unfortunately the examples in their documentation are all in C#. Using it from C++ is very similar to using IFileOpenDialog: include “<windows.storage.pickers.h>”, and use ABI::Windows::Storage::Pickers::IFileOpenPicker interface. The only notable difference is that you have to use “RoActivateInstance” function instead of “CoCreateInstance” function to create an instance of picker object. If you have any troubles with making it work, let me know.

Hey Tautvydas,
thanks for response!

Spent half of the day, trying to figure out things, based on your reply, but unfortunately, I have not succeed again.

I have rewritten my code, it goes without errors, and returns success value(55, just a random number, for me to know), but File Picker isn’t shown neither on Surface, nor on Visual Studios Windows Store build. Really dunno why.

        HRESULT hr;
        ComPtr<IInspectable> basePicker;
        HSTRING pickerType = HString::MakeReference(RuntimeClass_Windows_Storage_Pickers_FileOpenPicker).Get();
        ComPtr<IFileOpenPicker> picker;
        
        hr = RoActivateInstance(pickerType, &basePicker);

        if (FAILED(hr))
        {
            return 0;
        }

        hr = basePicker.Get()->QueryInterface(IID_PPV_ARGS(&picker));

        if (FAILED(hr))
        {
            return 1;
        }

        if (FAILED(hr))
        {
            return 2;
        }
        else
        {
            picker->put_ViewMode(PickerViewMode_Thumbnail);
            picker->put_SuggestedStartLocation(PickerLocationId::PickerLocationId_Desktop);

            ComPtr<IVector<HSTRING>> filters;
            hr = picker->get_FileTypeFilter(&filters);

            filters->Clear();
            hr = filters->Append(HString::MakeReference(L"*").Get());

            const char * m_pickerID = "ID_12345";

            ComPtr<IAsyncOperation<StorageFile *>> op;

            picker->PickSingleFileAsync(&op);

            return 55;
        }

Hey, I’m impressed you were able to figure out that much actually :). You’re very close to making it work!

Anyway, there are a couple of issues with the code:

HSTRING pickerType = HString::MakeReference(RuntimeClass_Windows_Storage_Pickers_FileOpenPicker).Get();

This line creates a temporary string, then retrieves a pointer to it. This means that after that line, the actual string data will no longer valid memory, and you’ve just created a dangling pointer. Do this, instead:

HStringReference pickerType(RuntimeClass_Windows_Storage_Pickers_FileOpenPicker);

Secondly, you’re missing some HRESULT checks, especially on the main call - PickSingleFileAsync.

Thirdly, you have no callback attached to the operation, so even if user picks a file, you’ll not get the result.

However, even without fixing those issues, the code worked for me - I got a FileOpenDialog popup when running it on an empty VS project. Are you calling it on the correct thread? Adding missing HRESULT checks will perhaps help in figuring out what’s wrong.

Here’s the whole fixed code for you:

#include <inspectable.h>
#include <windows.storage.pickers.h>
#include <wrl.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Storage;
using namespace ABI::Windows::Storage::Pickers;

#define AssertAndReturnIfFailed(hr) do { if (FAILED(hr)) { __debugbreak(); return hr; } } while (false);

HRESULT PickFile()
{
   ComPtr<IInspectable> basePicker;
   auto hr = RoActivateInstance(HStringReference(RuntimeClass_Windows_Storage_Pickers_FileOpenPicker).Get(), &basePicker);
   AssertAndReturnIfFailed(hr);

   ComPtr<IFileOpenPicker> picker;
   hr = basePicker.As(&picker);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_ViewMode(PickerViewMode_Thumbnail);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_SuggestedStartLocation(PickerLocationId::PickerLocationId_Desktop);
   AssertAndReturnIfFailed(hr);

   ComPtr<IVector<HSTRING>> filters;
   hr = picker->get_FileTypeFilter(&filters);
   AssertAndReturnIfFailed(hr);

   hr = filters->Clear();
   AssertAndReturnIfFailed(hr);

   hr = filters->Append(HString::MakeReference(L"*").Get());
   AssertAndReturnIfFailed(hr);

   const char * m_pickerID = "ID_12345";

   ComPtr<IAsyncOperation<StorageFile*>> op;

   hr = picker->PickSingleFileAsync(&op);
   AssertAndReturnIfFailed(hr);

   hr = op->put_Completed(Callback<IAsyncOperationCompletedHandler<StorageFile*>>([](IAsyncOperation<StorageFile*>* operation, AsyncStatus status) -> HRESULT
   {
     if (status != AsyncStatus::Completed)
     {
       // Figure out the error

       ComPtr<IAsyncInfo> asyncInfo;
       auto hr = operation->QueryInterface(IID_PPV_ARGS(&asyncInfo));
       AssertAndReturnIfFailed(hr);

       HRESULT errorCode;
       hr = asyncInfo->get_ErrorCode(&errorCode);
       AssertAndReturnIfFailed(hr);

       // Do something with errorCode
       // ...
       // ...

       return S_OK;
     }

     ComPtr<IStorageFile> storageFile;
     auto hr = operation->GetResults(&storageFile);
     AssertAndReturnIfFailed(hr);

     // Hooray, file got picked. Do something with storageFile.
     // ...
     // ...

     return S_OK;
   }).Get());
   AssertAndReturnIfFailed(hr);

   return S_OK;
}

Hey once again, thx a lot for help and reply, still not working, but would be figuring out right now, whats wrong, mb will have some more info.

One question, regarding your answer, what you mean by “calling it on the correct thread” ?

Im just trying to Open that dll method, on simple Unity’s OnButtonClick. However, I have started to assume, that I was needed to do that in UI thread, or so, but it is not possible on IL2CPP build, as it doesn’t have async/await operators.

Yeah, it will not work from OnButtonClick callback. You have to run it on the UI thread.

You don’t need async/await operators to execute stuff on the UI thread. You can use this on IL2CPP too:

https://docs.unity3d.com/ScriptReference/WSA.Application.InvokeOnUIThread.html

Alternatively, it’s also possible to put stuff on UI thread from C++ by getting MainView from CoreApplication:

https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication#Windows_ApplicationModel_Core_CoreApplication_MainView

Then getting its Dispatcher:

https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplicationview#Windows_ApplicationModel_Core_CoreApplicationView_Dispatcher

And finally pushing your callback on it:

https://docs.microsoft.com/en-us/uwp/api/windows.ui.core.coredispatcher#Windows_UI_Core_CoreDispatcher_RunAsync_Windows_UI_Core_CoreDispatcherPriority_Windows_UI_Core_DispatchedHandler_

When you say it doesn’t work, do you get any failed HRESULTs after my changes?

Bingo!:slight_smile:

After putting that into UI thread all works, thanks a lot Tautvydas!

Development build, throws some errors, Im not really sure, is that crucial, mb we can skip that?

And the last question, if you would be so kind pls:))

how can I modify that function, to pass a callback from Unity to it and to return path to picked file?

lets say, when I have opened file - call C# Action(), to do smth?

kinda string PickFile(Action onComplete)?

or I don’t need callback on that, and just can wait until user picked smth, and we have received value from that function, and can proceed in Unity?

Anyways, it was super complicated! Thank you for your help, I guess this thread would help to a lot of other developers

I would not suggest ignoring those errors. It means you’re calling Unity APIs from non-Unity main thread (perhaps you’re calling them on the UI thread?). That can easily lead to random crashes. Use this to get back on Unity’s main thread:

For callbacks from native code, check this thread out:

If that still leaves open questions, feel free to post them here.

Hey. can you pls help me with FileSavePicker to?

Cant understand, what is going wrong with that, if I try to do following:

ComPtr<IAsyncOperation<StorageFile*>> op;

hr = picker->PickSaveFileAsync(&op);
AssertAndReturnIfFailed(hr);

It every times fails.

#include "pch.h"
#include "NativeDialogs.h"
#include <functional>
#include <wrl.h>
#include <windows.foundation.h>
#include <windows.storage.pickers.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Storage;
using namespace ABI::Windows::Storage::pickers;

#define AssertAndReturnIfFailed(hr) do { if (FAILED(hr)) { __debugbreak(); return hr; } } while (false);

extern "C" {

int ShowFileSavePicker(char *suggestedFileName, int length)
{
ComPtr<IInspectable> basePicker;
auto hr = RoActivateInstance(HStringReference(RuntimeClass_Windows_Storage_Pickers_FileSavePicker).Get(), &basePicker);
AssertAndReturnIfFailed(hr);

ComPtr<IFileSavePicker> picker;
hr = basePicker.As(&picker);
AssertAndReturnIfFailed(hr);

hr = picker->put_SuggestedStartLocation(PickerLocationId::pickerLocationId_DocumentsLibrary);
AssertAndReturnIfFailed(hr);

// char* to WCHAR convertation
const WCHAR *pwcsName;
int nChars = MultiByteToWideChar(CP_ACP, 0, suggestedFileName, -1, NULL, 0);
pwcsName = new WCHAR[nChars];
MultiByteToWideChar(CP_ACP, 0, suggestedFileName, -1, (LPWSTR)pwcsName, nChars);
HSTRING outSuggestedFileName;

hr = WindowsCreateString(pwcsName, sizeof(pwcsName), &outSuggestedFileName);
delete[] pwcsName;
AssertAndReturnIfFailed(hr);

hr = picker->put_SuggestedFileName(outSuggestedFileName);
AssertAndReturnIfFailed(hr);

ComPtr<IAsyncOperation<StorageFile*>> op;

hr = picker->PickSaveFileAsync(&op);
AssertAndReturnIfFailed(hr);

hr = op->put_Completed(Callback<IAsyncOperationCompletedHandler<StorageFile*>>([](IAsyncOperation<StorageFile*>* operation, AsyncStatus status) -> HRESULT
{
if (status != AsyncStatus::Completed)
{
// Figure out the error

ComPtr<IAsyncInfo> asyncInfo;
auto hr = operation->QueryInterface(IID_PPV_ARGS(&asyncInfo));
AssertAndReturnIfFailed(hr);

HRESULT errorCode;
hr = asyncInfo->get_ErrorCode(&errorCode);
AssertAndReturnIfFailed(hr);

// Do something with errorCode
// ...
// ...

return 9;
}

ComPtr<IStorageFile> storageFile;
auto hr = operation->GetResults(&storageFile);
AssertAndReturnIfFailed(hr);

// Hooray, file got picked. Do something with storageFile.
// ...
// ...

return 3;
}).Get());
AssertAndReturnIfFailed(hr);

return 1;
}
}

Yeah saving is a bit more complicated. Here’s the fixed code:

#include "pch.h"

#include <vector>
#include <inspectable.h>
#include <windows.storage.pickers.h>
#include <wrl.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Storage;
using namespace ABI::Windows::Storage::Pickers;

#define AssertAndReturnIfFailed(hr) do { if (FAILED(hr)) { __debugbreak(); return hr; } } while (false);

class StringVector : public RuntimeClass<RuntimeClassFlags<WinRtClassicComMix>, IVector<HSTRING>, IVectorView<HSTRING>, IIterable<HSTRING>, FtmBase>
{
public:
   StringVector()
   {
   }

   virtual HRESULT GetAt(uint32_t index, HSTRING* item) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       return m_Items[index].CopyTo(item);
   }

   virtual HRESULT get_Size(uint32_t* size) override
   {
       *size = static_cast<uint32_t>(m_Items.size());
       return S_OK;
   }

   virtual HRESULT GetView(IVectorView<HSTRING>** view) override
   {
       AddRef();
       *view = this;
       return S_OK;
   }

   virtual HRESULT IndexOf(HSTRING value, uint32_t* index, boolean* found) override
   {
       for (uint32_t i = 0; i < m_Items.size(); i++)
       {
           int32_t comparisonResult;
           auto hr = WindowsCompareStringOrdinal(m_Items[i].Get(), value, &comparisonResult);
           if (SUCCEEDED(hr) && comparisonResult == 0)
           {
               *found = true;
               *index = i;
               return S_OK;
           }
       }

       *found = false;
       return S_OK;
   }

   virtual HRESULT SetAt(uint32_t index, HSTRING item) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       return m_Items[index].Set(item);
   }

   virtual HRESULT InsertAt(uint32_t index, HSTRING item) override
   {
       if (index > m_Items.size())
           return E_BOUNDS;

       HString itemWrapper;
       auto hr = itemWrapper.Set(item);
       AssertAndReturnIfFailed(hr);

       m_Items.insert(m_Items.begin() + index, std::move(itemWrapper));
   }

   virtual HRESULT RemoveAt(uint32_t index) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       m_Items.erase(m_Items.begin() + index);
       return S_OK;
   }

   virtual HRESULT Append(HSTRING item) override
   {
       HString itemWrapper;
       auto hr = itemWrapper.Set(item);
       AssertAndReturnIfFailed(hr);

       m_Items.push_back(std::move(itemWrapper));
       return S_OK;
   }

   virtual HRESULT RemoveAtEnd() override
   {
       m_Items.pop_back();
       return S_OK;
   }

   virtual HRESULT Clear() override
   {
       m_Items.clear();
       return S_OK;
   }

   virtual HRESULT First(IIterator<HSTRING>** first) override
   {
       *first = Make<Iterator>(this).Detach();
       return S_OK;
   }

private:
   std::vector<HString> m_Items;

   class Iterator : public RuntimeClass<RuntimeClassFlags<WinRtClassicComMix>, IIterator<HSTRING>, FtmBase>
   {
   public:
       Iterator(StringVector* parent) :
           m_Parent(parent)
       {
       }

       virtual HRESULT get_Current(HSTRING* current) override
       {
           if (m_Position >= m_Parent->m_Items.size())
               return E_BOUNDS;

           return m_Parent->m_Items[m_Position].CopyTo(current);
       }

       virtual HRESULT get_HasCurrent(boolean* hasCurrent) override
       {
           *hasCurrent = m_Parent->m_Items.size() > m_Position;
           return S_OK;
       }

       virtual HRESULT MoveNext(boolean* hasCurrent) override
       {
           if (m_Position < m_Parent->m_Items.size())
           {
               m_Position++;
               return get_HasCurrent(hasCurrent);
           }

           *hasCurrent = false;
           return S_OK;
       }

   private:
       ComPtr<StringVector> m_Parent;
       uint32_t m_Position;
   };
};

extern "C" HRESULT ShowFileSavePicker(const wchar_t* suggestedFileName)
{
   ComPtr<IInspectable> basePicker;
   auto hr = RoActivateInstance(HStringReference(RuntimeClass_Windows_Storage_Pickers_FileSavePicker).Get(), &basePicker);
   AssertAndReturnIfFailed(hr);

   ComPtr<IFileSavePicker> picker;
   hr = basePicker.As(&picker);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_SuggestedStartLocation(PickerLocationId::PickerLocationId_DocumentsLibrary);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_SuggestedFileName(HStringReference(suggestedFileName).Get());
   AssertAndReturnIfFailed(hr);

   ComPtr<IMap<HSTRING, IVector<HSTRING>*>> fileTypeChoices;
   hr = picker->get_FileTypeChoices(&fileTypeChoices);
   AssertAndReturnIfFailed(hr);

   auto txtChoice = Make<StringVector>();
   hr = txtChoice->Append(HStringReference(L".txt").Get());
   AssertAndReturnIfFailed(hr);

   boolean replaced;
   hr = fileTypeChoices->Insert(HStringReference(L"Text file").Get(), txtChoice.Get(), &replaced);
   AssertAndReturnIfFailed(hr);

   ComPtr<IAsyncOperation<StorageFile*>> op;
   hr = picker->PickSaveFileAsync(&op);
   AssertAndReturnIfFailed(hr);

   hr = op->put_Completed(Callback<IAsyncOperationCompletedHandler<StorageFile*>>([](IAsyncOperation<StorageFile*>* operation, AsyncStatus status) -> HRESULT
   {
       if (status != AsyncStatus::Completed)
       {
           // Figure out the error

           ComPtr<IAsyncInfo> asyncInfo;
           auto hr = operation->QueryInterface(IID_PPV_ARGS(&asyncInfo));
           AssertAndReturnIfFailed(hr);

           HRESULT errorCode;
           hr = asyncInfo->get_ErrorCode(&errorCode);
           AssertAndReturnIfFailed(hr);

           // Do something with errorCode
           // ...
           // ...

           return S_OK;
       }

       ComPtr<IStorageFile> storageFile;
       auto hr = operation->GetResults(&storageFile);
       AssertAndReturnIfFailed(hr);

       // Hooray, file got picked. Do something with storageFile.
       // ...
       // ...

       return S_OK;
   }).Get());
   AssertAndReturnIfFailed(hr);

   return S_OK;
}

By the way - you don’t have to convert from char to wchar_t manually - C# already uses wide strings by default, so all you’re doing is converting them twice. Instead, stick [MarshalAs(UnmanagedType.LPWStr)] on the string parameter in your P/Invoke signature, and then it will expect the native code to receive string data as a wchar_t pointer.

Thanks a lot! You have helped me so much!

I even was able to get a callback from C++ in C#. In common that task was super hard for me, I am pretty ok with C++, as working a lot with UE4, but this Windows’s one is pretty different. What has really helped me is public Chromium and QT sources, where they was doing the same pickers for their platforms, but without you - I would never succeed!

Wish you all the best

I’m glad to help :).

Tautvydas,

is there any way I can ask you pls to help me once more…:sweat_smile:

because of itextsharp not working, we are trying to save pngs instead of pdfs.

If it try to write the data in file at picked path in Unity with Unity.Windows.WriteAllBytes, it tells me that this file is failed to open, and just leaves it empty.

So I would like to modify this save file picker function in c++, to accept byte[ ] with image data from C# call,

and to write that byte[ ] in to picked file in c++.

And once again, trying to that myself, but it will take me ages…

Thank you!

Tried to use such method, in runs all ok, but file is still empty.

I call that method right after we did save, below:

  • // Hooray, file got picked. Do something with storageFile.
  • // …
  • // …
int SaveDataToAStream(BYTE* data, IStorageFile*  filePtr, CallbackFunc callback)
{
__FIAsyncOperation_1_Windows__CStorage__CStreams__CIRandomAccessStream_t * operation;
__debugbreak();
auto hr = filePtr->OpenAsync(FileAccessMode::FileAccessMode_ReadWrite, &operation);
AssertAndReturnIfFailed(hr);

Event operationCompleted(CreateEvent(nullptr, true, false, nullptr));

hr = operation->put_Completed(Callback<IAsyncOperationCompletedHandler<IRandomAccessStream*>>
  ([&](IAsyncOperation<IRandomAccessStream*> *pHandler, AsyncStatus status) -> HRESULT
{
  SetEvent(operationCompleted.Get());
  return S_OK;
}).Get());

AssertAndReturnIfFailed(hr);


WaitForSingleObject(operationCompleted.Get(), INFINITE);

ComPtr<IRandomAccessStream> stream;

hr = operation->GetResults(&stream);
AssertAndReturnIfFailed(hr);

IOutputStream * outputStream;

hr = stream->GetOutputStreamAt(0, &outputStream);
AssertAndReturnIfFailed(hr);

ComPtr<IDataWriter> dataWriter;

ComPtr<IDataWriterFactory> dataWriterFactory;

Windows::Foundation::GetActivationFactory
(HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(), &dataWriterFactory);

hr = dataWriterFactory->CreateDataWriter(outputStream, &dataWriter);
AssertAndReturnIfFailed(hr);

UINT32 byteCount;

hr = dataWriter->WriteBytes(sizeof(data), data);
AssertAndReturnIfFailed(hr);

__FIAsyncOperation_1_UINT32_t * storeOperation;

Event storeOperationCompleted(CreateEvent(nullptr, true, false, nullptr));

hr = dataWriter->StoreAsync(&storeOperation);
AssertAndReturnIfFailed(hr);


__debugbreak();
hr = storeOperation->put_Completed(Callback<IAsyncOperationCompletedHandler<UINT32>>
  ([&](IAsyncOperation<UINT32> *pHandler, AsyncStatus status) -> HRESULT
{
  SetEvent(storeOperationCompleted.Get());


  ComPtr<IStorageItem> item;
  auto hrr = filePtr->QueryInterface(IID_PPV_ARGS(&item));
  AssertAndReturnIfFailed(hrr);

  HString path;
  HRESULT hr = item->get_Path(path.GetAddressOf());
  AssertAndReturnIfFailed(hr);

  uint32_t pathLen;
  const wchar_t *pathStr = path.GetRawBuffer(&pathLen);
  __debugbreak();
  callback(pathStr);

  return S_OK;
}).Get());
AssertAndReturnIfFailed(hr);

WaitForSingleObject(storeOperationCompleted.Get(), INFINITE);

return 0;
}

And this is what I pass to it from unity, byte[ ]

IntPtr unmanagedArray = Marshal.AllocHGlobal(data.Length);
  Marshal.Copy(data,0,unmanagedArray,data.Length);

You’re calling WaitForSingleObject on wrong things… I assume you wanted to call it on the events you created?

Anyway, here’s my take on it. C# code:

       private delegate void OnFilePickedDelegate(IntPtr storageFile, IntPtr data);

       [DllImport("__Internal")]
       private static extern int ShowFileSavePicker([MarshalAs(UnmanagedType.LPWStr)]string suggestedFileName, OnFilePickedDelegate onFilePicked, IntPtr data);

       [DllImport("__Internal")]
       private static extern int SaveDataToFileAndReleaseIt(IntPtr storageFile, byte[] data, int dataLength);


       [MonoPInvokeCallback(typeof(OnFilePickedDelegate))]
       private static void OnFilePicked(IntPtr storageFile, IntPtr data)
       {
           var byteArray = (byte[])GCHandle.FromIntPtr(data).Target;
           var hr = SaveDataToFileAndReleaseIt(storageFile, byteArray, byteArray.Length);
           if (hr != 0)
               Marshal.ThrowExceptionForHR(hr);
       }

       private static void SaveBytes(string suggestedFileName, byte[] bytes)
       {
           var gcHandle = GCHandle.Alloc(bytes);
           var hr = ShowFileSavePicker(suggestedFileName, OnFilePicked, GCHandle.ToIntPtr(gcHandle));
           if (hr != 0)
               Marshal.ThrowExceptionForHR(hr);
       }

Complete C++ code (note, it’s missing error checking in case async operations fail):

#include <cstdint>
#include <vector>
#include <inspectable.h>
#include <robuffer.h>
#include <windows.storage.pickers.h>
#include <windows.storage.streams.h>
#include <wrl.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Storage;
using namespace ABI::Windows::Storage::Pickers;
using namespace ABI::Windows::Storage::Streams;
using Windows::Storage::Streams::IBufferByteAccess;

#define AssertAndReturnIfFailed(hr) do { if (FAILED(hr)) { __debugbreak(); return hr; } } while (false);

class StringVector : public RuntimeClass<RuntimeClassFlags<WinRtClassicComMix>, IVector<HSTRING>, IVectorView<HSTRING>, IIterable<HSTRING>, FtmBase>
{
public:
   StringVector()
   {
   }

   virtual HRESULT __stdcall GetAt(uint32_t index, HSTRING* item) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       return m_Items[index].CopyTo(item);
   }

   virtual HRESULT __stdcall get_Size(uint32_t* size) override
   {
       *size = static_cast<uint32_t>(m_Items.size());
       return S_OK;
   }

   virtual HRESULT __stdcall GetView(IVectorView<HSTRING>** view) override
   {
       AddRef();
       *view = this;
       return S_OK;
   }

   virtual HRESULT __stdcall IndexOf(HSTRING value, uint32_t* index, boolean* found) override
   {
       for (uint32_t i = 0; i < m_Items.size(); i++)
       {
           int32_t comparisonResult;
           auto hr = WindowsCompareStringOrdinal(m_Items[i].Get(), value, &comparisonResult);
           if (SUCCEEDED(hr) && comparisonResult == 0)
           {
               *found = true;
               *index = i;
               return S_OK;
           }
       }

       *found = false;
       return S_OK;
   }

   virtual HRESULT __stdcall SetAt(uint32_t index, HSTRING item) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       return m_Items[index].Set(item);
   }

   virtual HRESULT __stdcall InsertAt(uint32_t index, HSTRING item) override
   {
       if (index > m_Items.size())
           return E_BOUNDS;

       HString itemWrapper;
       auto hr = itemWrapper.Set(item);
       AssertAndReturnIfFailed(hr);

       m_Items.insert(m_Items.begin() + index, std::move(itemWrapper));
       return S_OK;
   }

   virtual HRESULT __stdcall RemoveAt(uint32_t index) override
   {
       if (index >= m_Items.size())
           return E_BOUNDS;

       m_Items.erase(m_Items.begin() + index);
       return S_OK;
   }

   virtual HRESULT __stdcall Append(HSTRING item) override
   {
       HString itemWrapper;
       auto hr = itemWrapper.Set(item);
       AssertAndReturnIfFailed(hr);

       m_Items.push_back(std::move(itemWrapper));
       return S_OK;
   }

   virtual HRESULT __stdcall RemoveAtEnd() override
   {
       m_Items.pop_back();
       return S_OK;
   }

   virtual HRESULT __stdcall Clear() override
   {
       m_Items.clear();
       return S_OK;
   }

   virtual HRESULT __stdcall First(IIterator<HSTRING>** first) override
   {
       *first = Make<Iterator>(this).Detach();
       return S_OK;
   }

private:
   std::vector<HString> m_Items;

   class Iterator : public RuntimeClass<RuntimeClassFlags<WinRtClassicComMix>, IIterator<HSTRING>, FtmBase>
   {
   public:
       Iterator(StringVector* parent) :
           m_Parent(parent)
       {
       }

       virtual HRESULT __stdcall get_Current(HSTRING* current) override
       {
           if (m_Position >= m_Parent->m_Items.size())
               return E_BOUNDS;

           return m_Parent->m_Items[m_Position].CopyTo(current);
       }

       virtual HRESULT __stdcall get_HasCurrent(boolean* hasCurrent) override
       {
           *hasCurrent = m_Parent->m_Items.size() > m_Position;
           return S_OK;
       }

       virtual HRESULT __stdcall MoveNext(boolean* hasCurrent) override
       {
           if (m_Position < m_Parent->m_Items.size())
           {
               m_Position++;
               return get_HasCurrent(hasCurrent);
           }

           *hasCurrent = false;
           return S_OK;
       }

   private:
       ComPtr<StringVector> m_Parent;
       uint32_t m_Position;
   };
};

class WindowsBuffer : public RuntimeClass<RuntimeClassFlags<WinRtClassicComMix>, IBuffer, IBufferByteAccess>
{
public:
   WindowsBuffer(uint8_t* data, uint32_t length) :
       m_Buffer(data, data + length),
       m_Length(length)
   {
   }

   virtual HRESULT __stdcall get_Capacity(UINT32* value) override
   {
       *value = static_cast<UINT32>(m_Buffer.size());
       return S_OK;
   }

   virtual HRESULT __stdcall get_Length(UINT32* value) override
   {
       *value = m_Length;
       return S_OK;
   }

   virtual HRESULT __stdcall put_Length(UINT32 value) override
   {
       if (value > m_Buffer.size())
           return E_BOUNDS;

       m_Length = value;
       return S_OK;
   }

   virtual HRESULT __stdcall Buffer(byte** value) override
   {
       *value = &m_Buffer[0];
       return S_OK;
   }

private:
   std::vector<uint8_t> m_Buffer;
   uint32_t m_Length;
};

typedef void (__stdcall* OnFilePicked)(IStorageFile* storageFile, void* data);

HRESULT GetOperationErrorCode(IAsyncInfo* asyncInfo, AsyncStatus status)
{
   HRESULT errorCode;
   auto hr = asyncInfo->get_ErrorCode(&errorCode);
   AssertAndReturnIfFailed(hr);
   return errorCode;
}

template <typename T>
HRESULT GetOperationErrorCode(T* operation, AsyncStatus status)
{
   if (status == AsyncStatus::Completed)
       return S_OK;

   // Figure out the error
   ComPtr<IAsyncInfo> asyncInfo;
   auto hr = operation->QueryInterface(IID_PPV_ARGS(&asyncInfo));
   AssertAndReturnIfFailed(hr);

   return GetOperationErrorCode(asyncInfo.Get(), status);
}

extern "C" HRESULT __stdcall ShowFileSavePicker(const wchar_t* suggestedFileName, OnFilePicked onFilePicked, void* data)
{
   ComPtr<IInspectable> basePicker;
   auto hr = RoActivateInstance(HStringReference(RuntimeClass_Windows_Storage_Pickers_FileSavePicker).Get(), &basePicker);
   AssertAndReturnIfFailed(hr);

   ComPtr<IFileSavePicker> picker;
   hr = basePicker.As(&picker);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_SuggestedStartLocation(PickerLocationId::PickerLocationId_DocumentsLibrary);
   AssertAndReturnIfFailed(hr);

   hr = picker->put_SuggestedFileName(HStringReference(suggestedFileName).Get());
   AssertAndReturnIfFailed(hr);

   ComPtr<IMap<HSTRING, IVector<HSTRING>*>> fileTypeChoices;
   hr = picker->get_FileTypeChoices(&fileTypeChoices);
   AssertAndReturnIfFailed(hr);

   auto txtChoice = Make<StringVector>();
   hr = txtChoice->Append(HStringReference(L".txt").Get());
   AssertAndReturnIfFailed(hr);

   boolean replaced;
   hr = fileTypeChoices->Insert(HStringReference(L"Text file").Get(), txtChoice.Get(), &replaced);
   AssertAndReturnIfFailed(hr);

   ComPtr<IAsyncOperation<StorageFile*>> op;
   hr = picker->PickSaveFileAsync(&op);
   AssertAndReturnIfFailed(hr);

   hr = op->put_Completed(Callback<IAsyncOperationCompletedHandler<StorageFile*>>([onFilePicked, data](IAsyncOperation<StorageFile*>* operation, AsyncStatus status) -> HRESULT
   {
       auto hr = GetOperationErrorCode(operation, status);
       if (FAILED(hr))
       {
           // Do something with the error code
           // ...
           // ...
           return S_OK;
       }

       ComPtr<IStorageFile> storageFile;
       hr = operation->GetResults(&storageFile);
       AssertAndReturnIfFailed(hr);

       // Hooray, file got picked. Do something with storageFile.
       if (storageFile != nullptr)
           onFilePicked(storageFile.Detach(), data);

       return S_OK;
   }).Get());
   AssertAndReturnIfFailed(hr);

   return S_OK;
}

extern "C" __declspec(dllexport) HRESULT __stdcall SaveDataToFileAndReleaseIt(IStorageFile* storageFileRaw, uint8_t* data, uint32_t dataLength)
{
   ComPtr<IStorageFile> storageFile;
   storageFile.Attach(storageFileRaw);

   ComPtr<IAsyncOperation<IRandomAccessStream*>> openOperation;
   auto hr = storageFile->OpenAsync(FileAccessMode::FileAccessMode_ReadWrite, &openOperation);
   AssertAndReturnIfFailed(hr);

   auto buffer = Make<WindowsBuffer>(data, dataLength);
   hr = openOperation->put_Completed(Callback<IAsyncOperationCompletedHandler<IRandomAccessStream*>>([buffer](IAsyncOperation<IRandomAccessStream*>* operation, AsyncStatus status) -> HRESULT
   {
       auto hr = GetOperationErrorCode(operation, status);
       if (FAILED(hr))
       {
           // Do something with the error code
           // ...
           // ...
           return S_OK;
       }

       ComPtr<IRandomAccessStream> stream;
       hr = operation->GetResults(&stream);
       AssertAndReturnIfFailed(hr);

       ComPtr<IOutputStream> outputStream;
       hr = stream.As(&outputStream);
       AssertAndReturnIfFailed(hr);

       ComPtr<IAsyncOperationWithProgress<UINT32, UINT32>> writeOperation;
       hr = outputStream->WriteAsync(buffer.Get(), &writeOperation);
       AssertAndReturnIfFailed(hr);

       hr = writeOperation->put_Completed(Callback<IAsyncOperationWithProgressCompletedHandler<UINT32, UINT32>>([](IAsyncOperationWithProgress<UINT32, UINT32>* operation, AsyncStatus status) -> HRESULT
       {
           auto hr = GetOperationErrorCode(operation, status);
           if (FAILED(hr))
           {
               // Do something with the error code
               // ...
               // ...
               return S_OK;
           }

           // Success! At this point the file is written.

           return S_OK;
       }).Get());
       AssertAndReturnIfFailed(hr);

       return S_OK;
   }).Get());
   AssertAndReturnIfFailed(hr);

   return S_OK;
}