From d77c74a5265922a19d4eed6e3df53fd00085c223 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 13:33:32 -0700 Subject: [PATCH 1/4] Add jni target to ReactCxxPlatform Summary: changelog: [internal] Differential Revision: D76240376 --- .../ReactCxxPlatform/react/jni/JniHelper.cpp | 28 +++++++++++++++++++ .../ReactCxxPlatform/react/jni/JniHelper.h | 19 +++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h diff --git a/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp new file mode 100644 index 000000000000..a3689d5d0949 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv* env) { + auto activityThreadClass = env->FindClass("android/app/ActivityThread"); + auto currentApplicationMethodID = env->GetStaticMethodID( + activityThreadClass, "currentApplication", "()Landroid/app/Application;"); + return env->CallStaticObjectMethod( + activityThreadClass, currentApplicationMethodID); +} + +jni::alias_ref getContext() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto application = getApplication(env); + return facebook::jni::wrap_alias( + static_cast(application)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h new file mode 100644 index 000000000000..8db809186f31 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv* env); + +jni::alias_ref getContext(); + +} // namespace facebook::react From d0260f2e29c6742536247806767857782e6baf8f Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 13:33:32 -0700 Subject: [PATCH 2/4] Add http target to ReactCxxPlatform Summary: changelog: [internal] Differential Revision: D76240550 --- .../react/http/IHttpClient.cpp | 13 +++ .../ReactCxxPlatform/react/http/IHttpClient.h | 89 +++++++++++++++++++ .../react/http/IWebSocketClient.cpp | 13 +++ .../react/http/IWebSocketClient.h | 47 ++++++++++ 4 files changed, 162 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp new file mode 100644 index 000000000000..c19f481ee22d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp @@ -0,0 +1,13 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char HttpClientFactoryKey[] = "HttpClientFactory"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h new file mode 100644 index 000000000000..9b3d78b5c9d6 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace http { + +using Headers = std::vector>; + +struct FormDataField { + std::string fieldName; + Headers headers; + std::optional string; + std::optional uri; +}; + +using FormData = std::vector; + +struct Body { + std::optional string; + std::optional blob; + std::optional formData; + std::optional base64; +}; + +using OnUploadProgress = std::function; +using OnResponse = std::function; +using OnBody = std::function body)>; +using OnBodyIncremental = std::function body)>; +using OnBodyProgress = std::function; +using OnResponseComplete = + std::function; + +struct NetworkCallbacks { + OnUploadProgress onUploadProgress{nullptr}; + OnResponse onResponse{nullptr}; + OnBody onBody{nullptr}; + OnBodyIncremental onBodyIncremental{nullptr}; + OnBodyProgress onBodyProgress{nullptr}; + OnResponseComplete onResponseComplete{nullptr}; + bool sendIncrementalUpdates{false}; + bool sendProgressUpdates{false}; +}; + +struct IRequestToken { + virtual ~IRequestToken() = default; + + virtual void cancel() noexcept = 0; +}; + +} // namespace http + +struct IHttpClient { + virtual ~IHttpClient() = default; + + virtual std::unique_ptr sendRequest( + http::NetworkCallbacks&& callback, + const std::string& method, + const std::string& url, + const http::Headers& headers = {}, + const http::Body& body = {}, + uint32_t timeout = 0, + std::optional loggingId = std::nullopt) = 0; +}; + +extern const char HttpClientFactoryKey[]; + +using HttpClientFactory = std::function()>; + +HttpClientFactory getHttpClientFactory(); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp new file mode 100644 index 000000000000..93800a35a7b7 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp @@ -0,0 +1,13 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char WebSocketClientFactoryKey[] = "WebSocketClientFactory"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h new file mode 100644 index 000000000000..a2ec1b0859bb --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient { + public: + using OnConnectCallback = std::function; + using OnClosedCallback = std::function; + using OnMessageCallback = std::function; + + virtual ~IWebSocketClient() = default; + + virtual void setOnClosedCallback(OnClosedCallback&& callback) noexcept = 0; + + virtual void setOnMessageCallback(OnMessageCallback&& callback) noexcept = 0; + + virtual void connect( + const std::string& url, + OnConnectCallback&& = nullptr) = 0; + + virtual void close(const std::string& reason) = 0; + + virtual void send(const std::string& message) = 0; + + virtual void ping() = 0; +}; + +extern const char WebSocketClientFactoryKey[]; + +using WebSocketClientFactory = + std::function()>; + +WebSocketClientFactory getWebSocketClientFactory(); + +} // namespace facebook::react From 1d942b1ab64c48c48a47928ab0f93e3088a5d50d Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 13:33:32 -0700 Subject: [PATCH 3/4] Add io target to ReactCxxPlatform Summary: changelog: [internal] Differential Revision: D76240623 --- .../react/io/ImageLoaderModule.cpp | 52 +++ .../react/io/ImageLoaderModule.h | 48 +++ .../react/io/NetworkingModule.cpp | 352 ++++++++++++++++++ .../react/io/NetworkingModule.h | 143 +++++++ .../react/io/ResourceLoader.cpp | 75 ++++ .../react/io/ResourceLoader.h | 32 ++ .../react/io/WebSocketModule.cpp | 156 ++++++++ .../react/io/WebSocketModule.h | 64 ++++ .../platform/android/AssetManagerHelpers.cpp | 60 +++ .../io/platform/android/AssetManagerHelpers.h | 21 ++ .../io/platform/android/ResourceLoader.cpp | 71 ++++ .../react/io/platform/cxx/ResourceLoader.cpp | 37 ++ .../react/io/tests/NetworkingModuleTests.cpp | 126 +++++++ 13 files changed, 1237 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp diff --git a/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp new file mode 100644 index 000000000000..1265eb259e3d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageLoaderModule.h" + +namespace facebook::react { + +// TODO: T170340321 - actual implementation + +jsi::Object ImageLoaderModule::getConstants(jsi::Runtime& rt) { + return jsi::Object(rt); +} + +void ImageLoaderModule::abortRequest(jsi::Runtime& rt, int32_t requestId) {} + +AsyncPromise ImageLoaderModule::getSize( + jsi::Runtime& rt, + const std::string& /*uri*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve({.width = 0.0, .height = 0.0}); + return promise; +} + +AsyncPromise ImageLoaderModule::getSizeWithHeaders( + jsi::Runtime& rt, + const std::string& /*uri*/, + jsi::Object /*headers*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve({.width = 0.0, .height = 0.0}); + return promise; +} + +AsyncPromise ImageLoaderModule::prefetchImage( + jsi::Runtime& rt, + const std::string& /*uri*/, + int32_t /*requestId*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve(false); + return promise; +} + +jsi::Object ImageLoaderModule::queryCache( + jsi::Runtime& rt, + const std::vector& /*uris*/) { + return jsi::Object(rt); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h new file mode 100644 index 000000000000..c0e2f9b4e8e8 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +using ImageSize = NativeImageLoaderAndroidImageSize; + +template <> +struct Bridging + : NativeImageLoaderAndroidImageSizeBridging {}; + +class ImageLoaderModule + : public NativeImageLoaderAndroidCxxSpec { + public: + ImageLoaderModule(std::shared_ptr jsInvoker) + : NativeImageLoaderAndroidCxxSpec(jsInvoker) {} + + jsi::Object getConstants(jsi::Runtime& rt); + void abortRequest(jsi::Runtime& rt, int32_t requestId); + + AsyncPromise getSize(jsi::Runtime& rt, const std::string& uri); + + AsyncPromise getSizeWithHeaders( + jsi::Runtime& rt, + const std::string& uri, + jsi::Object headers); + + AsyncPromise + prefetchImage(jsi::Runtime& rt, const std::string& uri, int32_t requestId); + + jsi::Object queryCache( + jsi::Runtime& rt, + const std::vector& uris); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp new file mode 100644 index 000000000000..bbf002a0931c --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp @@ -0,0 +1,352 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NetworkingModule.h" + +#include + +namespace facebook::react { + +// The requests abstraction is here to deal with a family of race conditions +// which can happen due to the async nature of this code: +// +// * store token vs callback +// * callback vs cancel +// * stop vs everything +// +// All of the access to the Requests data is protected by a mutex, so it is +// effectively serialized. The code takes care to handle execution in any +// order, so long as reserve for an id precedes any other operation on that id; +// the tests verify that behavior is correct for all use cases. + +Requests::~Requests() { + react_native_assert(isStopped() && "Requests::stop() was not called"); +} + +bool Requests::reserve(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.emplace(id, nullptr); + return true; +} + +void Requests::store(uint32_t id, std::unique_ptr token) { + // Three possibilities here: + + // 1. HttpClient is shutting down. Cancel the token, don't save it. + // 2. The entry in tokens_ has been erased, which means the callbacks have + // run. We cancel, which should be harmless, just in case. Nothing to save. + // 3. HttpClient and the request are still running. Save the token. + + { + if (stopped_) { + // option 1: fall past the lock and cancel + } else { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter == tokens_.end()) { + // option 2: fall past the lock and cancel + } else { + // option 3: save the token + iter->second = std::move(token); + return; + } + } + } + + if (token) { + token->cancel(); + } +} + +bool Requests::erase(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.erase(id); + return true; +} + +void Requests::cancel(uint32_t id) { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter != tokens_.end()) { + if (iter->second) { + iter->second->cancel(); + } + tokens_.erase(iter); + } +} + +void Requests::stop() { + stopped_ = true; + decltype(tokens_) tokensCopy; + { + std::lock_guard lock(tokensMutex_); + tokensCopy.swap(tokens_); + } + + for (const auto& token : tokensCopy) { + if (token.second) { + token.second->cancel(); + } + } +} + +bool Requests::isStopped() { + if (stopped_) { + std::lock_guard lock(tokensMutex_); + assert(tokens_.empty()); + } + return stopped_; +} + +NetworkingModule::NetworkingModule( + std::shared_ptr jsInvoker, + const HttpClientFactory& httpClientFactory) + : NativeNetworkingAndroidCxxSpec(jsInvoker) { + httpClient_ = httpClientFactory(); +} + +NetworkingModule::~NetworkingModule() { + requests_.stop(); +} + +void NetworkingModule::sendRequest( + jsi::Runtime& /*rt*/, + const std::string& method, + const std::string& url, + uint32_t requestId, + const http::Headers& headers, + const http::Body& body, + const std::string& responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool /*withCredentials*/) { + if (!requests_.reserve(requestId)) { + // If the NetworkingModule is shutting down, don't invoke the callback. + return; + } + + bool sendIncrementalUpdates = responseType == "text" && useIncrementalUpdates; + bool sendProgressUpdates = responseType != "text" && useIncrementalUpdates; + + http::NetworkCallbacks callbacks{ + .onUploadProgress = + [weakThis = weak_from_this(), requestId]( + int64_t progress, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didSendNetworkData(requestId, progress, total); + } + }, + .onResponse = + [weakThis = weak_from_this(), requestId, url]( + uint32_t responseCode, const http::Headers& headers) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkResponse( + requestId, responseCode, headers, url); + } + }, + .onBody = + [weakThis = weak_from_this(), requestId, responseType]( + std::unique_ptr data) { + if (auto strongThis = weakThis.lock()) { + try { + strongThis->didReceiveNetworkData( + requestId, responseType, std::move(data)); + } catch (const std::exception& e) { + LOG(ERROR) << "Failed to parse network response body: " + << e.what(); + } catch (...) { + LOG(ERROR) << "Failed to parse network response body."; + } + } + }, + .onBodyIncremental = + [weakThis = weak_from_this(), requestId]( + int64_t progress, + int64_t total, + std::unique_ptr body) { + int64_t bytesRead = 0; + if (auto strongThis = weakThis.lock()) { + bytesRead = strongThis->didReceiveNetworkIncrementalData( + requestId, std::move(body), progress, total); + } + return bytesRead; + }, + .onBodyProgress = + [weakThis = weak_from_this(), requestId]( + int64_t loaded, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkDataProgress( + requestId, loaded, total); + } + }, + .onResponseComplete = + [weakThis = weak_from_this(), requestId]( + const std::string& error, bool timeout) { + if (auto strongThis = weakThis.lock()) { + strongThis->didCompleteNetworkResponse(requestId, error, timeout); + } + }, + .sendIncrementalUpdates = sendIncrementalUpdates, + .sendProgressUpdates = sendProgressUpdates}; + + auto token = httpClient_->sendRequest( + std::move(callbacks), + method, + url, + headers, + body, + timeout, + std::to_string(requestId)); + if (token) { + requests_.store(requestId, std::move(token)); + } +} + +void NetworkingModule::didSendNetworkData( + uint32_t requestId, + int64_t progress, + int64_t total) { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didSendNetworkData", + [requestId, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = std::make_tuple( + requestId, + static_cast(progress), + static_cast(total)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +void NetworkingModule::didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers& headers, + const std::string& responseUrl) noexcept { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didReceiveNetworkResponse", + [requestId, statusCode, headers, responseUrl, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = + std::make_tuple(requestId, statusCode, headers, responseUrl); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +int64_t NetworkingModule::didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total) { + auto data = buf->moveToFbString().toStdString(); + auto bytesRead = data.size(); + emitDeviceEvent( + "didReceiveNetworkIncrementalData", + [requestId, data, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + data, + static_cast(progress), + static_cast(total))); + }); + return bytesRead; +}; + +void NetworkingModule::didReceiveNetworkData( + uint32_t requestId, + const std::string& /*responseType*/, + std::unique_ptr buf) { + if (requests_.isStopped()) { + return; + } + + auto responseData = buf->toString(); + emitDeviceEvent( + "didReceiveNetworkData", + [requestId, + responseData = std::move(responseData), + jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, static_cast(requestId), responseData)); + }); +} + +void NetworkingModule::didReceiveNetworkDataProgress( + uint32_t requestId, + int64_t bytesRead, + int64_t total) { + emitDeviceEvent( + "didReceiveNetworkDataProgress", + [requestId, bytesRead, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + static_cast(bytesRead), + static_cast(total))); + }); +} + +void NetworkingModule::didCompleteNetworkResponse( + uint32_t requestId, + const std::string& error, + bool timeoutError) { + if (requests_.isStopped() || !requests_.erase(requestId)) { + // If the HttpClient is shutting down, don't invoke the callback. + return; + } + emitDeviceEvent( + "didCompleteNetworkResponse", + [requestId, error, timeoutError, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + if (error.empty()) { + auto result = std::make_tuple(static_cast(requestId)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } else { + auto result = std::make_tuple( + static_cast(requestId), error, timeoutError); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } + }); +} + +void NetworkingModule::abortRequest(jsi::Runtime& /*rt*/, uint32_t requestId) { + requests_.cancel(requestId); +} + +void NetworkingModule::clearCookies( + jsi::Runtime& /*rt*/, + const AsyncCallback& /*callback*/) { + LOG(INFO) << "NetworkingModule::clearCookies"; +} + +void NetworkingModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& eventName) {} + +void NetworkingModule::removeListeners(jsi::Runtime& /*rt*/, uint32_t count) {} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h new file mode 100644 index 000000000000..00e16d5546fb --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h @@ -0,0 +1,143 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static http::FormDataField fromJs( + jsi::Runtime& rt, + const jsi::Object& value, + const std::shared_ptr& jsInvoker) { + auto fieldName = bridging::fromJs( + rt, value.getProperty(rt, "fieldName"), jsInvoker); + auto headers = bridging::fromJs( + rt, value.getProperty(rt, "headers"), jsInvoker); + auto string = bridging::fromJs>( + rt, value.getProperty(rt, "string"), jsInvoker); + auto uri = bridging::fromJs>( + rt, value.getProperty(rt, "uri"), jsInvoker); + return http::FormDataField{fieldName, headers, string, uri}; + } +}; + +template <> +struct Bridging { + static http::Body fromJs( + jsi::Runtime& rt, + const jsi::Object& value, + const std::shared_ptr& jsInvoker) { + return http::Body{ + bridging::fromJs>( + rt, value.getProperty(rt, "string"), jsInvoker), + bridging::fromJs>( + rt, value.getProperty(rt, "blob"), jsInvoker), + bridging::fromJs>( + rt, value.getProperty(rt, "formData"), jsInvoker), + bridging::fromJs>( + rt, value.getProperty(rt, "base64"), jsInvoker), + + }; + } +}; + +class Requests { + public: + Requests() noexcept = default; + ~Requests(); + Requests(Requests& other) = delete; + Requests& operator=(Requests& other) = delete; + Requests(Requests&& other) = delete; + Requests& operator=(Requests&& other) = delete; + + bool reserve(uint32_t id); + void store(uint32_t id, std::unique_ptr token); + bool erase(uint32_t id); + void cancel(uint32_t id); + void stop(); + bool isStopped(); + + private: + std::mutex tokensMutex_; + std::unordered_map> tokens_; + std::atomic stopped_{false}; +}; + +// Implementation of "Networking" TurboModule +class NetworkingModule + : public NativeNetworkingAndroidCxxSpec, + public std::enable_shared_from_this { + public: + NetworkingModule( + std::shared_ptr jsInvoker, + const HttpClientFactory& httpClientFactory); + + ~NetworkingModule() override; + + void sendRequest( + jsi::Runtime& rt, + const std::string& method, + const std::string& url, + uint32_t requestId, + const http::Headers& headers, + const http::Body& body, + const std::string& responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool withCredentials); + + void abortRequest(jsi::Runtime& rt, uint32_t requestId); + + void clearCookies(jsi::Runtime& rt, const AsyncCallback& callback); + + // RCTEventEmitter + void addListener(jsi::Runtime& rt, const std::string& eventName); + void removeListeners(jsi::Runtime& rt, uint32_t count); + + private: + void didSendNetworkData(uint32_t requestId, int64_t progress, int64_t total); + + void didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers& headers, + const std::string& responseUrl) noexcept; + + void didReceiveNetworkData( + uint32_t requestId, + const std::string& responseType, + std::unique_ptr buf); + + int64_t didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total); + + void didReceiveNetworkDataProgress( + uint32_t requestId, + int64_t bytesRead, + int64_t total); + + void didCompleteNetworkResponse( + uint32_t requestId, + const std::string& error, + bool timeoutError); + + std::unique_ptr httpClient_; + Requests requests_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp new file mode 100644 index 000000000000..7be3fa59d3a6 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" +#include +#include +#include + +namespace facebook::react { +bool ResourceLoader::isAbsolutePath(const std::string& path) { + return std::filesystem::path(path).is_absolute(); +} + +bool ResourceLoader::isDirectory(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::is_directory(path)) { + return true; + } + + return isResourceDirectory(path); +} + +bool ResourceLoader::isFile(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::exists(path) && + !std::filesystem::is_directory(path)) { + return true; + } + + return isResourceFile(path); +} + +std::string ResourceLoader::getFileContents(const std::string& path) { + if (isAbsolutePath(path)) { + std::ifstream file(path, std::ios::binary); + if (!file.good()) { + return getResourceFileContents(path); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + return getResourceFileContents(path); +} + +std::filesystem::path ResourceLoader::getCacheDirectory( + const std::string& path) { + auto root = getCacheRootPath() / CACHE_DIR; + if (!std::filesystem::exists(root)) { + try { + std::filesystem::create_directory(root); + } catch (...) { + LOG(ERROR) << "Failed to create root cache directory: " << root; + throw; + } + } + + if (path.empty()) { + return root; + } + + auto dir = root / path; + try { + std::filesystem::create_directories(dir); + return dir; + } catch (...) { + LOG(ERROR) << "Failed to create cache directory: " << dir; + throw; + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h new file mode 100644 index 000000000000..d4aa4f4c5606 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { +class ResourceLoader { + public: + static bool isDirectory(const std::string& path); + static bool isFile(const std::string& path); + static bool isAbsolutePath(const std::string& path); + static std::string getFileContents(const std::string& path); + static std::filesystem::path getCacheDirectory( + const std::string& path = std::string()); + + protected: + static bool isResourceDirectory(const std::string& path); + static bool isResourceFile(const std::string& path); + static std::string getResourceFileContents(const std::string& path); + + private: + static constexpr const auto CACHE_DIR = ".react-native-cxx-cache"; + static std::filesystem::path getCacheRootPath(); +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp new file mode 100644 index 000000000000..d11cab6e003e --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "WebSocketModule.h" + +#include + +namespace facebook::react { + +WebSocketModule::WebSocketModule( + std::shared_ptr jsInvoker, + WebSocketClientFactory webSocketClientFactory) + : NativeWebSocketModuleCxxSpec(jsInvoker), + webSocketClientFactory_(std::move(webSocketClientFactory)) {} + +WebSocketModule::~WebSocketModule() { + connections_.clear(); +} + +void WebSocketModule::connect( + jsi::Runtime& /*rt*/, + const std::string& url, + const std::optional>& /*protocols*/, + jsi::Object /*options*/, + int32_t socketID) { + auto webSocket = webSocketClientFactory_(); + connections_.emplace(socketID, std::move(webSocket)); + connections_[socketID]->setOnMessageCallback([weakThis = weak_from_this(), + socketID]( + const std::string& message) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + strongThis->emitDeviceEvent( + "websocketMessage", + [socketID, message = std::string(message)]( + jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, "type", facebook::jsi::String::createFromAscii(rt, "string")); + arg.setProperty( + rt, "data", facebook::jsi::String::createFromUtf8(rt, message)); + args.emplace_back(rt, arg); + }); + }); + connections_[socketID]->setOnClosedCallback( + [weakThis = weak_from_this(), socketID](const std::string& /*reason*/) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + strongThis->emitDeviceEvent( + "websocketClosed", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty(rt, "code", jsi::Value(0)); + arg.setProperty( + rt, + "reason", + facebook::jsi::String::createFromAscii(rt, "closed")); + args.emplace_back(rt, arg); + }); + }); + + connections_[socketID]->connect( + url, + [weakThis = weak_from_this(), socketID]( + bool success, const std::optional& /*message*/) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + if (success) { + strongThis->emitDeviceEvent( + "websocketOpen", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + args.emplace_back(rt, arg); + }); + } else { + strongThis->emitDeviceEvent( + "websocketFailed", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, + "message", + facebook::jsi::String::createFromAscii( + rt, "Could not connect to websocket.")); + args.emplace_back(rt, arg); + }); + } + }); +} + +void WebSocketModule::send( + jsi::Runtime& /*rt*/, + const std::string& message, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to send data to a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->send(message); +} + +void WebSocketModule::sendBinary( + jsi::Runtime& rt, + const std::string& base64String, + int32_t forSocketID) { + send(rt, base64String, forSocketID); +} + +void WebSocketModule::ping(jsi::Runtime& /*rt*/, int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to ping a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->ping(); +} + +void WebSocketModule::close( + jsi::Runtime& /*rt*/, + int32_t /*code*/, + const std::string& reason, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to close a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->close(reason); + connections_.erase(it); +} + +void WebSocketModule::addListener( + jsi::Runtime& rt, + const std::string& eventName) {} + +void WebSocketModule::removeListeners(jsi::Runtime& rt, int32_t count) {} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h new file mode 100644 index 000000000000..9483597bb262 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient; + +class WebSocketModule : public NativeWebSocketModuleCxxSpec, + public std::enable_shared_from_this { + public: + WebSocketModule( + std::shared_ptr jsInvoker, + WebSocketClientFactory webSocketClientFactory); + ~WebSocketModule() override; + WebSocketModule(WebSocketModule& other) = delete; + WebSocketModule& operator=(WebSocketModule& other) = delete; + WebSocketModule(WebSocketModule&& other) = delete; + WebSocketModule& operator=(WebSocketModule&& other) = delete; + + void connect( + jsi::Runtime& rt, + const std::string& url, + const std::optional>& protocols, + jsi::Object options, + int32_t socketID); + + void send(jsi::Runtime& rt, const std::string& message, int32_t socketID); + + void sendBinary( + jsi::Runtime& rt, + const std::string& base64String, + int32_t socketID); + + void ping(jsi::Runtime& rt, int32_t socketID); + + void close( + jsi::Runtime& rt, + int32_t code, + const std::string& reason, + int32_t socketID); + + void addListener(jsi::Runtime& rt, const std::string& eventName); + + void removeListeners(jsi::Runtime& rt, int32_t count); + + private: + WebSocketClientFactory webSocketClientFactory_; + std::unordered_map> connections_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp new file mode 100644 index 000000000000..8fd3a182d46b --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AssetManagerHelpers.h" + +#include +#include + +namespace facebook::react { + +jobject getAssetManagerObject(JNIEnv* env) { + auto contextObject = getApplication(env); + auto contextClass = env->GetObjectClass(contextObject); + auto getAssetsMethod = env->GetMethodID( + contextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + return env->CallObjectMethod(contextObject, getAssetsMethod); +} + +AAssetManager* getJavaAssetManager() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + return AAssetManager_fromJava(env, AssetManagerObject); +} + +bool isDirectoryNotEmpty(const std::string& path) { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + auto listMethodID = env->GetMethodID( + env->GetObjectClass(AssetManagerObject), + "list", + "(Ljava/lang/String;)[Ljava/lang/String;"); + + auto pathString = env->NewStringUTF(path.c_str()); + auto filesObject = static_cast( + env->CallObjectMethod(AssetManagerObject, listMethodID, pathString)); + env->DeleteLocalRef(pathString); + + auto length = env->GetArrayLength(filesObject); + for (int i = 0; i < length; i++) { + auto jstr = + static_cast(env->GetObjectArrayElement(filesObject, i)); + + const char* filename = env->GetStringUTFChars(jstr, nullptr); + + if (filename != nullptr) { + env->ReleaseStringUTFChars(jstr, filename); + env->DeleteLocalRef(jstr); + return true; + } + + env->DeleteLocalRef(jstr); + } + return false; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h new file mode 100644 index 000000000000..234cbd43393d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +AAssetManager* getJavaAssetManager(); + +bool isDirectoryNotEmpty(const std::string& path); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp new file mode 100644 index 000000000000..c6dedb6b4686 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" +#include "AssetManagerHelpers.h" + +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +AAssetManager* assetManager_ = nullptr; + +AAssetManager* getAssetManager() { + if (assetManager_ == nullptr) { + assetManager_ = getJavaAssetManager(); + } + + return assetManager_; +} +} // namespace + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + auto assetDir = AAssetManager_openDir(getAssetManager(), path.c_str()); + if (assetDir == nullptr) { + return false; + } + + bool exists = AAssetDir_getNextFileName(assetDir) != nullptr; + AAssetDir_close(assetDir); + if (exists) { + return true; + } + return isDirectoryNotEmpty(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + auto asset = AAssetManager_open( + getAssetManager(), path.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + return false; + } + + AAsset_close(asset); + return true; +} + +std::string ResourceLoader::getResourceFileContents(const std::string& path) { + auto asset = AAssetManager_open( + getAssetManager(), path.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + throw std::runtime_error("File not found " + path); + } + + std::string result( + (const char*)AAsset_getBuffer(asset), (size_t)AAsset_getLength(asset)); + AAsset_close(asset); + return result; +} + +std::filesystem::path ResourceLoader::getCacheRootPath() { + return getContext()->getCacheDir()->getAbsolutePath(); +} +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp new file mode 100644 index 000000000000..6d74f5bbb5a5 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" + +#include +#include +#include + +namespace facebook::react { + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + return std::filesystem::is_directory(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + return std::filesystem::exists(path) && !std::filesystem::is_directory(path); +} + +std::string ResourceLoader::getResourceFileContents(const std::string& path) { + std::ifstream file(path, std::ios::binary); + if (!file.good()) { + throw std::runtime_error("File not found " + path); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +std::filesystem::path ResourceLoader::getCacheRootPath() { + return std::filesystem::temp_directory_path(); +} +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp b/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp new file mode 100644 index 000000000000..b1c06a720282 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class TestCallInvoker : public CallInvoker { + public: + void invokeAsync(CallFunc&& fn) noexcept override { + queue_.push_back(std::move(fn)); + } + + void invokeSync(CallFunc&& /*func*/) override { + FAIL() << "JSCallInvoker does not support invokeSync()"; + } + + private: + std::list queue_; +}; + +class NetworkingModuleTests : public testing::Test { + protected: + void SetUp() override { + rt_ = facebook::hermes::makeHermesRuntime(); + jsInvoker_ = std::make_shared(); + } + + static void verifyFormData( + const http::FormDataField& formData, + const std::string& fieldName, + const std::string& string, + std::vector headerValues) { + EXPECT_EQ(formData.fieldName, fieldName); + EXPECT_EQ(formData.string, string); + for (int i = 0; i * 2 < headerValues.size(); ++i) { + EXPECT_EQ(formData.headers[i].first, headerValues[i * 2]); + EXPECT_EQ(formData.headers[i].second, headerValues[i * 2 + 1]); + } + }; + + std::unique_ptr rt_; + std::shared_ptr jsInvoker_; +}; + +// Test parsing a body with form data +TEST_F(NetworkingModuleTests, formDataTest) { + auto dynamic = folly::parseJson( + R"({ + "formData":[ + { + "fieldName":"field1", + "headers":[["header1","form-data; name=\"header1\""]], + "string":"string1" + }, + { + "fieldName":"field2", + "headers":[ + ["header1","form-data; name=\"header1\""], + ["header2","form-data; name=\"header2\""] + ], + "string":"string2" + } + ]})"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.formData.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.string.has_value()); + + auto formData = body.formData.value(); + EXPECT_EQ(formData.size(), 2); + verifyFormData( + formData[0], + "field1", + "string1", + {"header1", "form-data; name=\"header1\""}); + verifyFormData( + formData[1], + "field2", + "string2", + {"header1", + "form-data; name=\"header1\"", + "header2", + "form-data; name=\"header2\""}); +} + +// Test parsing a body with string data +TEST_F(NetworkingModuleTests, stringDataTest) { + auto dynamic = folly::parseJson( + R"({ + "string": "testString" + })"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.string.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.formData.has_value()); + + auto stringData = body.string.value(); + EXPECT_EQ(stringData, "testString"); +} + +} // namespace facebook::react From 48d4bd6183edbb036bc78f380ac56ffd17ed0804 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 18:47:42 -0700 Subject: [PATCH 4/4] Add devsupport target to ReactCxxPlatform (#51902) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51902 changelog: [internal] Reviewed By: andrewdacenko Differential Revision: D76240769 --- .../react/devsupport/DevLoadingViewModule.cpp | 46 ++++ .../react/devsupport/DevLoadingViewModule.h | 39 ++++ .../react/devsupport/DevServerHelper.cpp | 161 +++++++++++++ .../react/devsupport/DevServerHelper.h | 65 ++++++ .../react/devsupport/DevSettingsModule.cpp | 77 +++++++ .../react/devsupport/DevSettingsModule.h | 60 +++++ .../react/devsupport/IDevUIDelegate.h | 36 +++ .../react/devsupport/LogBoxModule.cpp | 42 ++++ .../react/devsupport/LogBoxModule.h | 28 +++ .../react/devsupport/PackagerConnection.cpp | 44 ++++ .../react/devsupport/PackagerConnection.h | 39 ++++ .../react/devsupport/SourceCodeModule.cpp | 23 ++ .../react/devsupport/SourceCodeModule.h | 37 +++ .../react/devsupport/inspector/Inspector.cpp | 214 ++++++++++++++++++ .../react/devsupport/inspector/Inspector.h | 68 ++++++ .../InspectorPackagerConnectionDelegate.cpp | 88 +++++++ .../InspectorPackagerConnectionDelegate.h | 64 ++++++ .../devsupport/inspector/InspectorThread.h | 29 +++ 18 files changed, 1160 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h create mode 100644 packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp new file mode 100644 index 000000000000..637c4d29edcf --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevLoadingViewModule.h" + +namespace facebook::react { + +const int32_t DEFAULT_TEXT_COLOR = 0xFFFFFFFF; +const int32_t DEFAULT_BACKGROUND_COLOR = 0xFF2584E8; + +DevLoadingViewModule::DevLoadingViewModule( + std::shared_ptr jsInvoker, + std::weak_ptr devUIDelegate) + : NativeDevLoadingViewCxxSpec(jsInvoker), + devUIDelegate_(std::move(devUIDelegate)) {} + +DevLoadingViewModule::~DevLoadingViewModule() { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->hideLoadingView(); + } +} + +void DevLoadingViewModule::showMessage( + jsi::Runtime& /*rt*/, + const std::string& message, + std::optional textColor, + std::optional backgroundColor) { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->showLoadingView( + message, + SharedColor{textColor.value_or(DEFAULT_TEXT_COLOR)}, + SharedColor{backgroundColor.value_or(DEFAULT_BACKGROUND_COLOR)}); + } +} + +void DevLoadingViewModule::hide(jsi::Runtime& /*rt*/) { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->hideLoadingView(); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h b/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h new file mode 100644 index 000000000000..9fa51278f7c4 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "IDevUIDelegate.h" + +#include +#include +#include + +namespace facebook::react { + +class DevLoadingViewModule + : public NativeDevLoadingViewCxxSpec { + public: + DevLoadingViewModule( + std::shared_ptr jsInvoker, + std::weak_ptr devUIDelegate); + + ~DevLoadingViewModule() override; + + void showMessage( + jsi::Runtime& rt, + const std::string& message, + std::optional textColor, + std::optional backgroundColor); + + void hide(jsi::Runtime& rt); + + private: + std::weak_ptr devUIDelegate_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp new file mode 100644 index 000000000000..17ffe4b43f0f --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevServerHelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::string_view DEFAULT_PLATFORM = "android"; + +std::string SHA256(const std::string& input) { + std::array hash{}; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, input.c_str(), input.size()); + SHA256_Final(hash.data(), &sha256); + std::stringstream ss; + for (unsigned char i : hash) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)i; + } + return ss.str(); +} + +std::string urlEscape(const std::string& str) { + std::regex pattern("[^a-zA-Z0-9._~-]"); + return std::regex_replace(str, pattern, ""); +} + +} // namespace + +namespace facebook::react { + +DevServerHelper::DevServerHelper( + std::string appId, + std::string deviceName, + const HttpClientFactory& httpClientFactory, + JavaScriptModuleCallback javaScriptModuleCallback) noexcept + : appId_(std::move(appId)), + deviceName_(std::move(deviceName)), + httpClient_(httpClientFactory()), + javaScriptModuleCallback_(std::move(javaScriptModuleCallback)) { + deviceId_ = SHA256(fmt::format("{}-{}", deviceName_, appId_)); +} + +std::future DevServerHelper::downloadBundleResourceSync( + const std::string& jsBundleUrl, + DownloadProgressCallback&& downloadProgressCallback) { + auto promise = std::make_shared>(); + http::NetworkCallbacks callbacks{ + .onBody = + [jsBundleUrl, promise, downloadProgressCallback]( + std::unique_ptr ioBuf) { + std::string responseStr = ioBuf->moveToFbString().toStdString(); + promise->set_value(responseStr); + }, + .onResponseComplete = + [jsBundleUrl, promise, downloadProgressCallback]( + const std::string& error, bool timeoutError) { + if (!error.empty() || timeoutError) { + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FAILED); + } + std::string errorMessage = + "Failed to download JS bundle from Url: " + jsBundleUrl + + ". Error: " + (error.empty() ? "Timeout" : error); + LOG(WARNING) << errorMessage; + try { + throw std::runtime_error(errorMessage); + } catch (...) { + try { + promise->set_exception(std::current_exception()); + } catch (...) { + } + } + } else if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FINISHED); + } + }}; + httpClient_->sendRequest(std::move(callbacks), "GET", jsBundleUrl); + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::STARTED); + } + return promise->get_future(); +} + +std::string DevServerHelper::getInspectorUrl() const { + bool isProfilingBuild = + jsinspector_modern::InspectorFlags::getInstance().getIsProfilingBuild(); + + return fmt::format( + "ws://{}:{}/inspector/device?name={}&app={}&device={}&profiling={}", + DEFAULT_DEV_SERVER_HOST, + DEFAULT_DEV_SERVER_PORT, + urlEscape(deviceName_), + appId_, + deviceId_, + isProfilingBuild); +} + +std::string DevServerHelper::getBundleUrl() const { + if (sourcePath_.empty()) { + return ""; + } + + bool dev = true; + bool lazy = dev; + bool minify = false; + bool splitBundle = false; + bool modulesOnly = splitBundle; + bool runModule = !splitBundle; + return fmt::format( + "http://{}:{}/{}.bundle?platform={}&dev={}&lazy={}&minify={}&app={}&modulesOnly={}&runModule={}&inlineSourceMap=false&excludeSource=true&sourcePaths=url-server", + DEFAULT_DEV_SERVER_HOST, + DEFAULT_DEV_SERVER_PORT, + sourcePath_, + DEFAULT_PLATFORM, + dev, + lazy, + minify, + appId_, + modulesOnly, + runModule); +}; + +std::string DevServerHelper::getPackagerConnectionUrl() const { + return fmt::format( + "ws://{}:{}/message", DEFAULT_DEV_SERVER_HOST, DEFAULT_DEV_SERVER_PORT); +} + +void DevServerHelper::openDebugger() const { + auto requestUrl = fmt::format( + "http://{}:{}/open-debugger?device={}", + DEFAULT_DEV_SERVER_HOST, + DEFAULT_DEV_SERVER_PORT, + deviceId_); + httpClient_->sendRequest({}, "POST", requestUrl); +} + +void DevServerHelper::setupHMRClient() const { + folly::dynamic params = folly::dynamic::array( + DEFAULT_PLATFORM, + sourcePath_, + DEFAULT_DEV_SERVER_HOST, + DEFAULT_DEV_SERVER_PORT, + true /*enable*/); + javaScriptModuleCallback_("HMRClient", "setup", std::move(params)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.h b/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.h new file mode 100644 index 000000000000..1b7ffc255f53 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevServerHelper.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +constexpr std::string_view DEFAULT_DEV_SERVER_HOST = "localhost"; +constexpr uint32_t DEFAULT_DEV_SERVER_PORT = 8081; + +} // namespace + +class DevServerHelper { + public: + enum class DownloadProgressStatus : short { STARTED, FAILED, FINISHED }; + using DownloadProgressCallback = std::function; + + DevServerHelper( + std::string appId, + std::string deviceName, + const HttpClientFactory& httpClientFactory, + JavaScriptModuleCallback javaScriptModuleCallback) noexcept; + ~DevServerHelper() noexcept = default; + + std::future downloadBundleResourceSync( + const std::string& jsBundleUrl, + DownloadProgressCallback&& downloadProgressCallback = nullptr); + + std::string getInspectorUrl() const; + + std::string getBundleUrl() const; + + std::string getPackagerConnectionUrl() const; + + void openDebugger() const; + + void setSourcePath(const std::string& sourcePath) { + sourcePath_ = sourcePath; + } + + void setupHMRClient() const; + + private: + std::string appId_; + std::string deviceName_; + std::unique_ptr httpClient_; + JavaScriptModuleCallback javaScriptModuleCallback_; + std::string deviceId_; + std::string sourcePath_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp new file mode 100644 index 000000000000..8de02e4a6d7b --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevSettingsModule.h" + +#include + +namespace facebook::react { + +void DevSettingsModule::reload(jsi::Runtime& /*rt*/) { + LOG(INFO) << "DevSettingsModule::reload"; + if (liveReloadCallback_) { + liveReloadCallback_(); + } +} + +void DevSettingsModule::reloadWithReason( + jsi::Runtime& /*rt*/, + const std::string& reason) { + LOG(INFO) << "DevSettingsModule::reloadWithReason: " << reason; + if (liveReloadCallback_) { + liveReloadCallback_(); + } +} + +void DevSettingsModule::onFastRefresh(jsi::Runtime& /*rt*/) { + LOG(INFO) << "DevSettingsModule::onFastRefresh"; +} + +void DevSettingsModule::setHotLoadingEnabled( + jsi::Runtime& /*rt*/, + bool isHotLoadingEnabled) { + LOG(INFO) << "DevSettingsModule::setHotLoadingEnabled: " + << (int)isHotLoadingEnabled; +} + +void DevSettingsModule::setIsDebuggingRemotely( + jsi::Runtime& /*rt*/, + bool /*isDebuggingRemotelyEnabled*/) {} + +void DevSettingsModule::setProfilingEnabled( + jsi::Runtime& /*rt*/, + bool /*isProfilingEnabled*/) {} + +void DevSettingsModule::toggleElementInspector(jsi::Runtime& rt) {} + +void DevSettingsModule::addMenuItem( + jsi::Runtime& /*rt*/, + const std::string& /*title*/) {} + +void DevSettingsModule::setIsShakeToShowDevMenuEnabled( + jsi::Runtime& /*rt*/, + bool /*enabled*/) {} + +void DevSettingsModule::openDebugger(jsi::Runtime& /*rt*/) { + if (auto devServerHelper = devServerHelper_.lock()) { + devServerHelper->openDebugger(); + } +} + +void DevSettingsModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) { + // noop +} + +void DevSettingsModule::removeListeners( + jsi::Runtime& /*rt*/, + double /*count*/) { + // noop +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.h b/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.h new file mode 100644 index 000000000000..ef954b1443b5 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/DevSettingsModule.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class DevSettingsModule : public NativeDevSettingsCxxSpec { + using LiveReloadCallback = std::function; + + public: + DevSettingsModule( + std::shared_ptr jsInvoker, + std::weak_ptr devServerHelper, + LiveReloadCallback&& liveReloadCallback) + : NativeDevSettingsCxxSpec(jsInvoker), + devServerHelper_(std::move(devServerHelper)), + liveReloadCallback_(std::move(liveReloadCallback)) {} + + void reload(jsi::Runtime& rt); + + void reloadWithReason(jsi::Runtime& rt, const std::string& reason); + + void onFastRefresh(jsi::Runtime& rt); + + void setHotLoadingEnabled(jsi::Runtime& rt, bool isHotLoadingEnabled); + + void setIsDebuggingRemotely( + jsi::Runtime& rt, + bool isDebuggingRemotelyEnabled); + + void setProfilingEnabled(jsi::Runtime& rt, bool isProfilingEnabled); + + void toggleElementInspector(jsi::Runtime& rt); + + void addMenuItem(jsi::Runtime& rt, const std::string& title); + + void setIsShakeToShowDevMenuEnabled(jsi::Runtime& rt, bool enabled); + + void openDebugger(jsi::Runtime& rt); + + void addListener(jsi::Runtime& rt, const std::string& eventName); + + void removeListeners(jsi::Runtime& rt, double count); + + private: + std::weak_ptr devServerHelper_; + LiveReloadCallback liveReloadCallback_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h b/packages/react-native/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h new file mode 100644 index 000000000000..daf8ecfc0b1d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +struct IDevUIDelegate { + virtual ~IDevUIDelegate() noexcept = default; + + virtual void showDownloadBundleProgress() = 0; + + virtual void hideDownloadBundleProgress() = 0; + + virtual void showLoadingView( + const std::string& message, + SharedColor textColor, + SharedColor backgroundColor) = 0; + + virtual void hideLoadingView() = 0; + + virtual void showDebuggerOverlay( + std::function&& resumeDebuggerFn) = 0; + + virtual void hideDebuggerOverlay() = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp new file mode 100644 index 000000000000..745fb3953cd4 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LogBoxModule.h" + +#include + +namespace facebook::react { + +LogBoxModule::LogBoxModule( + std::shared_ptr jsInvoker, + std::shared_ptr surfaceDelegate) + : NativeLogBoxCxxSpec(jsInvoker), + surfaceDelegate_(std::move(surfaceDelegate)) { + LOG(INFO) << "LogBoxModule initialized"; + surfaceDelegate_->createContentView("LogBox"); +} + +LogBoxModule::~LogBoxModule() { + surfaceDelegate_->destroyContentView(); +} + +void LogBoxModule::show(jsi::Runtime& /*rt*/) { + LOG(INFO) << "LogBoxModule show"; + if (surfaceDelegate_->isContentViewReady() && + !surfaceDelegate_->isShowing()) { + surfaceDelegate_->show(); + } +} + +void LogBoxModule::hide(jsi::Runtime& /*rt*/) { + LOG(INFO) << "LogBoxModule hide"; + if (surfaceDelegate_->isShowing()) { + surfaceDelegate_->hide(); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.h b/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.h new file mode 100644 index 000000000000..1ace8f9d6527 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/LogBoxModule.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { +class LogBoxModule : public NativeLogBoxCxxSpec { + public: + LogBoxModule( + std::shared_ptr jsInvoker, + std::shared_ptr surfaceDelegate); + ~LogBoxModule() override; + + void show(jsi::Runtime& rt); + + void hide(jsi::Runtime& rt); + + private: + std::shared_ptr surfaceDelegate_; +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp new file mode 100644 index 000000000000..05676162f68f --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PackagerConnection.h" + +#include +#include +#include + +namespace facebook::react { + +PackagerConnection::PackagerConnection( + const WebSocketClientFactory& webSocketClientFactory, + const std::string& packagerConnectionUrl, + LiveReloadCallback&& liveReloadCallback, + ShowDevMenuCallback&& showDevMenuCallback) + : liveReloadCallback_(std::move(liveReloadCallback)), + showDevMenuCallback_(std::move(showDevMenuCallback)) { + websocket_ = webSocketClientFactory(); + websocket_->setOnMessageCallback([this](const std::string& message) { + LOG(INFO) << "Received message from packager: " << message; + auto json = nlohmann::json::parse(message); + if (json.is_null() || json["version"] != 2) { + return; + } + auto method = json["method"]; + if (method == "reload") { + liveReloadCallback_(); + } else if (method == "showDevMenu") { + showDevMenuCallback_(); + } + }); + websocket_->connect(packagerConnectionUrl); +} + +PackagerConnection::~PackagerConnection() noexcept { + websocket_->close("PackagerConnection destroyed"); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h new file mode 100644 index 000000000000..8c53f6f6cc3c --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class PackagerConnection { + using LiveReloadCallback = std::function; + using ShowDevMenuCallback = std::function; + + public: + PackagerConnection( + const WebSocketClientFactory& webSocketClientFactory, + const std::string& packagerConnectionUrl, + LiveReloadCallback&& liveReloadCallback, + ShowDevMenuCallback&& showDevMenuCallback); + ~PackagerConnection() noexcept; + PackagerConnection(const PackagerConnection& other) = delete; + PackagerConnection& operator=(PackagerConnection& other) = delete; + PackagerConnection(PackagerConnection&& other) = delete; + PackagerConnection& operator=(PackagerConnection&& other) = delete; + + private: + const LiveReloadCallback liveReloadCallback_; + const ShowDevMenuCallback showDevMenuCallback_; + std::unique_ptr websocket_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp new file mode 100644 index 000000000000..ba2bc8d3df36 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "SourceCodeModule.h" + +#include +#include + +namespace facebook::react { + +SourceCodeConstants SourceCodeModule::getConstants(jsi::Runtime& /*rt*/) { + std::string scriptURL; + if (auto devServerHelper = devServerHelper_.lock()) { + scriptURL = devServerHelper->getBundleUrl(); + } + return SourceCodeConstants{.scriptURL = scriptURL}; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h new file mode 100644 index 000000000000..bbe4931ebe4f --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class DevServerHelper; + +using SourceCodeConstants = NativeSourceCodeSourceCodeConstants; + +template <> +struct Bridging + : NativeSourceCodeSourceCodeConstantsBridging {}; + +class SourceCodeModule : public NativeSourceCodeCxxSpec { + public: + explicit SourceCodeModule( + std::shared_ptr jsInvoker, + std::shared_ptr devServerHelper = nullptr) + : NativeSourceCodeCxxSpec(jsInvoker), devServerHelper_(devServerHelper) {} + + SourceCodeConstants getConstants(jsi::Runtime& rt); + + private: + std::weak_ptr devServerHelper_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp new file mode 100644 index 000000000000..743aa3ff2c2a --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "Inspector.h" +#include "InspectorPackagerConnectionDelegate.h" + +#include +#include + +namespace facebook::react { + +namespace { + +constexpr std::string_view INTEGRATION_NAME = "RNHOST"; + +class InspectorHostTargetDelegate + : public jsinspector_modern::HostTargetDelegate, + public std::enable_shared_from_this { + public: + InspectorHostTargetDelegate( + std::string appName, + std::string deviceName, + const HttpClientFactory& httpClientFactory, + LiveReloadCallbackFn&& liveReloadCallbackFn, + ToggleDebuggerOverlayFn&& toggleDebuggerOverlayFn) noexcept + : appName_(std::move(appName)), + deviceName_(std::move(deviceName)), + httpClient_(httpClientFactory()), + liveReloadCallbackFn_(std::move(liveReloadCallbackFn)), + toggleDebuggerOverlayFn_(std::move(toggleDebuggerOverlayFn)) {} + + jsinspector_modern::HostTargetMetadata getMetadata() override { + return { + .appIdentifier = appName_, + .deviceName = deviceName_, + .integrationName = std::string(INTEGRATION_NAME), + }; + } + + void onReload(const jsinspector_modern::HostTargetDelegate::PageReloadRequest& + /*request*/) override { + if (liveReloadCallbackFn_ != nullptr) { + liveReloadCallbackFn_(); + } + } + + void onSetPausedInDebuggerMessage( + const jsinspector_modern::HostTargetDelegate:: + OverlaySetPausedInDebuggerMessageRequest& request) override { + if (toggleDebuggerOverlayFn_ != nullptr) { + toggleDebuggerOverlayFn_( + request.message.has_value(), [target = target_]() { + if (auto strongTarget = target.lock()) { + strongTarget->sendCommand( + jsinspector_modern::HostCommand::DebuggerResume); + } + }); + } + } + + void loadNetworkResource( + const jsinspector_modern::LoadNetworkResourceRequest& params, + jsinspector_modern::ScopedExecutor< + jsinspector_modern::NetworkRequestListener> executor) override { + http::NetworkCallbacks callbacks{ + .onResponse = + [executor](uint32_t responseCode, const http::Headers& headers) { + executor([responseCode, + headers](jsinspector_modern::NetworkRequestListener& + listener) { + facebook::react::jsinspector_modern::Headers responseHeaders; + for (const auto& [key, value] : headers) { + responseHeaders[key] = value; + } + listener.onHeaders(responseCode, responseHeaders); + }); + }, + .onBody = + [executor](std::unique_ptr ioBuf) { + executor( + [response = ioBuf->moveToFbString().toStdString()]( + jsinspector_modern::NetworkRequestListener& listener) { + listener.onData(response); + }); + }, + .onResponseComplete = + [executor, url = params.url]( + const std::string& error, bool timeout) { + if (error.empty() && !timeout) { + executor( + [](jsinspector_modern::NetworkRequestListener& listener) { + listener.onCompletion(); + }); + } else { + executor( + [errorMessage = + "Failed to download JS bundle from Url: " + url + + ". Error: " + (error.empty() ? "Timeout" : error)]( + jsinspector_modern::NetworkRequestListener& listener) { + listener.onError(errorMessage); + }); + } + }}; + httpClient_->sendRequest(std::move(callbacks), "GET", params.url); + } + + void setTarget(std::weak_ptr target) { + target_ = std::move(target); + } + + private: + std::string appName_; + std::string deviceName_; + std::unique_ptr httpClient_; + LiveReloadCallbackFn liveReloadCallbackFn_; + ToggleDebuggerOverlayFn toggleDebuggerOverlayFn_; + std::weak_ptr target_; +}; + +} // namespace + +Inspector::Inspector( + std::string appName, + std::string deviceName, + WebSocketClientFactory webSocketClientFactory, + HttpClientFactory httpClientFactory) noexcept + : appName_(std::move(appName)), + deviceName_(std::move(deviceName)), + webSocketClientFactory_(std::move(webSocketClientFactory)), + httpClientFactory_(std::move(httpClientFactory)) {} + +Inspector::~Inspector() noexcept { + if (pageId_) { + jsinspector_modern::getInspectorInstance().removePage(*pageId_); + taskDispatchThread_.runSync([weakThis = weak_from_this()]() { + if (auto strongThis = weakThis.lock()) { + strongThis->pageId_.reset(); + strongThis->target_.reset(); + } + }); + } + taskDispatchThread_.quit(); +} + +void Inspector::connectDebugger(const std::string& inspectorUrl) noexcept { + if (!packagerConnection_) { + packagerConnection_ = + std::make_unique( + inspectorUrl, + deviceName_, + appName_, + std::make_unique( + weak_from_this(), webSocketClientFactory_)); + } + if (!packagerConnection_->isConnected()) { + packagerConnection_->connect(); + } +} + +void Inspector::ensureHostTarget( + LiveReloadCallbackFn&& liveReloadCallbackFn, + ToggleDebuggerOverlayFn&& toggleDebuggerOverlayFn) noexcept { + hostDelegate_ = std::make_shared( + appName_, + deviceName_, + httpClientFactory_, + std::move(liveReloadCallbackFn), + std::move(toggleDebuggerOverlayFn)); + target_ = jsinspector_modern::HostTarget::create( + *hostDelegate_, + [weakThis = weak_from_this()](std::function&& callback) { + if (auto strongThis = weakThis.lock()) { + strongThis->invokeElsePost(std::move(callback)); + } + }); + static_cast(*hostDelegate_).setTarget(target_); + jsinspector_modern::InspectorTargetCapabilities capabilities{ + .nativePageReloads = true, .prefersFuseboxFrontend = true}; + pageId_ = jsinspector_modern::getInspectorInstance().addPage( + std::string(INTEGRATION_NAME), + "", /*vm*/ + [weakInspectorTarget = std::weak_ptr(target_)]( + std::unique_ptr remote) + -> std::unique_ptr { + if (auto inspectorTarget = weakInspectorTarget.lock()) { + return inspectorTarget->connect(std::move(remote)); + } + // Reject the connection on destruction + return nullptr; + }, + capabilities); +} + +void Inspector::invokeElsePost( + TaskDispatchThread::TaskFn&& callback, + std::chrono::milliseconds delayMs) { + if (taskDispatchThread_.isOnThread() && + delayMs == std::chrono::milliseconds::zero()) { + callback(); + } else { + taskDispatchThread_.runAsync(std::move(callback), delayMs); + } +} + +std::shared_ptr Inspector::inspectorTarget() + const { + return target_; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.h b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.h new file mode 100644 index 000000000000..22fb9bffe4c8 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "InspectorThread.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +using ToggleDebuggerOverlayFn = + std::function&&)>; + +using LiveReloadCallbackFn = std::function; + +class Inspector : public InspectorThread, + public std::enable_shared_from_this { + public: + Inspector( + std::string appName, + std::string deviceName, + WebSocketClientFactory webSocketClientFactory, + HttpClientFactory httpClientFactory) noexcept; + ~Inspector() noexcept override; + Inspector(const Inspector& other) = delete; + Inspector& operator=(Inspector& other) = delete; + Inspector(Inspector&& other) = delete; + Inspector& operator=(Inspector&& other) = delete; + + void connectDebugger(const std::string& inspectorUrl) noexcept; + + void ensureHostTarget( + LiveReloadCallbackFn&& liveReloadCallbackFn, + ToggleDebuggerOverlayFn&& toggleDebuggerOverlayFn) noexcept; + + std::shared_ptr inspectorTarget() const; + + private: + void invokeElsePost( + TaskDispatchThread::TaskFn&& callback, + std::chrono::milliseconds delayMs = + std::chrono::milliseconds::zero()) override; + + TaskDispatchThread taskDispatchThread_{"InspectorThread"}; + std::string appName_; + std::string deviceName_; + WebSocketClientFactory webSocketClientFactory_; + HttpClientFactory httpClientFactory_; + std::shared_ptr hostDelegate_; + std::shared_ptr target_; + std::optional pageId_; + std::unique_ptr + packagerConnection_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp new file mode 100644 index 000000000000..2b260785944c --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "InspectorPackagerConnectionDelegate.h" + +#include +#include + +namespace facebook::react { + +InspectorPackagerConnectionDelegate::WebSocket::WebSocket( + const std::string& url, + std::weak_ptr webSocketDelegate, + std::weak_ptr inspectorThread, + const WebSocketClientFactory& webSocketClientFactory) + : webSocketDelegate_{webSocketDelegate}, + inspectorThread_(std::move(inspectorThread)) { + websocket_ = webSocketClientFactory(); + websocket_->setOnMessageCallback( + [webSocketDelegate](const std::string& message) { + if (const auto strongDelegate = webSocketDelegate.lock()) { + strongDelegate->didReceiveMessage(message); + } + }); + websocket_->setOnClosedCallback( + [webSocketDelegate](const std::string& /*message*/) { + if (const auto strongDelegate = webSocketDelegate.lock()) { + strongDelegate->didClose(); + } + }); + websocket_->connect( + url, [webSocketDelegate](bool success, const std::string& message) { + const auto strongDelegate = webSocketDelegate.lock(); + if (!strongDelegate) { + return; + } + + if (success) { + strongDelegate->didOpen(); + } else { + strongDelegate->didFailWithError(std::nullopt, message); + } + }); +} + +InspectorPackagerConnectionDelegate::WebSocket::~WebSocket() { + websocket_->close("InspectorPackagerConnectionDelegate destroyed"); +}; + +void InspectorPackagerConnectionDelegate::WebSocket::send( + std::string_view message) { + if (auto strongInspectorThread = inspectorThread_.lock()) { + strongInspectorThread->invokeElsePost( + [this, message = std::string(message)]() { + if (websocket_) { + websocket_->send(message); + } + }); + } +} + +InspectorPackagerConnectionDelegate::InspectorPackagerConnectionDelegate( + std::weak_ptr inspectorThread, + WebSocketClientFactory webSocketClientFactory) noexcept + : inspectorThread_(std::move(inspectorThread)), + webSocketClientFactory_(std::move(webSocketClientFactory)) {} + +std::unique_ptr +InspectorPackagerConnectionDelegate::connectWebSocket( + const std::string& url, + std::weak_ptr delegate) { + return std::make_unique( + url, delegate, inspectorThread_, webSocketClientFactory_); +} + +void InspectorPackagerConnectionDelegate::scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs) { + if (auto inspectorThread = inspectorThread_.lock()) { + inspectorThread->invokeElsePost(std::move(callback), delayMs); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h new file mode 100644 index 000000000000..871e82eb1753 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "InspectorThread.h" + +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient; + +class InspectorPackagerConnectionDelegate final + : public jsinspector_modern::InspectorPackagerConnectionDelegate { + class WebSocket : public jsinspector_modern::IWebSocket { + public: + WebSocket( + const std::string& url, + std::weak_ptr webSocketDelegate, + std::weak_ptr inspectorThread, + const WebSocketClientFactory& webSocketClientFactory); + ~WebSocket() override; + WebSocket(const WebSocket& other) = delete; + WebSocket& operator=(WebSocket& other) = delete; + WebSocket(WebSocket&& other) = delete; + WebSocket& operator=(WebSocket&& other) = delete; + + void send(std::string_view message) override; + + private: + std::weak_ptr webSocketDelegate_; + std::weak_ptr inspectorThread_; + std::unique_ptr websocket_; + }; + + public: + InspectorPackagerConnectionDelegate( + std::weak_ptr inspectorThread, + WebSocketClientFactory webSocketClientFactory) noexcept; + + std::unique_ptr connectWebSocket( + const std::string& url, + std::weak_ptr delegate) override; + + void scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs = + std::chrono::milliseconds::zero()) override; + + private: + std::weak_ptr inspectorThread_; + WebSocketClientFactory webSocketClientFactory_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h new file mode 100644 index 000000000000..e6b25fe8fbcc --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class InspectorThread { + public: + InspectorThread() noexcept = default; + virtual ~InspectorThread() = default; + InspectorThread(const InspectorThread& other) = delete; + InspectorThread& operator=(InspectorThread& other) = delete; + InspectorThread(InspectorThread&& other) = delete; + InspectorThread& operator=(InspectorThread&& other) = delete; + + virtual void invokeElsePost( + TaskDispatchThread::TaskFn&& callback, + std::chrono::milliseconds delayMs = std::chrono::milliseconds(0)) = 0; +}; + +} // namespace facebook::react