Threading
Crossing threads in JSI
A non-JSI example to illustrate
Threading could be easy, it's just that JSI has no first-class APIs for it. Here I show an Old Architecture React Native Windows native module that demonstrates how easy thread-crossing can be with some decent primitives. Here, it's easy because we have easy access to UIDispatcher and have onSuccess and onFailure handlers passed to us.
#pragma once
#include "JSValue.h"
#include "NativeModules.h"
#include <winrt/Windows.Security.Authentication.Web.h>
using namespace winrt::Microsoft::ReactNative;
using namespace winrt::Windows::Security::Authentication::Web;
namespace MyModule
{
REACT_MODULE(ReactNativeModule, L"MyModule")
struct ReactNativeModule
{
REACT_INIT(Initialize)
void Initialize(ReactContext const &reactContext) noexcept
{
m_reactContext = reactContext;
}
public:
REACT_METHOD(authenticate)
void authenticate(
std::string authUri,
std::string redirectUri,
std::function<void(std::string)>&& onSuccess,
std::function<void(std::string)>&& onFailure
) noexcept
{
// WebAuthenticationBroker::AuthenticateAsync() must be called on the UI
// thread, otherwise the app crashes.
m_reactContext.UIDispatcher().Post([authUri, redirectUri, onSuccess, onFailure]() {
try {
auto startUri = winrt::Windows::Foundation::Uri(winrt::to_hstring(authUri));
auto endUri = winrt::Windows::Foundation::Uri(winrt::to_hstring(redirectUri));
// Based on:
// https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/WebAuthenticationBroker/cpp/Scenario2_oAuthFacebook.xaml.cpp
WebAuthenticationBroker::AuthenticateAsync(
WebAuthenticationOptions::None,
startUri,
endUri
)
.Completed(
[onSuccess, onFailure]
(
IAsyncOperation<WebAuthenticationResult> const& operation,
AsyncStatus const status
) noexcept {
auto result = operation.GetResults();
if (result.ResponseStatus() == WebAuthenticationStatus::Success) {
onSuccess(winrt::to_string(result.ResponseData()));
} else if (result.ResponseStatus() == WebAuthenticationStatus::ErrorHttp) {
onFailure("HTTP Status " + std::to_string(result.ResponseErrorDetail()));
} else {
onFailure("Authentication failed, or was cancelled.");
}
});
} catch (const std::exception& ex) {
onFailure("Exception: " + std::string(ex.what()));
}
});
}
private:
ReactContext m_reactContext{nullptr};
};
} // namespace MyModuleThis becomes a lot more fiddly in JSI because we need to get access to the UIDispatcher ourselves, not to mention managing a Promise. But let's not be discouraged!
This time with JSI
For now, I just have a Windows example, but the key concepts will be the same across all platforms. The main unknown will be figuring out how to get a reference to uiInvoker on each platform.
Apple platforms
TODO
Android
TODO
Windows
Here is a C++/WinRT example for React Native Windows (Old Arch), which launches the root folder of the local app data store. It shows how we can cross over to the UI thread with uiInvoker to run a native UI API, then cross back to the JS thread to resolve a Promise.
While this code example was written for Old Arch, it should be almost identical on New Arch. The main differece is that, in ReactNativeModule.h, the API for grabbing the jsInvoker and uiInvoker will have changed slightly.
MyHostObject.h
We'll make a MyHostObject with a launchRootFolder(): Promise<void> method on it. It relies on being passed a jsInvoker and uiInvoker at construction time (which we'll show how to do in ReactNativeModule.h).
#pragma once
#include <JSI/JsiApiContext.h>
#include <ReactCommon/TurboModule.h>
#include <ReactCommon/TurboModuleUtils.h>
#include <Windows.h>
#include <jsi/jsi.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.System.h>
#include <string>
#include "JSValue.h"
#include "NativeModules.h"
using namespace winrt::Microsoft::ReactNative;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::System;
using namespace facebook;
namespace winrt::MyModule
{
class MyHostObject : public jsi::HostObject
{
public:
MyHostObject(
std::shared_ptr<react::CallInvoker> jsInvoker,
std::shared_ptr<react::CallInvoker> uiInvoker
)
: jsi::HostObject(),
m_jsInvoker(jsInvoker),
m_uiInvoker(uiInvoker)
{
}
private:
std::shared_ptr<react::CallInvoker> m_jsInvoker{nullptr};
std::shared_ptr<react::CallInvoker> m_uiInvoker{nullptr};
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override
{
std::string name = propName.utf8(rt);
if (name == "launchRootFolder")
{
return jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, name),
0,
[this](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *arguments, size_t) -> jsi::Value
{
return rt.global()
.getPropertyAsFunction(rt, "Promise")
.callAsConstructor(
rt,
jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "launchRootFolderPromise"),
2,
[this, options](
jsi::Runtime &rt2, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value
{
auto resolverValue = std::make_shared<jsi::Value>((arguments[0].asObject(rt2)));
auto rejecterValue = std::make_shared<jsi::Value>((arguments[1].asObject(rt2)));
this->m_uiInvoker.get()->invokeAsync(
[this, resolverValue, rejecterValue, &rt2]()
{
// https://learn.microsoft.com/en-us/uwp/api/windows.storage.applicationdata.localfolder? view=winrt-26100#windows-storage-applicationdata-localfolder
StorageFolder storageFolder = ApplicationData::Current().LocalFolder();
// https://learn.microsoft.com/en-us/uwp/api/windows.system.launcher.launchfolderasync?view=winrt-26100
Launcher::LaunchFolderAsync(storageFolder)
.Completed(
[this, resolverValue, rejecterValue, &rt2](
IAsyncOperation<bool> const &operation,
AsyncStatus const status
)
{
this->m_jsInvoker.get()->invokeAsync(
[this, operation, resolverValue, rejecterValue, &rt2]()
{
bool result = operation.GetResults();
resolverValue->asObject(rt2).asFunction(rt2).call(rt2, jsi::Value(result));
});
});
});
return jsi::Value::undefined();
}
)
);
});
}
return jsi::Value::undefined();
}
};
} // namespace winrt::MyModuleReactNativeModule.h
In our Initialize method for the TurboModule, we install a MyHostObject instance onto the global object, passing our jsInvoker and uiInvoker to it.
#pragma once
// TODO: Investigate which of these headers can be removed.
#include <JSI/JsiAbiApi.h>
#include <JSI/JsiApiContext.h>
#include <ReactCommon/TurboModule.h>
#include <TurboModuleProvider.h>
#include <Windows.h>
#include <jsi/jsi.h>
#include <winrt/Microsoft.ReactNative.h>
#include <winrt/Windows.Foundation.h>
#include "MyHostObject.h"
#include "JSValue.h"
#include "NativeModules.h"
using namespace winrt::Microsoft::ReactNative;
using namespace facebook;
namespace winrt::MyModule
{
REACT_MODULE(ReactNativeModule, L"MyModule")
struct ReactNativeModule
{
REACT_INIT(Initialize)
void Initialize(ReactContext const &reactContext) noexcept
{
ExecuteJsi(
reactContext,
[reactContext](jsi::Runtime &rt)
{
auto jsInvoker = winrt::Microsoft::ReactNative::MakeAbiCallInvoker(reactContext.JSDispatcher().Handle());
auto uiInvoker = winrt::Microsoft::ReactNative::MakeAbiCallInvoker(reactContext.UIDispatcher().Handle());
rt.global().setProperty(
rt,
"MyHostObject",
jsi::Object::createFromHostObject(rt, std::make_shared<MyHostObject>(jsInvoker, uiInvoker)));
});
}
public:
~ReactNativeModule()
{
}
};
} // namespace winrt::MyModule