react-native-windows-samples icon indicating copy to clipboard operation
react-native-windows-samples copied to clipboard

UIDispatcher how to return value/resolve/reject promise without crash? (Documentation unlcear)

Open creambyemute opened this issue 2 years ago • 2 comments

Problem Description

I have two small native c++ modules which use the Launcher::LaunchFileAsync and FileOpenPicker::pickSingleFileAsync.

After upgrading from 0.63 to 0.65 we have to use the UIDispatcher in order for it to work. So I had a look at the following Documentation: Using UIDispatcher with C++/WinRT

Unfortunately, the example lacks the bit about how to return a value from there (in that example, that would probably be the file.Path()).

So I digged a bit further and had a look at the following 4 modules inside react-native-windows: Clipboard Module AccessibilityInfoModule ImageViewManagerModule AlertModule

I tried multiple different variations now and had a look at the crash dump but I always crash with an Access Violation on the promise.resolve line.

What am I doing wrong there? I don't see a lot of differences between my code and the above modules despite the co_await usage. I'm also having the same problem with FileOpenPicker where I'd want to resolve the tempFile.Path() after picking+moving it to the apps temp folder (StorageFile tempFile{ co_await file.CopyAsync(winrt::Windows::Storage::ApplicationData::Current().TemporaryFolder(), file.Name(), NameCollisionOption::ReplaceExisting) };)

Steps To Reproduce

  1. Init a new project
  2. Add the FileOpener posted below to the project
  3. Add an openable file to the application folder (for example a pdf)
  4. Call NativeModules.FileOpener.openFileAsync(filePath, shouldShowOpenWithDialog)

Expected Results

Not crash, resolve the promise to JS.

Documentation to reflect an actual usage of FileOpenPicker/UIDispatcher with result returns/promise.resolve.

CLI version

6.1.0

Environment

info Fetching system and libraries information...
System:
    OS: Windows 10 10.0.19043
    CPU: (12) x64 AMD Ryzen 5 5600X 6-Core Processor
    Memory: 24.72 GB / 31.94 GB
  Binaries:
    Node: 14.16.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.11 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 6.14.11 - C:\Program Files\nodejs\npm.CMD
    Watchman: Not Found
  SDKs:
    Android SDK:
      API Levels: 28, 29, 30
      Build Tools: 29.0.0, 29.0.1, 29.0.2, 29.0.3, 30.0.0, 30.0.1, 30.0.2, 30.0.3
      System Images: android-28 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-30 | Google APIs Intel x86 Atom
      Android NDK: Not Found
    Windows SDK:
      AllowDevelopmentWithoutDevLicense: Enabled
      AllowAllTrustedApps: Enabled
      Versions: 10.0.16299.0, 10.0.17134.0, 10.0.17763.0, 10.0.18362.0, 10.0.19041.0
  IDEs:
    Android Studio: Not Found
    Visual Studio: 16.9.31313.79 (Visual Studio Community 2019)
  Languages:
    Java: 1.8.0_282 - /c/Program Files/OpenJDK/openjdk-8u282-b08/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.2 => 17.0.2
    react-native: 0.65.2 => 0.65.2
    react-native-windows: ^0.65.7 => 0.65.7
  npmGlobalPackages:
    *react-native*: Not Found

Target Platform Version

10.0.19041

Target Device(s)

Desktop

Visual Studio Version

Visual Studio 2019

Build Configuration

Release

Snack, code example, screenshot, or link to a repository

FileOpener.h

#pragma once

#include "pch.h"
#include "NativeModules.h"
#include <string>

namespace RN = winrt::Microsoft::ReactNative;

namespace inspection::FileOpener {
    REACT_MODULE(FileOpener, L"FileOpener");
    struct FileOpener final {
        RN::ReactContext m_reactContext;

        REACT_INIT(Initialize)
            void Initialize(RN::ReactContext const& reactContext) noexcept;

        REACT_METHOD(openFileAsync)
        winrt::fire_and_forget openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept;
    };
}

FileOpener.cpp

#include "pch.h"

#include "FileOpener.h"

#include <filesystem>
#include <windows.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.System.h>

using namespace winrt;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Foundation;
using namespace Windows::System;

namespace inspection::FileOpener {
    void FileOpener::Initialize(RN::ReactContext const& reactContext) noexcept {
        m_reactContext = reactContext;
    }

    winrt::fire_and_forget FileOpener::openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept
    try {
        std::filesystem::path fPath{ filepath };
        fPath.make_preferred();
        auto jsDispatcher = m_reactContext.JSDispatcher();

        try {
            StorageFile file{ co_await StorageFile::GetFileFromPathAsync(winrt::to_hstring(fPath.c_str())) };
            try {
                if (file) {
                    m_reactContext.UIDispatcher().Post([showOpenWithDialog, file, promise, fPath, jsDispatcher]()->winrt::fire_and_forget {
                        LauncherOptions launchOptions;
                        launchOptions.DisplayApplicationPicker(showOpenWithDialog);
                        bool success{ co_await Launcher::LaunchFileAsync(file, launchOptions) };
                        if (success) {
                            jsDispatcher.Post([promise] { promise.Resolve(); });
                        } else {
                            jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
                        }
                    });
                } else {
                    promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) });
                }
            } catch (const hresult_error& ex) {
                promise.Reject(RN::ReactError{ "Unable to LaunchFileAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
            }

        } catch(const hresult_error& ex) {
            promise.Reject(RN::ReactError{ "Unable to GetFileFromPathAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
        }

    } catch(const hresult_error& ex) {
        promise.Reject(RN::ReactError{ "Unable to make path or make_preferred for File " + filepath, winrt::to_string(ex.message()).c_str() });
    }
}

creambyemute avatar Nov 17 '21 17:11 creambyemute

Thanks for the detailed issue report, @creambyemute. Sorry that the documentation hasn't been clear here. Let me see if we can bring in someone to help unblock you and also get the documentation better expanded for the next person.

Since I think this is a documentation issue I'll move to the repo where that article lives.

chrisglein avatar Nov 18 '21 19:11 chrisglein

I have got it working now. Turns out, the comment // unfortunately, lambda captures doesn't work well with winrt::fire_and_forget and co_await here // call asyncOp.Completed explicitly in ClipboardModule is correct.

When using The IAsyncOperation<T> with asyncOp.Completed I can successfully resolve to the JS realm.

Is there any way to use co_await in this scenario? The code would be a lot shorter without the asyncOp.Completed, especially for the case where you'd have more than one of the asyncOperations.

Edit: According to https://github.com/microsoft/react-native-windows-samples/issues/427#issuecomment-826667012 it may be possible with a std::move on the promise. I'll try that out an report back (I think this may be worth mentioning in the docs)

winrt::fire_and_forget FileOpener::openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept
    try {
        std::filesystem::path fPath{ filepath };
        fPath.make_preferred();
        auto jsDispatcher = m_reactContext.JSDispatcher();

        try {
            StorageFile file{ co_await StorageFile::GetFileFromPathAsync(winrt::to_hstring(fPath.c_str())) };
            try {
                if (file) {
                    m_reactContext.UIDispatcher().Post([showOpenWithDialog, file, promise, fPath, jsDispatcher] {
                        LauncherOptions launchOptions;
                        launchOptions.DisplayApplicationPicker(showOpenWithDialog);
                        IAsyncOperation<bool> asyncLaunchFileOp = Launcher::LaunchFileAsync(file, launchOptions);
                        asyncLaunchFileOp.Completed([jsDispatcher, promise, fPath](const IAsyncOperation<bool>& asyncLaunchFileOp, AsyncStatus status) {
                            switch (status) {
                                case AsyncStatus::Completed: {
                                    bool success = asyncLaunchFileOp.GetResults();
                                    if (success) {
                                        jsDispatcher.Post([promise] { promise.Resolve(); });
                                    }
                                    else {
                                        jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
                                    }
                                    break;
                                }
                                case AsyncStatus::Canceled: {
                                    jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
                                    break;
                                }
                                case AsyncStatus::Error: {
                                    auto message = std::wstring(winrt::hresult_error(asyncLaunchFileOp.ErrorCode()).message());
                                    jsDispatcher.Post([promise, message, fPath] { promise.Reject(RN::ReactError{ "Unable to open File" + winrt::to_string(fPath.c_str()), winrt::to_string(message.c_str()) }); });
                                    break;
                                }
                                case AsyncStatus::Started: {
                                    jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
                                    break;
                                }
                            }
                        });
                    });
                } else {
                    promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) });
                }
            } catch (const hresult_error& ex) {
                promise.Reject(RN::ReactError{ "Unable to LaunchFileAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
            }

        } catch(const hresult_error& ex) {
            promise.Reject(RN::ReactError{ "Unable to GetFileFromPathAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
        }

    } catch(const hresult_error& ex) {
        promise.Reject(RN::ReactError{ "Unable to make path or make_preferred for File " + filepath, winrt::to_string(ex.message()).c_str() });
    }

creambyemute avatar Nov 19 '21 08:11 creambyemute