diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm index 64b9ccceca8..b58482512a8 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm @@ -29,6 +29,7 @@ #import #endif #import +#import @interface RCTAppDelegate () @end @@ -210,6 +211,10 @@ return std::make_shared(jsInvoker); } + if (name == facebook::react::NativeMicrotasks::kModuleName) { + return std::make_shared(jsInvoker); + } + return nullptr; } diff --git a/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec b/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec index 4ddc800c2d7..24b7368cb71 100644 --- a/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec +++ b/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec @@ -78,6 +78,7 @@ Pod::Spec.new do |s| s.dependency "React-nativeconfig" s.dependency "ReactCodegen" s.dependency "React-featureflagsnativemodule" + s.dependency "React-microtasksnativemodule" add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) add_dependency(s, "React-NativeModulesApple") diff --git a/packages/react-native/Libraries/Core/setUpTimers.js b/packages/react-native/Libraries/Core/setUpTimers.js index 86d73869f5e..db0ac5dc1e7 100644 --- a/packages/react-native/Libraries/Core/setUpTimers.js +++ b/packages/react-native/Libraries/Core/setUpTimers.js @@ -10,7 +10,9 @@ 'use strict'; -const {isNativeFunction} = require('../Utilities/FeatureDetection'); +const ReactNativeFeatureFlags = require('../../src/private/featureflags/ReactNativeFeatureFlags'); +const NativeReactNativeFeatureFlags = + require('../../src/private/featureflags/specs/NativeReactNativeFeatureFlags').default; const {polyfillGlobal} = require('../Utilities/PolyfillFunctions'); if (__DEV__) { @@ -19,14 +21,6 @@ if (__DEV__) { } } -// Currently, Hermes `Promise` is implemented via Internal Bytecode. -const hasHermesPromiseQueuedToJSVM = - global.HermesInternal?.hasPromise?.() === true && - global.HermesInternal?.useEngineQueue?.() === true; - -const hasNativePromise = isNativeFunction(Promise); -const hasPromiseQueuedToJSVM = hasNativePromise || hasHermesPromiseQueuedToJSVM; - // In bridgeless mode, timers are host functions installed from cpp. if (global.RN$Bridgeless !== true) { /** @@ -56,13 +50,33 @@ if (global.RN$Bridgeless !== true) { defineLazyTimer('cancelIdleCallback'); } -/** - * Set up immediate APIs, which is required to use the same microtask queue - * as the Promise. - */ -if (hasPromiseQueuedToJSVM) { - // When promise queues to the JSVM microtasks queue, we shim the immediate - // APIs via `queueMicrotask` to maintain the backward compatibility. +// We need to check if the native module is available before accessing the +// feature flag, because otherwise the API would throw an error in the legacy +// architecture in OSS, where the native module isn't available. +if ( + NativeReactNativeFeatureFlags != null && + ReactNativeFeatureFlags.enableMicrotasks() +) { + // This is the flag that tells React to use `queueMicrotask` to batch state + // updates, instead of using the scheduler to schedule a regular task. + // We use a global variable because we don't currently have any other + // mechanism to pass feature flags from RN to React in OSS. + global.RN$enableMicrotasksInReact = true; + + polyfillGlobal('queueMicrotask', () => { + const nativeQueueMicrotask = + require('../../src/private/webapis/microtasks/specs/NativeMicrotasks') + .default?.queueMicrotask; + if (nativeQueueMicrotask) { + return nativeQueueMicrotask; + } else { + // For backwards-compatibility + return global.HermesInternal?.enqueueJob; + } + }); + + // We shim the immediate APIs via `queueMicrotask` to maintain the backward + // compatibility. polyfillGlobal( 'setImmediate', () => require('./Timers/immediateShim').setImmediate, @@ -72,6 +86,12 @@ if (hasPromiseQueuedToJSVM) { () => require('./Timers/immediateShim').clearImmediate, ); } else { + // Polyfill it with promise (regardless it's polyfilled or native) otherwise. + polyfillGlobal( + 'queueMicrotask', + () => require('./Timers/queueMicrotask.js').default, + ); + // When promise was polyfilled hence is queued to the RN microtask queue, // we polyfill the immediate APIs as aliases to the ReactNativeMicrotask APIs. // Note that in bridgeless mode, immediate APIs are installed from cpp. @@ -86,18 +106,3 @@ if (hasPromiseQueuedToJSVM) { ); } } - -/** - * Set up the microtask queueing API, which is required to use the same - * microtask queue as the Promise. - */ -if (hasHermesPromiseQueuedToJSVM) { - // Fast path for Hermes. - polyfillGlobal('queueMicrotask', () => global.HermesInternal?.enqueueJob); -} else { - // Polyfill it with promise (regardless it's polyfilled or native) otherwise. - polyfillGlobal( - 'queueMicrotask', - () => require('./Timers/queueMicrotask.js').default, - ); -} diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 8e30da2cda6..af48dbd60ee 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -102,6 +102,7 @@ add_react_common_subdir(react/bridging) add_react_common_subdir(react/renderer/mapbuffer) add_react_common_subdir(react/nativemodule/core) add_react_common_subdir(react/nativemodule/featureflags) +add_react_common_subdir(react/nativemodule/microtasks) add_react_common_subdir(jserrorhandler) add_react_common_subdir(react/runtime) add_react_common_subdir(react/runtime/hermes) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt index 8ecdfadb9b4..d541aa2305c 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt @@ -22,4 +22,5 @@ target_link_libraries(react_newarchdefaults react_codegen_rncore react_cxxreactpackage react_nativemodule_featureflags + react_nativemodule_microtasks jsi) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/DefaultTurboModuleManagerDelegate.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/DefaultTurboModuleManagerDelegate.cpp index ce5c07bf97d..7a291d71821 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/DefaultTurboModuleManagerDelegate.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/DefaultTurboModuleManagerDelegate.cpp @@ -10,6 +10,7 @@ #include #include +#include #include namespace facebook::react { @@ -78,6 +79,10 @@ std::shared_ptr DefaultTurboModuleManagerDelegate::getTurboModule( return std::make_shared(jsInvoker); } + if (name == NativeMicrotasks::kModuleName) { + return std::make_shared(jsInvoker); + } + return nullptr; } diff --git a/packages/react-native/ReactCommon/react/nativemodule/microtasks/CMakeLists.txt b/packages/react-native/ReactCommon/react/nativemodule/microtasks/CMakeLists.txt new file mode 100644 index 00000000000..80fe7fdc29e --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/microtasks/CMakeLists.txt @@ -0,0 +1,24 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"ReactNative\") + +file(GLOB react_nativemodule_microtasks_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_nativemodule_microtasks SHARED ${react_nativemodule_microtasks_SRC}) + +target_include_directories(react_nativemodule_microtasks PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_nativemodule_microtasks + react_codegen_rncore + reactnative) diff --git a/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.cpp b/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.cpp new file mode 100644 index 00000000000..25d5f2c41a5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.cpp @@ -0,0 +1,31 @@ +/* + * 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 "NativeMicrotasks.h" + +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER +#include "Plugins.h" +#endif + +std::shared_ptr NativeMicrotasksModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared( + std::move(jsInvoker)); +} + +namespace facebook::react { + +NativeMicrotasks::NativeMicrotasks(std::shared_ptr jsInvoker) + : NativeMicrotasksCxxSpec(std::move(jsInvoker)) {} + +void NativeMicrotasks::queueMicrotask( + jsi::Runtime& runtime, + jsi::Function callback) { + runtime.queueMicrotask(callback); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.h b/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.h new file mode 100644 index 00000000000..2d631bb2f47 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.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 + +#if __has_include("rncoreJSI.h") // Cmake headers on Android +#include "rncoreJSI.h" +#elif __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else +#include +#endif + +namespace facebook::react { + +class NativeMicrotasks : public NativeMicrotasksCxxSpec, + std::enable_shared_from_this { + public: + NativeMicrotasks(std::shared_ptr jsInvoker); + + void queueMicrotask(jsi::Runtime& runtime, jsi::Function callback); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/microtasks/React-microtasksnativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/microtasks/React-microtasksnativemodule.podspec new file mode 100644 index 00000000000..20ff70d9e43 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/microtasks/React-microtasksnativemodule.podspec @@ -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. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +header_search_paths = [] + +if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the microtasks module access its own files +end + +Pod::Spec.new do |s| + s.name = "React-microtasksnativemodule" + s.version = version + s.summary = "React Native microtasks native module" + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = min_supported_versions + s.source = source + s.source_files = "*.{cpp,h}" + s.header_dir = "react/nativemodule/microtasks" + s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), + "DEFINES_MODULE" => "YES" } + + if ENV['USE_FRAMEWORKS'] + s.module_name = "React_microtasksnativemodule" + s.header_mappings_dir = "../.." + end + + install_modules_dependencies(s) + + s.dependency "ReactCommon/turbomodule/core" +end diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 6c13f048bfe..e60920404de 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -126,6 +126,7 @@ def use_react_native! ( pod 'React-utils', :path => "#{prefix}/ReactCommon/react/utils" pod 'React-featureflags', :path => "#{prefix}/ReactCommon/react/featureflags" pod 'React-featureflagsnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/featureflags" + pod 'React-microtasksnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/microtasks" pod 'React-Mapbuffer', :path => "#{prefix}/ReactCommon" pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler" pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon" diff --git a/packages/react-native/src/private/webapis/microtasks/specs/NativeMicrotasks.js b/packages/react-native/src/private/webapis/microtasks/specs/NativeMicrotasks.js new file mode 100644 index 00000000000..5fa44e54803 --- /dev/null +++ b/packages/react-native/src/private/webapis/microtasks/specs/NativeMicrotasks.js @@ -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. + * + * @flow strict + * @format + */ + +import type {TurboModule} from '../../../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +queueMicrotask: (callback: () => mixed) => void; +} + +export default (TurboModuleRegistry.get('NativeMicrotasksCxx'): ?Spec);