From e7cadaf9f315d8984bdea1c7fb6da4e302952819 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 3 Jul 2026 01:41:41 -0700 Subject: [PATCH 01/12] Add IntBuffer and DoubleBuffer entry types to MapBuffer (#57359) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57359 Adds two new MapBuffer entry types, `IntBuffer` and `DoubleBuffer`, for storing homogeneous arrays of ints and doubles compactly in the dynamic data section. Unlike `Map` / map lists, these carry no per-element key/type overhead: a batch of N values costs ~N*elementSize bytes plus a single 4-byte count prefix instead of N 12-byte buckets. The bucket value holds the offset of the array within the dynamic data section. Covers the full surface: the C++ reader (`MapBuffer::getIntBuffer` / `getDoubleBuffer`), the C++ builder (`MapBufferBuilder::putIntBuffer` / `putDoubleBuffer`), and the Kotlin reader API (`MapBuffer.getIntBuffer` / `getDoubleBuffer`, `Entry.intBufferValue` / `doubleBufferValue`). The `DataType` enum gains `IntBuffer = 6` and `DoubleBuffer = 7`, kept in sync across C++ and Kotlin. Changelog: [General][Added] - Add `IntBuffer` and `DoubleBuffer` entry types to MapBuffer for compact homogeneous int/double arrays landed-with-radar-review Reviewed By: zeyap Differential Revision: D109848476 fbshipit-source-id: f9e86b7c094dea796d9a8b725e53eb948c1390ca --- .../ReactAndroid/api/ReactAndroid.api | 8 ++ .../react/common/mapbuffer/MapBuffer.kt | 40 ++++++++ .../common/mapbuffer/ReadableMapBuffer.kt | 38 ++++++++ .../common/mapbuffer/WritableMapBuffer.kt | 30 ++++++ .../common/mapbuffer/JWritableMapBuffer.cpp | 13 +++ .../react/renderer/mapbuffer/MapBuffer.cpp | 40 ++++++++ .../react/renderer/mapbuffer/MapBuffer.h | 16 +++- .../renderer/mapbuffer/MapBufferBuilder.cpp | 46 +++++++++ .../renderer/mapbuffer/MapBufferBuilder.h | 4 + .../mapbuffer/tests/MapBufferTest.cpp | 95 +++++++++++++++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 6 ++ .../api-snapshots/ReactAndroidNewarchCxx.api | 6 ++ .../api-snapshots/ReactAndroidReleaseCxx.api | 6 ++ .../api-snapshots/ReactAppleDebugCxx.api | 6 ++ .../api-snapshots/ReactAppleNewarchCxx.api | 6 ++ .../api-snapshots/ReactAppleReleaseCxx.api | 6 ++ .../api-snapshots/ReactCommonDebugCxx.api | 6 ++ .../api-snapshots/ReactCommonNewarchCxx.api | 6 ++ .../api-snapshots/ReactCommonReleaseCxx.api | 6 ++ 19 files changed, 382 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 699c267339a7..3eae29cea63a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1665,7 +1665,9 @@ public abstract interface class com/facebook/react/common/mapbuffer/MapBuffer : public abstract fun getBoolean (I)Z public abstract fun getCount ()I public abstract fun getDouble (I)D + public abstract fun getDoubleBuffer (I)[D public abstract fun getInt (I)I + public abstract fun getIntBuffer (I)[I public abstract fun getKeyOffset (I)I public abstract fun getLong (I)J public abstract fun getMapBuffer (I)Lcom/facebook/react/common/mapbuffer/MapBuffer; @@ -1680,7 +1682,9 @@ public final class com/facebook/react/common/mapbuffer/MapBuffer$Companion { public final class com/facebook/react/common/mapbuffer/MapBuffer$DataType : java/lang/Enum { public static final field BOOL Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field DOUBLE Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; + public static final field DOUBLE_BUFFER Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field INT Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; + public static final field INT_BUFFER Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field LONG Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field MAP Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field STRING Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; @@ -1691,7 +1695,9 @@ public final class com/facebook/react/common/mapbuffer/MapBuffer$DataType : java public abstract interface class com/facebook/react/common/mapbuffer/MapBuffer$Entry { public abstract fun getBooleanValue ()Z + public abstract fun getDoubleBufferValue ()[D public abstract fun getDoubleValue ()D + public abstract fun getIntBufferValue ()[I public abstract fun getIntValue ()I public abstract fun getKey ()I public abstract fun getLongValue ()J @@ -1708,7 +1714,9 @@ public final class com/facebook/react/common/mapbuffer/ReadableMapBuffer : com/f public fun getBoolean (I)Z public fun getCount ()I public fun getDouble (I)D + public fun getDoubleBuffer (I)[D public fun getInt (I)I + public fun getIntBuffer (I)[I public fun getKeyOffset (I)I public fun getLong (I)J public synthetic fun getMapBuffer (I)Lcom/facebook/react/common/mapbuffer/MapBuffer; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt index 44af587bd9f1..17db9cd7a1d3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt @@ -46,6 +46,8 @@ public interface MapBuffer : Iterable { STRING, MAP, LONG, + INT_BUFFER, + DOUBLE_BUFFER, } /** @@ -161,6 +163,30 @@ public interface MapBuffer : Iterable { */ public fun getMapBufferList(key: Int): List + /** + * Provides parsed [IntArray] value if the entry for given key exists with [DataType.INT_BUFFER] + * type. This is a compact representation of a homogeneous list of ints with no per-element + * key/type overhead. + * + * @param key key to lookup the [IntArray] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exist + * @throws IllegalStateException if the data type doesn't match + */ + public fun getIntBuffer(key: Int): IntArray + + /** + * Provides parsed [DoubleArray] value if the entry for given key exists with + * [DataType.DOUBLE_BUFFER] type. This is a compact representation of a homogeneous list of + * doubles with no per-element key/type overhead. + * + * @param key key to lookup the [DoubleArray] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exist + * @throws IllegalStateException if the data type doesn't match + */ + public fun getDoubleBuffer(key: Int): DoubleArray + /** Iterable entry representing parsed MapBuffer values */ public interface Entry { /** @@ -213,5 +239,19 @@ public interface MapBuffer : Iterable { * @throws IllegalStateException if the data type doesn't match [DataType.MAP] */ public val mapBufferValue: MapBuffer + + /** + * Entry value represented as [IntArray] + * + * @throws IllegalStateException if the data type doesn't match [DataType.INT_BUFFER] + */ + public val intBufferValue: IntArray + + /** + * Entry value represented as [DoubleArray] + * + * @throws IllegalStateException if the data type doesn't match [DataType.DOUBLE_BUFFER] + */ + public val doubleBufferValue: DoubleArray } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt index c73ccb6da1dc..35ed29590051 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt @@ -152,6 +152,24 @@ private constructor( return readMapBufferList } + private fun readIntBufferValue(bufferPosition: Int): IntArray { + var offset = offsetForDynamicData + buffer.getInt(bufferPosition) + val count = buffer.getInt(offset) + offset += Int.SIZE_BYTES + val result = IntArray(count) + buffer.duplicate().order(buffer.order()).apply { position(offset) }.asIntBuffer().get(result) + return result + } + + private fun readDoubleBufferValue(bufferPosition: Int): DoubleArray { + var offset = offsetForDynamicData + buffer.getInt(bufferPosition) + val count = buffer.getInt(offset) + offset += Int.SIZE_BYTES + val result = DoubleArray(count) + buffer.duplicate().order(buffer.order()).apply { position(offset) }.asDoubleBuffer().get(result) + return result + } + private fun getKeyOffsetForBucketIndex(bucketIndex: Int): Int { return offsetToMapBuffer + HEADER_SIZE + BUCKET_SIZE * bucketIndex } @@ -193,6 +211,12 @@ private constructor( override fun getMapBufferList(key: Int): List = readMapBufferListValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.MAP)) + override fun getIntBuffer(key: Int): IntArray = + readIntBufferValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.INT_BUFFER)) + + override fun getDoubleBuffer(key: Int): DoubleArray = + readDoubleBufferValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.DOUBLE_BUFFER)) + override fun hashCode(): Int { buffer.rewind() return buffer.hashCode() @@ -229,6 +253,8 @@ private constructor( append('"') } MapBuffer.DataType.MAP -> append(entry.mapBufferValue.toString()) + MapBuffer.DataType.INT_BUFFER -> append(entry.intBufferValue.contentToString()) + MapBuffer.DataType.DOUBLE_BUFFER -> append(entry.doubleBufferValue.contentToString()) } } } @@ -311,6 +337,18 @@ private constructor( assertType(MapBuffer.DataType.MAP) return readMapBufferValue(bucketOffset + VALUE_OFFSET) } + + override val intBufferValue: IntArray + get() { + assertType(MapBuffer.DataType.INT_BUFFER) + return readIntBufferValue(bucketOffset + VALUE_OFFSET) + } + + override val doubleBufferValue: DoubleArray + get() { + assertType(MapBuffer.DataType.DOUBLE_BUFFER) + return readDoubleBufferValue(bucketOffset + VALUE_OFFSET) + } } public companion object { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt index d75e8a39a8a2..be6895b25fa9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt @@ -84,6 +84,24 @@ internal class WritableMapBuffer : MapBuffer { */ fun put(key: Int, value: MapBuffer): WritableMapBuffer = putInternal(key, value) + /** + * Adds an [IntArray] value for given key to the current MapBuffer. + * + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: IntArray): WritableMapBuffer = putInternal(key, value) + + /** + * Adds a [DoubleArray] value for given key to the current MapBuffer. + * + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: DoubleArray): WritableMapBuffer = putInternal(key, value) + private fun putInternal(key: Int, value: Any): WritableMapBuffer { require(key in KEY_RANGE) { "Only integers in [${UShort.MIN_VALUE};${UShort.MAX_VALUE}] range are allowed for keys." @@ -126,6 +144,10 @@ internal class WritableMapBuffer : MapBuffer { override fun getMapBufferList(key: Int): List = verifyValue(key, values.get(key)) + override fun getIntBuffer(key: Int): IntArray = verifyValue(key, values.get(key)) + + override fun getDoubleBuffer(key: Int): DoubleArray = verifyValue(key, values.get(key)) + /** Generalizes verification of the value types based on the requested type. */ private inline fun verifyValue(key: Int, value: Any?): T { require(value != null) { "Key not found: $key" } @@ -143,6 +165,8 @@ internal class WritableMapBuffer : MapBuffer { is Double -> DataType.DOUBLE is String -> DataType.STRING is MapBuffer -> DataType.MAP + is IntArray -> DataType.INT_BUFFER + is DoubleArray -> DataType.DOUBLE_BUFFER else -> throw IllegalStateException("Key $key has value of unknown type: ${value.javaClass}") } } @@ -176,6 +200,12 @@ internal class WritableMapBuffer : MapBuffer { override val mapBufferValue: MapBuffer get() = verifyValue(key, values.valueAt(index)) + + override val intBufferValue: IntArray + get() = verifyValue(key, values.valueAt(index)) + + override val doubleBufferValue: DoubleArray + get() = verifyValue(key, values.valueAt(index)) } /* diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/mapbuffer/react/common/mapbuffer/JWritableMapBuffer.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/mapbuffer/react/common/mapbuffer/JWritableMapBuffer.cpp index 3273c5202bcd..f243aeae4b82 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/mapbuffer/react/common/mapbuffer/JWritableMapBuffer.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/mapbuffer/react/common/mapbuffer/JWritableMapBuffer.cpp @@ -35,6 +35,8 @@ MapBuffer JWritableMapBuffer::getMapBuffer() { static const auto stringClass = jni::JString::javaClassStatic(); static const auto readableMapClass = JReadableMapBuffer::javaClassStatic(); static const auto writableMapClass = JWritableMapBuffer::javaClassStatic(); + static const auto intArrayClass = jni::JArrayInt::javaClassStatic(); + static const auto doubleArrayClass = jni::JArrayDouble::javaClassStatic(); if (value->isInstanceOf(booleanClass)) { auto element = jni::static_ref_cast(value); @@ -56,6 +58,17 @@ MapBuffer JWritableMapBuffer::getMapBuffer() { auto element = jni::static_ref_cast(value); builder.putMapBuffer(key, element->getMapBuffer()); + } else if (value->isInstanceOf(intArrayClass)) { + auto array = jni::static_ref_cast(value); + auto pinned = array->pin(); + builder.putIntBuffer( + key, + std::vector(pinned.get(), pinned.get() + pinned.size())); + } else if (value->isInstanceOf(doubleArrayClass)) { + auto array = jni::static_ref_cast(value); + auto pinned = array->pin(); + builder.putDoubleBuffer( + key, std::vector(pinned.get(), pinned.get() + pinned.size())); } } diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp index 401a6670d03b..e7c0a97ffe5c 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp @@ -163,6 +163,46 @@ std::vector MapBuffer::getMapBufferList(MapBuffer::Key key) const { return mapBufferList; } +std::vector MapBuffer::getIntBuffer(MapBuffer::Key key) const { + auto bucketIndex = getKeyBucket(key); + react_native_assert(bucketIndex != -1 && "Key not found in MapBuffer"); + if (bucketIndex == -1) { + return {}; + } + + int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); + int32_t count = *reinterpret_cast(bytes_.data() + offset); + + std::vector result(count); + if (count > 0) { + memcpy( + result.data(), + bytes_.data() + offset + sizeof(int32_t), + static_cast(count) * sizeof(int32_t)); + } + return result; +} + +std::vector MapBuffer::getDoubleBuffer(MapBuffer::Key key) const { + auto bucketIndex = getKeyBucket(key); + react_native_assert(bucketIndex != -1 && "Key not found in MapBuffer"); + if (bucketIndex == -1) { + return {}; + } + + int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); + int32_t count = *reinterpret_cast(bytes_.data() + offset); + + std::vector result(count); + if (count > 0) { + memcpy( + result.data(), + bytes_.data() + offset + sizeof(int32_t), + static_cast(count) * sizeof(double)); + } + return result; +} + size_t MapBuffer::size() const { return bytes_.size(); } diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index 1ae3595368c9..2d920af86e8e 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -94,8 +94,9 @@ class MapBuffer { /** * Data types available for serialization in MapBuffer - * Keep in sync with `DataType` enum in `JReadableMapBuffer.java`, which - * expects the same values after reading them through JNI. + * Keep in sync with the `DataType` enum in `MapBuffer.kt` + * (packages/react-native/ReactAndroid/.../common/mapbuffer/MapBuffer.kt), + * which is ordinal-indexed on the JVM side, so the order must match exactly. */ enum DataType : uint16_t { Boolean = 0, @@ -104,6 +105,13 @@ class MapBuffer { String = 3, Map = 4, Long = 5, + // Homogeneous, length-prefixed arrays stored contiguously in the dynamic + // data section. Unlike Map, they carry no per-element key/type overhead, so + // a batch of N values costs ~N*elementSize bytes plus a single 4-byte count + // prefix instead of N*12-byte buckets. The bucket value is the offset of the + // array within the dynamic data section. + IntBuffer = 6, + DoubleBuffer = 7, }; explicit MapBuffer(std::vector data); @@ -131,6 +139,10 @@ class MapBuffer { std::vector getMapBufferList(MapBuffer::Key key) const; + std::vector getIntBuffer(MapBuffer::Key key) const; + + std::vector getDoubleBuffer(MapBuffer::Key key) const; + size_t size() const; const uint8_t *data() const; diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp index 244f1065b524..c0e77bdbb5d1 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp @@ -161,6 +161,52 @@ void MapBufferBuilder::putMapBufferList( INT_SIZE); } +void MapBufferBuilder::putIntBuffer( + MapBuffer::Key key, + const std::vector& value) { + // Wire format: [element count (int32)] + [count * int32]. The count is the + // number of elements, not bytes; see MapBuffer::getIntBuffer. + auto count = static_cast(value.size()); + auto payloadSize = static_cast(value.size() * sizeof(int32_t)); + + auto offset = static_cast(dynamicData_.size()); + dynamicData_.resize(offset + INT_SIZE + payloadSize, 0); + memcpy(dynamicData_.data() + offset, &count, INT_SIZE); + if (payloadSize > 0) { + memcpy(dynamicData_.data() + offset + INT_SIZE, value.data(), payloadSize); + } + + storeKeyValue( + key, + MapBuffer::DataType::IntBuffer, + reinterpret_cast(&offset), + INT_SIZE); +} + +void MapBufferBuilder::putDoubleBuffer( + MapBuffer::Key key, + const std::vector& value) { + // Wire format: [element count (int32)] + [count * double]. Doubles are copied + // byte-for-byte; the reader uses memcpy, so the payload needs no special + // alignment for correctness. A consumer that wants a zero-copy typed view on + // the JVM (ByteBuffer::asDoubleBuffer) must ensure 8-byte alignment itself. + auto count = static_cast(value.size()); + auto payloadSize = static_cast(value.size() * sizeof(double)); + + auto offset = static_cast(dynamicData_.size()); + dynamicData_.resize(offset + INT_SIZE + payloadSize, 0); + memcpy(dynamicData_.data() + offset, &count, INT_SIZE); + if (payloadSize > 0) { + memcpy(dynamicData_.data() + offset + INT_SIZE, value.data(), payloadSize); + } + + storeKeyValue( + key, + MapBuffer::DataType::DoubleBuffer, + reinterpret_cast(&offset), + INT_SIZE); +} + static inline bool compareBuckets( const MapBuffer::Bucket& a, const MapBuffer::Bucket& b) { diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h index af054da55789..2963c77a9b36 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h @@ -39,6 +39,10 @@ class MapBufferBuilder { void putMapBufferList(MapBuffer::Key key, const std::vector &mapBufferList); + void putIntBuffer(MapBuffer::Key key, const std::vector &value); + + void putDoubleBuffer(MapBuffer::Key key, const std::vector &value); + MapBuffer build(); private: diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp index 3cc169563fca..ce4fd6237c03 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp @@ -205,6 +205,101 @@ TEST(MapBufferTest, testMapListEntries) { EXPECT_EQ(mapBufferList2[1].getDouble(3), 908.1); } +TEST(MapBufferTest, testEmptyMapBufferList) { + auto builder = MapBufferBuilder(); + + builder.putMapBufferList(0, {}); + auto map = builder.build(); + + EXPECT_EQ(map.getMapBufferList(0).size(), 0); +} + +// Place the list behind another dynamic-data entry so its offset is non-zero, +// exercising `getDynamicDataOffset() + getIntAtBucket(...)` against a non-zero +// base rather than the zero-offset path testMapListEntries covers. +TEST(MapBufferTest, testMapListEntriesAtNonZeroOffset) { + std::vector mapBufferList; + auto inner = MapBufferBuilder(); + inner.putString(0, "inner"); + inner.putInt(1, 42); + mapBufferList.push_back(inner.build()); + + auto builder = MapBufferBuilder(); + builder.putString(0, "prefix"); + builder.putMapBufferList(1, mapBufferList); + auto map = builder.build(); + + EXPECT_EQ(map.getString(0), "prefix"); + std::vector readList = map.getMapBufferList(1); + EXPECT_EQ(readList.size(), 1); + EXPECT_EQ(readList[0].getString(0), "inner"); + EXPECT_EQ(readList[0].getInt(1), 42); +} + +TEST(MapBufferTest, testIntBufferEntries) { + auto builder = MapBufferBuilder(); + + std::vector values{ + 1, + -2, + 3, + std::numeric_limits::min(), + std::numeric_limits::max()}; + builder.putIntBuffer(0, values); + auto map = builder.build(); + + EXPECT_EQ(map.count(), 1); + EXPECT_EQ(map.getIntBuffer(0), values); +} + +TEST(MapBufferTest, testEmptyIntBuffer) { + auto builder = MapBufferBuilder(); + + builder.putIntBuffer(0, {}); + auto map = builder.build(); + + EXPECT_EQ(map.getIntBuffer(0).size(), 0); +} + +TEST(MapBufferTest, testDoubleBufferEntries) { + auto builder = MapBufferBuilder(); + + std::vector values{0.0, -1.5, 3.14159, 1e300, -1e-300}; + builder.putDoubleBuffer(0, values); + auto map = builder.build(); + + EXPECT_EQ(map.count(), 1); + EXPECT_EQ(map.getDoubleBuffer(0), values); +} + +TEST(MapBufferTest, testEmptyDoubleBuffer) { + auto builder = MapBufferBuilder(); + + builder.putDoubleBuffer(0, {}); + auto map = builder.build(); + + EXPECT_EQ(map.getDoubleBuffer(0).size(), 0); +} + +// Mirrors the batched-animated-props use case: a pair of typed streams plus +// some scalar metadata, with keys inserted out of order to exercise both the +// dynamic-data section and the bucket sort path. +TEST(MapBufferTest, testIntAndDoubleBuffersAlongsideScalars) { + std::vector intStream{1, 100, 1, 2, 4, 15, 4}; + std::vector doubleStream{0.5, 12.0, 0.25}; + + auto builder = MapBufferBuilder(); + builder.putDoubleBuffer(2, doubleStream); + builder.putInt(0, 7); + builder.putIntBuffer(1, intStream); + auto map = builder.build(); + + EXPECT_EQ(map.count(), 3); + EXPECT_EQ(map.getInt(0), 7); + EXPECT_EQ(map.getIntBuffer(1), intStream); + EXPECT_EQ(map.getDoubleBuffer(2), doubleStream); +} + TEST(MapBufferTest, testMapRandomAccess) { auto builder = MapBufferBuilder(); builder.putInt(1234, 4321); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index d484218abb9d..a3c11b134bd6 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -3249,7 +3249,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -3257,7 +3259,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -3282,7 +3286,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index ce3256b5f8be..ffe4f90614c7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3158,7 +3158,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -3166,7 +3168,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -3191,7 +3195,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 04c7b0691b18..82b7241990ea 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -3246,7 +3246,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -3254,7 +3256,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -3279,7 +3283,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 6e8bf9f249d1..8efdf4143c47 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5484,7 +5484,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -5492,7 +5494,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -5517,7 +5521,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 8c903a172ef6..6c7799c8dc30 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -5408,7 +5408,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -5416,7 +5418,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -5441,7 +5445,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 6f48057c356d..276c824bec11 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5481,7 +5481,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -5489,7 +5491,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -5514,7 +5518,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 66d75beaa700..676107ead62c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2170,7 +2170,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -2178,7 +2180,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -2203,7 +2207,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index cd680589a109..69b7eb0ca5f9 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2106,7 +2106,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -2114,7 +2116,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -2139,7 +2143,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index b6573d00004b..5d77f4412fff 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2167,7 +2167,9 @@ class facebook::react::MapBuffer { public size_t size() const; public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; + public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; + public std::vector getIntBuffer(facebook::react::MapBuffer::Key key) const; public uint16_t count() const; public using Key = uint16_t; } @@ -2175,7 +2177,9 @@ class facebook::react::MapBuffer { enum facebook::react::MapBuffer::DataType : uint16_t { Boolean, Double, + DoubleBuffer, Int, + IntBuffer, Long, Map, String, @@ -2200,7 +2204,9 @@ class facebook::react::MapBufferBuilder { public static facebook::react::MapBuffer EMPTY(); public void putBool(facebook::react::MapBuffer::Key key, bool value); public void putDouble(facebook::react::MapBuffer::Key key, double value); + public void putDoubleBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putInt(facebook::react::MapBuffer::Key key, int32_t value); + public void putIntBuffer(facebook::react::MapBuffer::Key key, const std::vector& value); public void putLong(facebook::react::MapBuffer::Key key, int64_t value); public void putMapBuffer(facebook::react::MapBuffer::Key key, const facebook::react::MapBuffer& map); public void putMapBufferList(facebook::react::MapBuffer::Key key, const std::vector& mapBufferList); From 6a957746f6a2fc280bd6b30724d4fe10b4841841 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 3 Jul 2026 01:41:41 -0700 Subject: [PATCH 02/12] Add MapBufferList entry type to MapBuffer (#57360) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57360 Introduces a dedicated `MapBufferList` `DataType` for an ordered array of nested MapBuffers, instead of overloading the `Map` type for lists. This makes a list of MapBuffers self-describing and distinguishable from a single nested `Map` (they were byte-distinct in payload but previously shared the `Map` type tag). Updates the C++ builder (`putMapBufferList`), the Kotlin `MapBuffer` interface, `ReadableMapBuffer`, and `WritableMapBuffer`, and adds cross-language JNI round-trip coverage in the serialization instrumentation test. Changelog: [Android][Added] - Add a dedicated `MapBufferList` type to `MapBuffer` for ordered lists of nested `MapBuffer`s landed-with-radar-review Reviewed By: zeyap Differential Revision: D109848477 fbshipit-source-id: 7f590d5999d0cc4ee2d9d28cc34ae9220e442e18 --- .../react-native/ReactAndroid/api/ReactAndroid.api | 2 ++ .../com/facebook/react/common/mapbuffer/MapBuffer.kt | 12 ++++++++++-- .../react/common/mapbuffer/ReadableMapBuffer.kt | 9 ++++++++- .../react/common/mapbuffer/WritableMapBuffer.kt | 3 +++ .../ReactCommon/react/renderer/mapbuffer/MapBuffer.h | 4 ++++ .../react/renderer/mapbuffer/MapBufferBuilder.cpp | 5 +++-- .../cxx-api/api-snapshots/ReactAndroidDebugCxx.api | 1 + .../cxx-api/api-snapshots/ReactAndroidNewarchCxx.api | 1 + .../cxx-api/api-snapshots/ReactAndroidReleaseCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 1 + .../cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 1 + .../cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 1 + .../cxx-api/api-snapshots/ReactCommonDebugCxx.api | 1 + .../cxx-api/api-snapshots/ReactCommonNewarchCxx.api | 1 + .../cxx-api/api-snapshots/ReactCommonReleaseCxx.api | 1 + 15 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 3eae29cea63a..c4e2927d1568 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1687,6 +1687,7 @@ public final class com/facebook/react/common/mapbuffer/MapBuffer$DataType : java public static final field INT_BUFFER Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field LONG Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field MAP Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; + public static final field MAP_BUFFER_LIST Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static final field STRING Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; @@ -1701,6 +1702,7 @@ public abstract interface class com/facebook/react/common/mapbuffer/MapBuffer$En public abstract fun getIntValue ()I public abstract fun getKey ()I public abstract fun getLongValue ()J + public abstract fun getMapBufferListValue ()Ljava/util/List; public abstract fun getMapBufferValue ()Lcom/facebook/react/common/mapbuffer/MapBuffer; public abstract fun getStringValue ()Ljava/lang/String; public abstract fun getType ()Lcom/facebook/react/common/mapbuffer/MapBuffer$DataType; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt index 17db9cd7a1d3..feba0dc97d6a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt @@ -48,6 +48,7 @@ public interface MapBuffer : Iterable { LONG, INT_BUFFER, DOUBLE_BUFFER, + MAP_BUFFER_LIST, } /** @@ -153,8 +154,8 @@ public interface MapBuffer : Iterable { public fun getMapBuffer(key: Int): MapBuffer /** - * Provides parsed [List] value if the entry for given key exists with [DataType.MAP] - * type + * Provides parsed [List] value if the entry for given key exists with + * [DataType.MAP_BUFFER_LIST] type * * @param key key to lookup [List] value for * @return value associated with the requested key @@ -253,5 +254,12 @@ public interface MapBuffer : Iterable { * @throws IllegalStateException if the data type doesn't match [DataType.DOUBLE_BUFFER] */ public val doubleBufferValue: DoubleArray + + /** + * Entry value represented as [List] + * + * @throws IllegalStateException if the data type doesn't match [DataType.MAP_BUFFER_LIST] + */ + public val mapBufferListValue: List } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt index 35ed29590051..875873bfb0cc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt @@ -209,7 +209,7 @@ private constructor( readMapBufferValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.MAP)) override fun getMapBufferList(key: Int): List = - readMapBufferListValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.MAP)) + readMapBufferListValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.MAP_BUFFER_LIST)) override fun getIntBuffer(key: Int): IntArray = readIntBufferValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.INT_BUFFER)) @@ -255,6 +255,7 @@ private constructor( MapBuffer.DataType.MAP -> append(entry.mapBufferValue.toString()) MapBuffer.DataType.INT_BUFFER -> append(entry.intBufferValue.contentToString()) MapBuffer.DataType.DOUBLE_BUFFER -> append(entry.doubleBufferValue.contentToString()) + MapBuffer.DataType.MAP_BUFFER_LIST -> append(entry.mapBufferListValue.toString()) } } } @@ -349,6 +350,12 @@ private constructor( assertType(MapBuffer.DataType.DOUBLE_BUFFER) return readDoubleBufferValue(bucketOffset + VALUE_OFFSET) } + + override val mapBufferListValue: List + get() { + assertType(MapBuffer.DataType.MAP_BUFFER_LIST) + return readMapBufferListValue(bucketOffset + VALUE_OFFSET) + } } public companion object { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt index be6895b25fa9..dea593ca6e7b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt @@ -206,6 +206,9 @@ internal class WritableMapBuffer : MapBuffer { override val doubleBufferValue: DoubleArray get() = verifyValue(key, values.valueAt(index)) + + override val mapBufferListValue: List + get() = verifyValue(key, values.valueAt(index)) } /* diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index 2d920af86e8e..ae89b674b6a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -112,6 +112,10 @@ class MapBuffer { // array within the dynamic data section. IntBuffer = 6, DoubleBuffer = 7, + // A homogeneous, ordered array of nested MapBuffers. Distinct from `Map` so + // that a list of MapBuffers is self-describing (a single Map and a list are + // byte-distinct in payload but previously shared the `Map` type tag). + MapBufferList = 8, }; explicit MapBuffer(std::vector data); diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp index c0e77bdbb5d1..8ee37c1c01e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp @@ -153,10 +153,11 @@ void MapBufferBuilder::putMapBufferList( mapBufferSize); } - // Store Key and pointer to the string + // Store Key and pointer to the list. Uses the dedicated MapBufferList type so + // the entry is self-describing and distinguishable from a single Map. storeKeyValue( key, - MapBuffer::DataType::Map, + MapBuffer::DataType::MapBufferList, reinterpret_cast(&offset), INT_SIZE); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index a3c11b134bd6..8022b30807e7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -3264,6 +3264,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index ffe4f90614c7..c241745f95f5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3173,6 +3173,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 82b7241990ea..73beff563c82 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -3261,6 +3261,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 8efdf4143c47..0522de96cd20 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5499,6 +5499,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 6c7799c8dc30..e961ff1cf987 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -5423,6 +5423,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 276c824bec11..f5fd13c275cc 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5496,6 +5496,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 676107ead62c..5ec53a2f575b 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2185,6 +2185,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 69b7eb0ca5f9..2b67e1c5b4f2 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2121,6 +2121,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 5d77f4412fff..2f7f3c22e513 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2182,6 +2182,7 @@ enum facebook::react::MapBuffer::DataType : uint16_t { IntBuffer, Long, Map, + MapBufferList, String, } From 803456d699382fe530890d6e33bd15d35b0e4998 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 3 Jul 2026 01:41:41 -0700 Subject: [PATCH 03/12] Optimize MapBuffer representation (#57361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/57361 Broadens the former header-shrink change into a single representation-optimization commit for MapBuffer. It bundles three layout optimizations that were previously split: (1) the header is reduced to a single 2-byte `count` field; (2) multi-byte values are read via `memcpy` so unaligned access is well-defined on all platforms; (3) every dynamic-data entry (`String`, `Map`, `MapBufferList`, `IntBuffer`, `DoubleBuffer`) packs its `[offset][byteLength]` into the bucket's 8-byte value instead of writing an in-band length prefix into the dynamic data section. Net effect: 4 fewer bytes per dynamic entry, one fewer indirection on read (the length is already in the bucket), and every dynamic entry becomes self-delimiting from its bucket alone. No public API change — only the internal serialized representation. Changelog: [Internal] landed-with-radar-review Reviewed By: lenaic, zeyap Differential Revision: D109848478 fbshipit-source-id: 30749aaa1c2d9fc9b1f8684a3b67815ef44632e8 --- .../common/mapbuffer/ReadableMapBuffer.kt | 50 ++++--- .../react/renderer/mapbuffer/MapBuffer.cpp | 124 ++++++++++++------ .../react/renderer/mapbuffer/MapBuffer.h | 32 ++--- .../renderer/mapbuffer/MapBufferBuilder.cpp | 124 ++++++++---------- .../mapbuffer/tests/MapBufferTest.cpp | 2 +- .../api-snapshots/ReactAndroidDebugCxx.api | 3 - .../api-snapshots/ReactAndroidNewarchCxx.api | 3 - .../api-snapshots/ReactAndroidReleaseCxx.api | 3 - .../api-snapshots/ReactAppleDebugCxx.api | 3 - .../api-snapshots/ReactAppleNewarchCxx.api | 3 - .../api-snapshots/ReactAppleReleaseCxx.api | 3 - .../api-snapshots/ReactCommonDebugCxx.api | 3 - .../api-snapshots/ReactCommonNewarchCxx.api | 3 - .../api-snapshots/ReactCommonReleaseCxx.api | 3 - 14 files changed, 181 insertions(+), 178 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt index 875873bfb0cc..de9adb5d7faa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt @@ -51,13 +51,11 @@ private constructor( ReadableMapBuffer(buffer.duplicate().apply { position(offset) }, offset) private fun readHeader() { - // byte order - val storedAlignment = buffer.short - if (storedAlignment.toInt() != ALIGNMENT) { - buffer.order(ByteOrder.LITTLE_ENDIAN) - } - // count - count = readUnsignedShort(buffer.position()).toInt() + // The C++ writer always serializes in little-endian byte order. ByteBuffer + // defaults to big-endian and duplicate() resets the order, so set it + // explicitly on every instance, including nested clones. + buffer.order(ByteOrder.LITTLE_ENDIAN) + count = readUnsignedShort(offsetToMapBuffer).toInt() } /** @@ -122,26 +120,27 @@ private constructor( return readIntValue(bufferPosition) == 1 } + // Dynamic-data entries store [offset][byteLength] in the bucket's 8-byte + // value: getInt(bufferPosition) is the offset, getInt(bufferPosition + 4) is + // the byte length. The dynamic data section itself carries no length prefix. private fun readStringValue(bufferPosition: Int): String { val offset = offsetForDynamicData + buffer.getInt(bufferPosition) - val sizeOfString = buffer.getInt(offset) + val sizeOfString = buffer.getInt(bufferPosition + Int.SIZE_BYTES) val result = ByteArray(sizeOfString) - val stringOffset = offset + Int.SIZE_BYTES - buffer.position(stringOffset) - buffer[result, 0, sizeOfString] + buffer.position(offset) + buffer.get(result, 0, sizeOfString) return String(result) } private fun readMapBufferValue(position: Int): ReadableMapBuffer { val offset = offsetForDynamicData + buffer.getInt(position) - return cloneWithOffset(offset + Int.SIZE_BYTES) + return cloneWithOffset(offset) } private fun readMapBufferListValue(position: Int): List { val readMapBufferList = arrayListOf() - var offset = offsetForDynamicData + buffer.getInt(position) - val sizeMapBufferList = buffer.getInt(offset) - offset += Int.SIZE_BYTES + val offset = offsetForDynamicData + buffer.getInt(position) + val sizeMapBufferList = buffer.getInt(position + Int.SIZE_BYTES) var curLen = 0 while (curLen < sizeMapBufferList) { val sizeMapBuffer = buffer.getInt(offset + curLen) @@ -153,18 +152,18 @@ private constructor( } private fun readIntBufferValue(bufferPosition: Int): IntArray { - var offset = offsetForDynamicData + buffer.getInt(bufferPosition) - val count = buffer.getInt(offset) - offset += Int.SIZE_BYTES + val offset = offsetForDynamicData + buffer.getInt(bufferPosition) + val byteLength = buffer.getInt(bufferPosition + Int.SIZE_BYTES) + val count = byteLength / Int.SIZE_BYTES val result = IntArray(count) buffer.duplicate().order(buffer.order()).apply { position(offset) }.asIntBuffer().get(result) return result } private fun readDoubleBufferValue(bufferPosition: Int): DoubleArray { - var offset = offsetForDynamicData + buffer.getInt(bufferPosition) - val count = buffer.getInt(offset) - offset += Int.SIZE_BYTES + val offset = offsetForDynamicData + buffer.getInt(bufferPosition) + val byteLength = buffer.getInt(bufferPosition + Int.SIZE_BYTES) + val count = byteLength / Double.SIZE_BYTES val result = DoubleArray(count) buffer.duplicate().order(buffer.order()).apply { position(offset) }.asDoubleBuffer().get(result) return result @@ -359,13 +358,10 @@ private constructor( } public companion object { - // Value used to verify if the data is serialized with LittleEndian order. - private const val ALIGNMENT = 0xFE - - // 8 bytes = 2 (alignment) + 2 (count) + 4 (size) - private const val HEADER_SIZE = 8 + // 2 bytes = 2 (count) + private const val HEADER_SIZE = 2 - // 10 bytes = 2 (key) + 2 (type) + 8 (value) + // 12 bytes = 2 (key) + 2 (type) + 8 (value) private const val BUCKET_SIZE = 12 // 2 bytes = 2 (key) diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp index e7c0a97ffe5c..e657cc5514fc 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp @@ -8,8 +8,43 @@ #include "MapBuffer.h" #include +#include +#include + namespace facebook::react { +namespace { +// Reads a value of type T from a (possibly unaligned) offset in the buffer. +// MapBuffer's packed layout places multi-byte values at offsets that are not +// naturally aligned for their type (e.g. an 8-byte value at a 2-byte boundary), +// so dereferencing a reinterpret_cast pointer there is undefined behavior and +// can fault on 32-bit ARM. memcpy compiles to a single unaligned load on +// arm64/x86 and to alignment-safe loads on armv7. +template +inline T readUnaligned(const uint8_t* data, int32_t offset) { + T value; + std::memcpy(&value, data + offset, sizeof(T)); + return value; +} + +// Debug-asserts on OOB (catches corrupt buffers early in dev) AND clamps in +// release so a corrupt bucket length can never drive an OOB memcpy read. +// react_native_assert is compiled out in release, so the runtime cost outside +// dev is the single min() call. +inline int32_t +clampToBufferBounds(int32_t offset, int32_t byteLength, size_t bufferSize) { + react_native_assert(offset >= 0 && byteLength >= 0); + react_native_assert( + static_cast(offset) + static_cast(byteLength) <= + bufferSize); + size_t maxLength = bufferSize > static_cast(offset) + ? bufferSize - static_cast(offset) + : 0; + return static_cast( + std::min(static_cast(std::max(byteLength, 0)), maxLength)); +} +} // namespace + static inline int32_t bucketOffset(int32_t index) { return sizeof(MapBuffer::Header) + sizeof(MapBuffer::Bucket) * index; } @@ -18,16 +53,20 @@ static inline int32_t valueOffset(int32_t bucketIndex) { return bucketOffset(bucketIndex) + offsetof(MapBuffer::Bucket, data); } +// Dynamic-data entries pack [offset (low 32 bits)][byteLength (high 32 bits)] +// into the bucket's 8-byte value, so the payload in the dynamic data section +// carries no in-band length prefix. This returns the position of the high +// 32 bits (the length). +static inline int32_t lengthOffset(int32_t bucketIndex) { + return valueOffset(bucketIndex) + static_cast(sizeof(int32_t)); +} + // TODO T83483191: Extend MapBuffer C++ implementation to support basic random // access MapBuffer::MapBuffer(std::vector data) : bytes_(std::move(data)) { - auto header = reinterpret_cast(bytes_.data()); - count_ = header->count; - - if (header->bufferSize != bytes_.size()) { - LOG(ERROR) << "Error: Data size does not match, expected " - << header->bufferSize << " found: " << bytes_.size(); - abort(); + if (bytes_.size() >= sizeof(Header)) { + auto header = reinterpret_cast(bytes_.data()); + count_ = header->count; } } @@ -37,8 +76,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const { while (lo <= hi) { int32_t mid = (lo + hi) >> 1; - Key midVal = - *reinterpret_cast(bytes_.data() + bucketOffset(mid)); + Key midVal = readUnaligned(bytes_.data(), bucketOffset(mid)); if (midVal < key) { lo = mid + 1; @@ -53,8 +91,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const { } inline int32_t MapBuffer::getIntAtBucket(int32_t bucketIndex) const { - return *reinterpret_cast( - bytes_.data() + valueOffset(bucketIndex)); + return readUnaligned(bytes_.data(), valueOffset(bucketIndex)); } int32_t MapBuffer::getInt(Key key) const { @@ -74,8 +111,7 @@ int64_t MapBuffer::getLong(Key key) const { return 0; } - return *reinterpret_cast( - bytes_.data() + valueOffset(bucketIndex)); + return readUnaligned(bytes_.data(), valueOffset(bucketIndex)); } bool MapBuffer::getBool(Key key) const { @@ -89,8 +125,7 @@ double MapBuffer::getDouble(Key key) const { return 0; } - return *reinterpret_cast( - bytes_.data() + valueOffset(bucketIndex)); + return readUnaligned(bytes_.data(), valueOffset(bucketIndex)); } int32_t MapBuffer::getDynamicDataOffset() const { @@ -107,9 +142,10 @@ std::string MapBuffer::getString(Key key) const { } int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); - int32_t stringLength = - *reinterpret_cast(bytes_.data() + offset); - const uint8_t* stringPtr = bytes_.data() + offset + sizeof(int); + auto stringLength = + readUnaligned(bytes_.data(), lengthOffset(bucketIndex)); + stringLength = clampToBufferBounds(offset, stringLength, bytes_.size()); + const uint8_t* stringPtr = bytes_.data() + offset; return {stringPtr, stringPtr + stringLength}; } @@ -122,17 +158,13 @@ MapBuffer MapBuffer::getMapBuffer(Key key) const { } int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); - int32_t mapBufferLength = - *reinterpret_cast(bytes_.data() + offset); - size_t maxLength = bytes_.size() - offset - sizeof(int32_t); - if (mapBufferLength > maxLength) { - mapBufferLength = maxLength; - } + auto mapBufferLength = + readUnaligned(bytes_.data(), lengthOffset(bucketIndex)); + mapBufferLength = clampToBufferBounds(offset, mapBufferLength, bytes_.size()); std::vector value(mapBufferLength); - memcpy( - value.data(), bytes_.data() + offset + sizeof(int32_t), mapBufferLength); + memcpy(value.data(), bytes_.data() + offset, mapBufferLength); return MapBuffer(std::move(value)); } @@ -146,19 +178,27 @@ std::vector MapBuffer::getMapBufferList(MapBuffer::Key key) const { std::vector mapBufferList; int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); - int32_t mapBufferListLength = - *reinterpret_cast(bytes_.data() + offset); - offset = offset + sizeof(uint32_t); + auto mapBufferListLength = + readUnaligned(bytes_.data(), lengthOffset(bucketIndex)); + mapBufferListLength = + clampToBufferBounds(offset, mapBufferListLength, bytes_.size()); int32_t curLen = 0; while (curLen < mapBufferListLength) { - int32_t mapBufferLength = - *reinterpret_cast(bytes_.data() + offset + curLen); - curLen = curLen + sizeof(uint32_t); + if (curLen + sizeof(int32_t) > mapBufferListLength) { + break; + } + + auto mapBufferLength = + readUnaligned(bytes_.data(), offset + curLen); + curLen += sizeof(int32_t); + + mapBufferLength = + clampToBufferBounds(offset + curLen, mapBufferLength, bytes_.size()); std::vector value(mapBufferLength); memcpy(value.data(), bytes_.data() + offset + curLen, mapBufferLength); mapBufferList.emplace_back(std::move(value)); - curLen = curLen + mapBufferLength; + curLen += mapBufferLength; } return mapBufferList; } @@ -171,13 +211,18 @@ std::vector MapBuffer::getIntBuffer(MapBuffer::Key key) const { } int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); - int32_t count = *reinterpret_cast(bytes_.data() + offset); + auto byteLength = + readUnaligned(bytes_.data(), lengthOffset(bucketIndex)); + byteLength = clampToBufferBounds(offset, byteLength, bytes_.size()); + int32_t count = byteLength / static_cast(sizeof(int32_t)); std::vector result(count); if (count > 0) { + // Copy only whole elements: a clamped byteLength may not be a multiple of + // sizeof(int32_t), and result holds exactly count elements. memcpy( result.data(), - bytes_.data() + offset + sizeof(int32_t), + bytes_.data() + offset, static_cast(count) * sizeof(int32_t)); } return result; @@ -191,13 +236,18 @@ std::vector MapBuffer::getDoubleBuffer(MapBuffer::Key key) const { } int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex); - int32_t count = *reinterpret_cast(bytes_.data() + offset); + auto byteLength = + readUnaligned(bytes_.data(), lengthOffset(bucketIndex)); + byteLength = clampToBufferBounds(offset, byteLength, bytes_.size()); + int32_t count = byteLength / static_cast(sizeof(double)); std::vector result(count); if (count > 0) { + // Copy only whole elements: a clamped byteLength may not be a multiple of + // sizeof(double), and result holds exactly count elements. memcpy( result.data(), - bytes_.data() + offset + sizeof(int32_t), + bytes_.data() + offset, static_cast(count) * sizeof(double)); } return result; diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index ae89b674b6a3..07b64bdfba99 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -40,11 +40,11 @@ class JReadableMapBuffer; * * MapBuffer data is stored in a continuous chunk of memory (bytes_ field below) with the following layout: * - * ┌─────────────────────Header──────────────────────┐ - * │ 10 bytes │ - * ├─Alignment─┬─Item count─┬──────Buffer size───────┤ - * │ 2 bytes │ 2 bytes │ 4 bytes │ - * └───────────┴────────────┴────────────────────────┘ + * ┌──────Header──────┐ + * │ 2 bytes │ + * ├────Item count────┤ + * │ 2 bytes │ + * └──────────────────┘ * ┌────────────────────────────────────────────────────────────────────────────────────────┐ * │ Buckets (one per item in the map) │ * │ │ @@ -69,14 +69,8 @@ class MapBuffer { public: using Key = uint16_t; - // The first value in the buffer, used to check correct encoding/endianness on - // JVM side. - constexpr static uint16_t HEADER_ALIGNMENT = 0xFE; - struct Header { - uint16_t alignment = HEADER_ALIGNMENT; // alignment of serialization uint16_t count; // amount of items in the map - uint32_t bufferSize; // Amount of bytes used to store the map in memory }; #pragma pack(push, 1) @@ -89,7 +83,7 @@ class MapBuffer { }; #pragma pack(pop) - static_assert(sizeof(Header) == 8, "MapBuffer header size is incorrect."); + static_assert(sizeof(Header) == 2, "MapBuffer header size is incorrect."); static_assert(sizeof(Bucket) == 12, "MapBuffer bucket size is incorrect."); /** @@ -105,15 +99,17 @@ class MapBuffer { String = 3, Map = 4, Long = 5, - // Homogeneous, length-prefixed arrays stored contiguously in the dynamic + // Homogeneous arrays of raw elements stored contiguously in the dynamic // data section. Unlike Map, they carry no per-element key/type overhead, so - // a batch of N values costs ~N*elementSize bytes plus a single 4-byte count - // prefix instead of N*12-byte buckets. The bucket value is the offset of the - // array within the dynamic data section. + // a batch of N values costs ~N*elementSize bytes instead of N*12-byte + // buckets. The bucket value packs [offset][byteLength]; the element count is + // recovered as byteLength / elementSize. IntBuffer = 6, DoubleBuffer = 7, - // A homogeneous, ordered array of nested MapBuffers. Distinct from `Map` so - // that a list of MapBuffers is self-describing (a single Map and a list are + // A homogeneous, ordered array of nested MapBuffers. The bucket value packs + // [offset][byteLength] for the whole list region; within it each child stays + // framed as [int32 childSize][child bytes]. Distinct from `Map` so that a + // list of MapBuffers is self-describing (a single Map and a list are // byte-distinct in payload but previously shared the `Map` type tag). MapBufferList = 8, }; diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp index 8ee37c1c01e9..1944a01f261f 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp @@ -15,6 +15,14 @@ constexpr uint32_t LONG_SIZE = sizeof(uint64_t); constexpr uint32_t DOUBLE_SIZE = sizeof(double); constexpr uint32_t MAX_BUCKET_VALUE_SIZE = sizeof(uint64_t); +// Dynamic-data entries store their location in the bucket's 8-byte value as +// [offset (low 32 bits)][byteLength (high 32 bits)], so the payload in the +// dynamic data section needs no in-band length prefix. +static inline uint64_t packOffsetAndLength(int32_t offset, int32_t length) { + return static_cast(static_cast(offset)) | + (static_cast(static_cast(length)) << 32); +} + MapBuffer MapBufferBuilder::EMPTY() { return MapBufferBuilder(0).build(); } @@ -22,7 +30,6 @@ MapBuffer MapBufferBuilder::EMPTY() { MapBufferBuilder::MapBufferBuilder(uint32_t initialSize) { buckets_.reserve(initialSize); header_.count = 0; - header_.bufferSize = 0; } void MapBufferBuilder::storeKeyValue( @@ -84,128 +91,111 @@ void MapBufferBuilder::putLong(MapBuffer::Key key, int64_t value) { } void MapBufferBuilder::putString(MapBuffer::Key key, const std::string& value) { - // The wire format encodes lengths and offsets as int32_t (see - // MapBuffer::getString). Without an explicit narrowing cast, `auto` deduces - // size_t (8 bytes on 64-bit) and `memcpy(&x, ..., INT_SIZE)` then copies only - // the first 4 bytes of an 8-byte value: silent truncation on little-endian, - // wrong (high) bytes on big-endian. auto strSize = static_cast(value.size()); - const char* strData = value.data(); - - // format [length of string (int)] + [Array of Characters in the string] auto offset = static_cast(dynamicData_.size()); - dynamicData_.resize(offset + INT_SIZE + strSize, 0); - memcpy(dynamicData_.data() + offset, &strSize, INT_SIZE); - memcpy(dynamicData_.data() + offset + INT_SIZE, strData, strSize); - // Store Key and pointer to the string + // The bucket stores [offset][byteLength]; the dynamic section holds only the + // raw string bytes. + dynamicData_.resize(offset + strSize, 0); + if (strSize > 0) { + memcpy(dynamicData_.data() + offset, value.data(), strSize); + } + + uint64_t data = packOffsetAndLength(offset, strSize); storeKeyValue( key, MapBuffer::DataType::String, - reinterpret_cast(&offset), - INT_SIZE); + reinterpret_cast(&data), + sizeof(data)); } void MapBufferBuilder::putMapBuffer(MapBuffer::Key key, const MapBuffer& map) { - // Wire format encodes lengths and offsets as int32_t (see - // MapBuffer::getMapBuffer). Cast explicitly so memcpy(&x, ..., INT_SIZE) - // copies the full value, not the first 4 bytes of an 8-byte size_t. auto mapBufferSize = static_cast(map.size()); - auto offset = static_cast(dynamicData_.size()); - // format [length of buffer (int)] + [bytes of MapBuffer] - dynamicData_.resize(offset + INT_SIZE + mapBufferSize, 0); - memcpy(dynamicData_.data() + offset, &mapBufferSize, INT_SIZE); - // Copy the content of the map into dynamicData_ - memcpy(dynamicData_.data() + offset + INT_SIZE, map.data(), mapBufferSize); + // The bucket stores [offset][byteLength]; the dynamic section holds only the + // serialized child MapBuffer bytes. + dynamicData_.resize(offset + mapBufferSize, 0); + memcpy(dynamicData_.data() + offset, map.data(), mapBufferSize); - // Store Key and pointer to the string + uint64_t data = packOffsetAndLength(offset, mapBufferSize); storeKeyValue( key, MapBuffer::DataType::Map, - reinterpret_cast(&offset), - INT_SIZE); + reinterpret_cast(&data), + sizeof(data)); } void MapBufferBuilder::putMapBufferList( MapBuffer::Key key, const std::vector& mapBufferList) { auto offset = static_cast(dynamicData_.size()); - int32_t dataSize = 0; - for (const MapBuffer& mapBuffer : mapBufferList) { - dataSize = dataSize + INT_SIZE + static_cast(mapBuffer.size()); - } - - dynamicData_.resize(offset + INT_SIZE, 0); - memcpy(dynamicData_.data() + offset, &dataSize, INT_SIZE); + // The bucket stores [offset][byteLength] for the whole list region; within it + // each child stays framed as [int32 childSize][child bytes] so the children + // remain individually delimited. for (const MapBuffer& mapBuffer : mapBufferList) { auto mapBufferSize = static_cast(mapBuffer.size()); - auto dynamicDataSize = static_cast(dynamicData_.size()); - dynamicData_.resize(dynamicDataSize + INT_SIZE + mapBufferSize, 0); - // format [length of buffer (int)] + [bytes of MapBuffer] - memcpy(dynamicData_.data() + dynamicDataSize, &mapBufferSize, INT_SIZE); - // Copy the content of the map into dynamicData_ + auto pos = static_cast(dynamicData_.size()); + dynamicData_.resize(pos + INT_SIZE + mapBufferSize, 0); + memcpy(dynamicData_.data() + pos, &mapBufferSize, INT_SIZE); memcpy( - dynamicData_.data() + dynamicDataSize + INT_SIZE, - mapBuffer.data(), - mapBufferSize); + dynamicData_.data() + pos + INT_SIZE, mapBuffer.data(), mapBufferSize); } - // Store Key and pointer to the list. Uses the dedicated MapBufferList type so - // the entry is self-describing and distinguishable from a single Map. + auto totalSize = static_cast(dynamicData_.size()) - offset; + uint64_t data = packOffsetAndLength(offset, totalSize); + // Uses the dedicated MapBufferList type so the entry is self-describing and + // distinguishable from a single Map. storeKeyValue( key, MapBuffer::DataType::MapBufferList, - reinterpret_cast(&offset), - INT_SIZE); + reinterpret_cast(&data), + sizeof(data)); } void MapBufferBuilder::putIntBuffer( MapBuffer::Key key, const std::vector& value) { - // Wire format: [element count (int32)] + [count * int32]. The count is the - // number of elements, not bytes; see MapBuffer::getIntBuffer. - auto count = static_cast(value.size()); + // The bucket stores [offset][byteLength]; the dynamic section holds the raw + // int32 elements. Element count is recovered as byteLength / sizeof(int32_t). auto payloadSize = static_cast(value.size() * sizeof(int32_t)); - auto offset = static_cast(dynamicData_.size()); - dynamicData_.resize(offset + INT_SIZE + payloadSize, 0); - memcpy(dynamicData_.data() + offset, &count, INT_SIZE); + dynamicData_.resize(offset + payloadSize, 0); if (payloadSize > 0) { - memcpy(dynamicData_.data() + offset + INT_SIZE, value.data(), payloadSize); + memcpy(dynamicData_.data() + offset, value.data(), payloadSize); } + uint64_t data = packOffsetAndLength(offset, payloadSize); storeKeyValue( key, MapBuffer::DataType::IntBuffer, - reinterpret_cast(&offset), - INT_SIZE); + reinterpret_cast(&data), + sizeof(data)); } void MapBufferBuilder::putDoubleBuffer( MapBuffer::Key key, const std::vector& value) { - // Wire format: [element count (int32)] + [count * double]. Doubles are copied - // byte-for-byte; the reader uses memcpy, so the payload needs no special - // alignment for correctness. A consumer that wants a zero-copy typed view on - // the JVM (ByteBuffer::asDoubleBuffer) must ensure 8-byte alignment itself. - auto count = static_cast(value.size()); + // The bucket stores [offset][byteLength]; the dynamic section holds the raw + // double elements. Element count is recovered as byteLength / sizeof(double). + // Doubles are copied byte-for-byte; the reader uses memcpy, so the payload + // needs no special alignment for correctness. A consumer that wants a + // zero-copy typed view on the JVM (ByteBuffer::asDoubleBuffer) must ensure + // 8-byte alignment itself. auto payloadSize = static_cast(value.size() * sizeof(double)); - auto offset = static_cast(dynamicData_.size()); - dynamicData_.resize(offset + INT_SIZE + payloadSize, 0); - memcpy(dynamicData_.data() + offset, &count, INT_SIZE); + dynamicData_.resize(offset + payloadSize, 0); if (payloadSize > 0) { - memcpy(dynamicData_.data() + offset + INT_SIZE, value.data(), payloadSize); + memcpy(dynamicData_.data() + offset, value.data(), payloadSize); } + uint64_t data = packOffsetAndLength(offset, payloadSize); storeKeyValue( key, MapBuffer::DataType::DoubleBuffer, - reinterpret_cast(&offset), - INT_SIZE); + reinterpret_cast(&data), + sizeof(data)); } static inline bool compareBuckets( @@ -220,8 +210,6 @@ MapBuffer MapBufferBuilder::build() { auto headerSize = sizeof(MapBuffer::Header); auto bufferSize = headerSize + bucketSize + dynamicData_.size(); - header_.bufferSize = static_cast(bufferSize); - if (needsSort_) { std::sort(buckets_.begin(), buckets_.end(), compareBuckets); } diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp index ce4fd6237c03..05c70d203f59 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp @@ -49,7 +49,7 @@ TEST(MapBufferTest, testSimpleLongMap) { } TEST(MapBufferTest, testMapBufferExtension) { - // 26 = 2 buckets: 2*10 + 6 sizeof(header) + // initialSize is a reserve hint for the number of buckets int initialSize = 26; auto buffer = MapBufferBuilder(initialSize); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 8022b30807e7..ef70f42b27ef 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -3247,7 +3247,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -3276,9 +3275,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index c241745f95f5..a7b27cd3aa8a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3156,7 +3156,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -3185,9 +3184,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 73beff563c82..37597a3834e0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -3244,7 +3244,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -3273,9 +3272,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 0522de96cd20..947870b8c66e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5482,7 +5482,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -5511,9 +5510,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index e961ff1cf987..d60d97b5ac7f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -5406,7 +5406,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -5435,9 +5434,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index f5fd13c275cc..325e0e4dd4cc 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5479,7 +5479,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -5508,9 +5507,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 5ec53a2f575b..144dbea1f8ef 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2168,7 +2168,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -2197,9 +2196,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 2b67e1c5b4f2..c4253f9a715e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2104,7 +2104,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -2133,9 +2132,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 2f7f3c22e513..1cab650e5a96 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2165,7 +2165,6 @@ class facebook::react::MapBuffer { public int32_t getInt(facebook::react::MapBuffer::Key key) const; public int64_t getLong(facebook::react::MapBuffer::Key key) const; public size_t size() const; - public static constexpr uint16_t HEADER_ALIGNMENT; public std::string getString(facebook::react::MapBuffer::Key key) const; public std::vector getDoubleBuffer(facebook::react::MapBuffer::Key key) const; public std::vector getMapBufferList(facebook::react::MapBuffer::Key key) const; @@ -2194,9 +2193,7 @@ struct facebook::react::MapBuffer::Bucket { } struct facebook::react::MapBuffer::Header { - public uint16_t alignment; public uint16_t count; - public uint32_t bufferSize; } class facebook::react::MapBufferBuilder { From 2361189716afbcced6ee2cdb0fd860cf23460850 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Fri, 3 Jul 2026 03:35:52 -0700 Subject: [PATCH 04/12] Deprecate ImageBackground (#57412) Summary: Deprecates `ImageBackground` as part of the effort to keep core lean. It is a thin wrapper around a `View` and an `Image`, and can easily be replaced by composing a `View` with an absolutely positioned `Image` and layering children on top: ```tsx import type { ReactNode, Ref } from "react"; import { Image, StyleSheet, View, type ImageProps, type ViewProps, } from "react-native"; type ImageBackgroundProps = Omit & { children?: ReactNode; ref?: Ref; style?: ViewProps["style"]; }; export const ImageBackground = ({ children, importantForAccessibility, ref, style, ...props }: ImageBackgroundProps) => { const { height, width } = StyleSheet.flatten(style); return ( {children} ); }; ``` ## Changelog: [GENERAL] [DEPRECATED] - Deprecate `ImageBackground`, use a `View` with an absolutely positioned `Image` instead Pull Request resolved: https://github.com/react/react-native/pull/57412 Test Plan: - `deprecated` shows a strikethrough on `ImageBackground` in editors. - Importing/rendering `ImageBackground` logs the deprecation warning once. Reviewed By: javache Differential Revision: D110484567 Pulled By: cortinico fbshipit-source-id: 0f70893a0680b9bb34e1442bfef41e9210c08d07 --- .../react-native/Libraries/Image/ImageBackground.js | 4 ++++ packages/react-native/Libraries/Image/ImageProps.js | 8 +++++++- packages/react-native/index.js | 11 +++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Image/ImageBackground.js b/packages/react-native/Libraries/Image/ImageBackground.js index c8e0beae02df..15e2243d98f2 100644 --- a/packages/react-native/Libraries/Image/ImageBackground.js +++ b/packages/react-native/Libraries/Image/ImageBackground.js @@ -47,7 +47,11 @@ export type {ImageBackgroundProps} from './ImageProps'; * }); * ``` * + * ImageBackground is deprecated and will be removed in a future release. + * Use a `View` with an absolutely positioned `Image` instead. + * * @see https://reactnative.dev/docs/imagebackground + * @deprecated */ class ImageBackground extends React.Component { setNativeProps(props: {...}) { diff --git a/packages/react-native/Libraries/Image/ImageProps.js b/packages/react-native/Libraries/Image/ImageProps.js index a893ac450a60..631ba635bec4 100644 --- a/packages/react-native/Libraries/Image/ImageProps.js +++ b/packages/react-native/Libraries/Image/ImageProps.js @@ -338,7 +338,13 @@ export type ImageProps = Readonly<{ style?: ?ImageStyleProp, }>; -/** @build-types emit-as-interface Uniwind compatibility */ +/** + * ImageBackground is deprecated and will be removed in a future release. + * Use a `View` with an absolutely positioned `Image` instead. + * @see https://reactnative.dev/docs/imagebackground + * @deprecated + * @build-types emit-as-interface Uniwind compatibility + */ export type ImageBackgroundProps = Readonly<{ ...ImageProps, children?: React.Node, diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 533c97f038fa..c2873961a5f0 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -62,7 +62,18 @@ module.exports = { get Image() { return require('./Libraries/Image/Image').default; }, + /** + * @deprecated ImageBackground is deprecated and will be removed in a future release. + * Use a View with an absolutely positioned Image instead. + * See https://reactnative.dev/docs/imagebackground + */ get ImageBackground() { + warnOnce( + 'image-background-deprecated', + 'ImageBackground is deprecated and will be removed in a future release. ' + + 'Use a View with an absolutely positioned Image instead. ' + + 'See https://reactnative.dev/docs/imagebackground', + ); return require('./Libraries/Image/ImageBackground').default; }, get InputAccessoryView() { From c65845cde259807eb4892d0e37b80ca65be8c191 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 3 Jul 2026 05:26:04 -0700 Subject: [PATCH 05/12] Fix dropped React revision when merging commit branches (#57424) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57424 With commit branching (enableFabricCommitBranching), commits from React land on a separate revision (currentReactRevision_) that is later merged into the main tree. React-branch revisions are numbered off the main revision, so two React commits that occur before the main revision advances get the SAME revision number. mergeReactRevision decided whether to clear currentReactRevision_ by comparing revision *numbers*. When a newer React revision landed before the merge of the previous one completed, its coincidentally-equal number caused it to be cleared, silently dropping a pending update. It also cleared the revision even when the merge commit did not succeed. Fix: - Clear currentReactRevision_ by comparing root shadow node identity instead of the revision number, and only when the merge commit actually succeeded. - Compute the React-branch revision number from the snapshot taken under the shared lock instead of reading currentRevision_ without the (deferred) unique lock. Changelog: [General][Fixed] Fixed potential revision drop during merge Reviewed By: rubennorte Differential Revision: D110577969 fbshipit-source-id: b3f1f5aad8b3783e5080fde851571b62ceceaf94 --- .../react/renderer/mounting/ShadowTree.cpp | 22 ++- .../tests/ShadowTreeReactBranchingTest.cpp | 141 ++++++++++++++++++ 2 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeReactBranchingTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 93554c5b47c3..5633a99ad37d 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -428,7 +428,9 @@ CommitStatus ShadowTree::tryCommit( return CommitStatus::Failed; } - auto newRevisionNumber = currentRevision_.number + 1; + auto newRevisionNumber = isReactBranch + ? oldRevisionForStateProgression.number + 1 + : currentRevision_.number + 1; { std::scoped_lock dispatchLock(EventEmitter::DispatchMutex()); @@ -513,11 +515,12 @@ void ShadowTree::mergeReactRevision() const { } } - ShadowTreeRevision::Number lastMergedRevisionNumber; + RootShadowNode::Shared lastMergedRootShadowNode; + bool lastMergeSucceeded = false; if (isPropsUpdatesAccumulationGuaranteed()) { - lastMergedRevisionNumber = promotedRevision.number; - this->commit( + lastMergedRootShadowNode = promotedRevision.rootShadowNode; + auto status = this->commit( [revision = std::move(promotedRevision)]( const RootShadowNode& /*oldRootShadowNode*/) { return std::make_shared( @@ -528,13 +531,14 @@ void ShadowTree::mergeReactRevision() const { .mountSynchronously = true, .source = CommitSource::ReactRevisionMerge, }); + lastMergeSucceeded = status == CommitStatus::Succeeded; } else { for (size_t i = 0; i < promotedRevisions.size(); ++i) { auto& revision = promotedRevisions[i]; bool isLast = i == promotedRevisions.size() - 1; - lastMergedRevisionNumber = revision.number; + lastMergedRootShadowNode = revision.rootShadowNode; - this->commit( + auto status = this->commit( [revision = std::move(revision)]( const RootShadowNode& /*oldRootShadowNode*/) { return std::make_shared( @@ -545,6 +549,7 @@ void ShadowTree::mergeReactRevision() const { .mountSynchronously = true, .source = CommitSource::ReactRevisionMerge, }); + lastMergeSucceeded = status == CommitStatus::Succeeded; } } @@ -554,8 +559,9 @@ void ShadowTree::mergeReactRevision() const { // If the current react revision is the same as the one that was just // merged, clear it. - if (currentReactRevision_.has_value() && - lastMergedRevisionNumber == currentReactRevision_.value().number) { + if (lastMergeSucceeded && currentReactRevision_.has_value() && + currentReactRevision_.value().rootShadowNode == + lastMergedRootShadowNode) { currentReactRevision_.reset(); } } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeReactBranchingTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeReactBranchingTest.cpp new file mode 100644 index 000000000000..79d76a6094fb --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeReactBranchingTest.cpp @@ -0,0 +1,141 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace facebook::react; + +namespace { + +class BranchingEnabledFlags : public ReactNativeFeatureFlagsDefaults { + public: + bool enableFabricCommitBranching() override { + return true; + } +}; + +class DummyShadowTreeDelegate : public ShadowTreeDelegate { + public: + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree& /*shadowTree*/, + const RootShadowNode::Shared& /*oldRootShadowNode*/, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTree::CommitOptions& /*commitOptions*/) const override { + return newRootShadowNode; + } + + void shadowTreeDidFinishTransaction( + std::shared_ptr /*mountingCoordinator*/, + bool /*mountSynchronously*/) const override {} + + void shadowTreeDidFinishReactCommit( + const ShadowTree& /*shadowTree*/) const override {} + + void shadowTreeDidPromoteReactRevision( + const ShadowTree& /*shadowTree*/) const override {} +}; + +} // namespace + +class ShadowTreeReactBranchingTest : public ::testing::Test { + protected: + void SetUp() override { + // Must override before any component construction reads a flag. + ReactNativeFeatureFlags::dangerouslyReset(); + ReactNativeFeatureFlags::override( + std::make_unique()); + } + + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } +}; + +// Two React revisions committed before a merge share a revision number. +// Merging the first must not clear the second: the old number-based comparison +// cleared it (dropping a pending update); the fix compares root node identity. +TEST_F(ShadowTreeReactBranchingTest, mergeDoesNotDropNewerReactRevision) { + // clang-format off + auto element = + Element() + .children({ + Element() + }); + // clang-format on + + ContextContainer contextContainer{}; + auto builder = simpleComponentBuilder(); + auto initialRootShadowNode = builder.build(element); + auto shadowTreeDelegate = DummyShadowTreeDelegate{}; + + ShadowTree shadowTree{ + SurfaceId{11}, + LayoutConstraints{}, + LayoutContext{}, + shadowTreeDelegate, + contextContainer}; + + // Initial (non-React) commit establishes `currentRevision_`. + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast(initialRootShadowNode); + }, + {.enableStateReconciliation = false}); + + // First React revision (R1). + auto rootR1 = std::static_pointer_cast( + initialRootShadowNode->ShadowNode::clone({})); + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { return rootR1; }, + {.enableStateReconciliation = false, + .mountSynchronously = false, + .source = ShadowTreeCommitSource::React}); + + auto revisionAfterR1 = shadowTree.getCurrentReactRevision(); + ASSERT_TRUE(revisionAfterR1.has_value()); + + // Promote R1 (end-of-tick), but do not merge it yet. + shadowTree.promoteReactRevision(); + + // Second React revision (R2) lands before the merge. The main revision has + // not advanced, so R2 gets R1's revision number but is a distinct tree. + auto rootR2 = std::static_pointer_cast( + initialRootShadowNode->ShadowNode::clone({})); + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { return rootR2; }, + {.enableStateReconciliation = false, + .mountSynchronously = false, + .source = ShadowTreeCommitSource::React}); + + auto revisionAfterR2 = shadowTree.getCurrentReactRevision(); + ASSERT_TRUE(revisionAfterR2.has_value()); + + // Precondition for the bug: same number, different revisions. + EXPECT_EQ(revisionAfterR1->number, revisionAfterR2->number); + EXPECT_NE(revisionAfterR2->rootShadowNode, revisionAfterR1->rootShadowNode); + + // Merging R1 must not clear R2. + shadowTree.mergeReactRevision(); + + auto revisionAfterMerge = shadowTree.getCurrentReactRevision(); + ASSERT_TRUE(revisionAfterMerge.has_value()) + << "Newer React revision (R2) was incorrectly dropped while merging R1"; + EXPECT_EQ( + revisionAfterMerge->rootShadowNode, revisionAfterR2->rootShadowNode); +} From 97aa7ad2723fb8267f1737d4f7389dbbb9820517 Mon Sep 17 00:00:00 2001 From: Shahid R Date: Fri, 3 Jul 2026 06:42:22 -0700 Subject: [PATCH 06/12] Fix ClassCastException in clearFocusAndMaybeRefocus when EditText is detached (#57423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On Android 9 and below (`SDK_INT <= P`) in touch mode, `ReactEditText.clearFocusAndMaybeRefocus()` unconditionally casts `rootView` to `ViewGroup`: ```kotlin val rootViewGroup = rootView as ViewGroup ``` `View.getRootView()` returns the view **itself** when the view is detached from the window. An IME editor action is delivered asynchronously over Binder (`IInputConnectionWrapper`), so a submit-key press can arrive after the EditText has already been removed from the hierarchy (screen unmount/navigation racing the keyboard). When that happens the cast throws and kills the app: ``` java.lang.ClassCastException: com.facebook.react.views.textinput.ReactEditText cannot be cast to android.view.ViewGroup at com.facebook.react.views.textinput.ReactEditText.clearFocusAndMaybeRefocus (ReactEditText.kt:379) at com.facebook.react.views.textinput.ReactTextInputManager.addEventEmitters$lambda$3 (ReactTextInputManager.kt:933) at android.widget.TextView.onEditorAction (TextView.java:6615) at com.android.internal.widget.EditableInputConnection.performEditorAction (EditableInputConnection.java:138) at android.view.inputmethod.InputConnectionWrapper.performEditorAction (InputConnectionWrapper.java:190) at com.android.internal.view.IInputConnectionWrapper.executeMessage (IInputConnectionWrapper.java:360) ``` We see this steadily in production Crashlytics (RN 0.86, New Architecture): all events are on Android 7–9 devices (Samsung SM-J710GN / SM-G610F on 8.1.0, etc.), zero on Android 10+, because API > 28 takes the plain `super.clearFocus()` branch and never reaches the cast. This change replaces the unchecked cast with a safe cast and falls back to a plain `clearFocus()` when the root is not a `ViewGroup`. That fallback is correct because the only reason the root isn't a `ViewGroup` is that the view is already detached — there is no surviving focus hierarchy to protect with the `descendantFocusability` workaround, and `hideSoftKeyboard()` still runs afterwards. Behavior is unchanged on API > 28, in non-touch mode, and in the normal attached case on old Android. ## Changelog: [ANDROID] [FIXED] - Fix ClassCastException crash on Android 9 and below when an IME submit action races the unmount of a TextInput Pull Request resolved: https://github.com/react/react-native/pull/57423 Test Plan: The race is timing-dependent, so it is exercised by the scenario rather than a unit test: 1. On an API 26–28 emulator/device, render a single-line `` with default `submitBehavior` (`blurAndSubmit`), focused, keyboard open. 2. Unmount the input (conditional render / navigation) in the same frame as pressing the keyboard's submit key. The IME action arrives over Binder after the view is detached. 3. Before this change: `getRootView()` returns the detached `ReactEditText` itself → `ClassCastException` (stack above). After this change: the safe cast falls back to `super.clearFocus()` + `hideSoftKeyboard()`, no crash, no behavioral difference otherwise. Also verified: - On the attached path (normal blur-on-submit on API ≤ 28), `rootView` is the DecorView, the safe cast succeeds, and the existing descendant-focusability logic runs exactly as before. - On API > 28 the first branch is taken, unchanged. Reviewed By: cortinico Differential Revision: D110567654 Pulled By: javache fbshipit-source-id: eb580aed7d0d029eb265b815ba7bed5e5ac85b4c --- .../react/views/textinput/ReactEditText.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index a0146aaa4eef..0415dca1126b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -380,11 +380,20 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Avoid refocusing to a new view on old versions of Android by default // by preventing `requestFocus()` on the rootView from moving focus to any child. // https://cs.android.com/android/_/android/platform/frameworks/base/+/bdc66cb5a0ef513f4306edf9156cc978b08e06e4 - val rootViewGroup = rootView as ViewGroup - val oldDescendantFocusability = rootViewGroup.descendantFocusability - rootViewGroup.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS - super.clearFocus() - rootViewGroup.descendantFocusability = oldDescendantFocusability + // + // getRootView() returns the view itself when it is detached from the window, so the root + // is not necessarily a ViewGroup: an IME editor action delivered over Binder can race the + // removal of this view from the hierarchy. There is no focus to move in that case, so a + // plain clearFocus() is enough. + val rootViewGroup = rootView as? ViewGroup + if (rootViewGroup != null) { + val oldDescendantFocusability = rootViewGroup.descendantFocusability + rootViewGroup.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS + super.clearFocus() + rootViewGroup.descendantFocusability = oldDescendantFocusability + } else { + super.clearFocus() + } } hideSoftKeyboard() } From 91b2537f8ac5b29d022743ab7246d860343c3e81 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 3 Jul 2026 08:36:23 -0700 Subject: [PATCH 07/12] Allow to disable shouldDelayChildPressedState on scrollable Android containers (#57259) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57259 X-link: https://github.com/facebook/react-native/pull/57128 Changelog: [Android][Added] Added an entry point that allows changing whether the scrollable React Native containers should delay pressed state in children views Scrollable Android containers should return `true` (the default implementation) from [`shouldDelayChildPressedState`](https://developer.android.com/reference/android/view/ViewGroup#shouldDelayChildPressedState%28%29) in order to delay pressed state feedback in children. This way the feedback isn't triggered at all during quick scrolls. React Native touch system doesn't rely on this so it's not affected by that behavior, but native components are which can produce a divergent experience. This diff adds a property to all scrollable components of React Native which allows external consumers to control this behavor on a per-view basis. The API is analoguous to [`delaysContentTouches`](https://developer.apple.com/documentation/uikit/uiscrollview/delayscontenttouches?language=objc) on iOS. Reviewed By: javache Differential Revision: D108003375 fbshipit-source-id: 2858f930ec7eef0d31660ea3c14d8a9429431e4f --- .../ReactAndroid/api/ReactAndroid.api | 25 +++++++++++++--- .../uimanager/HasChildPressedStateDelay.kt | 29 +++++++++++++++++++ .../react/views/drawer/ReactDrawerLayout.kt | 9 +++++- .../views/scroll/ReactHorizontalScrollView.kt | 8 ++++- .../views/scroll/ReactNestedScrollView.kt | 10 +++++-- .../react/views/scroll/ReactScrollView.kt | 8 ++++- .../swiperefresh/ReactSwipeRefreshLayout.kt | 8 ++++- 7 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/HasChildPressedStateDelay.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c4e2927d1568..53e1ca1c2631 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3313,6 +3313,11 @@ public abstract class com/facebook/react/uimanager/GuardedFrameCallback : androi protected abstract fun doFrameGuarded (J)V } +public abstract interface class com/facebook/react/uimanager/HasChildPressedStateDelay { + public abstract fun getHasChildPressedStateDelay ()Ljava/lang/Boolean; + public abstract fun setHasChildPressedStateDelay (Ljava/lang/Boolean;)V +} + public abstract interface class com/facebook/react/uimanager/IViewGroupManager : com/facebook/react/uimanager/IViewManagerWithChildren { public abstract fun addView (Landroid/view/View;Landroid/view/View;I)V public abstract fun getChildAt (Landroid/view/View;I)Landroid/view/View; @@ -5156,10 +5161,13 @@ public abstract interface class com/facebook/react/viewmanagers/VirtualViewManag public abstract fun setRenderState (Landroid/view/View;I)V } -public final class com/facebook/react/views/drawer/ReactDrawerLayout : androidx/drawerlayout/widget/DrawerLayout { +public final class com/facebook/react/views/drawer/ReactDrawerLayout : androidx/drawerlayout/widget/DrawerLayout, com/facebook/react/uimanager/HasChildPressedStateDelay { public fun (Lcom/facebook/react/bridge/ReactContext;)V + public fun getHasChildPressedStateDelay ()Ljava/lang/Boolean; public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z public fun onTouchEvent (Landroid/view/MotionEvent;)Z + public fun setHasChildPressedStateDelay (Ljava/lang/Boolean;)V + public fun shouldDelayChildPressedState ()Z } public final class com/facebook/react/views/drawer/ReactDrawerLayoutManager : com/facebook/react/uimanager/ViewGroupManager, com/facebook/react/viewmanagers/AndroidDrawerLayoutManagerInterface { @@ -5460,7 +5468,7 @@ public final class com/facebook/react/views/scroll/ReactHorizontalScrollContaine public final class com/facebook/react/views/scroll/ReactHorizontalScrollContainerViewManager$Companion { } -public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android/widget/HorizontalScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper, com/facebook/react/views/scroll/VirtualViewContainer { +public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android/widget/HorizontalScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/HasChildPressedStateDelay, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper, com/facebook/react/views/scroll/VirtualViewContainer { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;)V public synthetic fun (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -5480,6 +5488,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun getFadingEdgeLengthStart ()I public fun getFlingAnimator ()Landroid/animation/ValueAnimator; public fun getFlingExtrapolatedDistance (I)I + public fun getHasChildPressedStateDelay ()Ljava/lang/Boolean; public fun getLastScrollDispatchTime ()J protected fun getLeftFadingEdgeStrength ()F public fun getOverflow ()Ljava/lang/String; @@ -5527,6 +5536,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun setEndFillColor (I)V public fun setFadingEdgeLengthEnd (I)V public fun setFadingEdgeLengthStart (I)V + public fun setHasChildPressedStateDelay (Ljava/lang/Boolean;)V public fun setLastScrollDispatchTime (J)V public fun setOverflow (Ljava/lang/String;)V public fun setOverflowInset (IIII)V @@ -5545,6 +5555,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun setSnapToEnd (Z)V public fun setSnapToStart (Z)V public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V + public fun shouldDelayChildPressedState ()Z public fun startFlingAnimator (II)V public fun updateClippingRect ()V public fun updateClippingRect (Ljava/util/Set;)V @@ -5607,7 +5618,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager : public final class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager$Companion { } -public class com/facebook/react/views/scroll/ReactScrollView : android/widget/ScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper, com/facebook/react/views/scroll/VirtualViewContainer { +public class com/facebook/react/views/scroll/ReactScrollView : android/widget/ScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/HasChildPressedStateDelay, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper, com/facebook/react/views/scroll/VirtualViewContainer { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;)V public synthetic fun (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -5625,6 +5636,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun getFadingEdgeLengthStart ()I public fun getFlingAnimator ()Landroid/animation/ValueAnimator; public fun getFlingExtrapolatedDistance (I)I + public fun getHasChildPressedStateDelay ()Ljava/lang/Boolean; public fun getLastScrollDispatchTime ()J protected fun getOverScrollerFromParent ()Landroid/widget/OverScroller; public fun getOverflow ()Ljava/lang/String; @@ -5671,6 +5683,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun setEndFillColor (I)V public fun setFadingEdgeLengthEnd (I)V public fun setFadingEdgeLengthStart (I)V + public fun setHasChildPressedStateDelay (Ljava/lang/Boolean;)V public fun setLastScrollDispatchTime (J)V public fun setOverflow (Ljava/lang/String;)V public fun setOverflowInset (IIII)V @@ -5691,6 +5704,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun setSnapToEnd (Z)V public fun setSnapToStart (Z)V public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V + public fun shouldDelayChildPressedState ()Z public fun startFlingAnimator (II)V public fun updateClippingRect ()V public fun updateClippingRect (Ljava/util/Set;)V @@ -5959,15 +5973,18 @@ public final class com/facebook/react/views/scroll/VirtualViewContainerState$Com public final fun create (Landroid/view/ViewGroup;)Lcom/facebook/react/views/scroll/VirtualViewContainerState; } -public class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout { +public class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout, com/facebook/react/uimanager/HasChildPressedStateDelay { public fun (Lcom/facebook/react/bridge/ReactContext;)V public fun canChildScrollUp ()Z + public fun getHasChildPressedStateDelay ()Ljava/lang/Boolean; public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z public fun onLayout (ZIIII)V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun requestDisallowInterceptTouchEvent (Z)V + public fun setHasChildPressedStateDelay (Ljava/lang/Boolean;)V public final fun setProgressViewOffset (F)V public fun setRefreshing (Z)V + public fun shouldDelayChildPressedState ()Z } public final class com/facebook/react/views/text/DefaultStyleValuesUtil { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/HasChildPressedStateDelay.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/HasChildPressedStateDelay.kt new file mode 100644 index 000000000000..be102ecaebaa --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/HasChildPressedStateDelay.kt @@ -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. + */ + +package com.facebook.react.uimanager + +/** + * Implemented by scrollable container views that allow the delaying of their children's pressed + * state (see [android.view.ViewGroup.shouldDelayChildPressedState]) to be toggled externally, + * without changing the view's default behavior. + * + * This lets a module that holds a reference to such a container disable the delay (e.g. when it + * knows the gesture will not turn into a scroll, so children should show their pressed state + * immediately) and re-enable it afterwards. + */ +public interface HasChildPressedStateDelay { + /** + * Overrides whether this view delays its children's pressed state (see + * [android.view.ViewGroup.shouldDelayChildPressedState]). + * + * When `null` (the default), the view's framework default is used. Set to `true` or `false` to + * force the behavior, and back to `null` to restore the default. Lets a module holding a + * reference to the container toggle the delay without changing the default. + */ + public var hasChildPressedStateDelay: Boolean? +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.kt index 2c87d6405507..7523bf1fe930 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.kt @@ -20,6 +20,7 @@ import com.facebook.common.logging.FLog import com.facebook.react.R import com.facebook.react.bridge.ReactContext import com.facebook.react.common.ReactConstants +import com.facebook.react.uimanager.HasChildPressedStateDelay import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole import com.facebook.react.uimanager.events.NativeGestureUtil.notifyNativeGestureEnded import com.facebook.react.uimanager.events.NativeGestureUtil.notifyNativeGestureStarted @@ -28,11 +29,14 @@ import com.facebook.react.uimanager.events.NativeGestureUtil.notifyNativeGesture * Wrapper view for [DrawerLayout]. It manages the properties that can be set on the drawer and * contains some ReactNative-specific functionality. */ -public class ReactDrawerLayout(reactContext: ReactContext) : DrawerLayout(reactContext) { +public class ReactDrawerLayout(reactContext: ReactContext) : + DrawerLayout(reactContext), HasChildPressedStateDelay { private var drawerPosition = Gravity.START private var drawerWidth = DEFAULT_DRAWER_WIDTH private var dragging = false + override var hasChildPressedStateDelay: Boolean? = null + init { ViewCompat.setAccessibilityDelegate( this, @@ -60,6 +64,9 @@ public class ReactDrawerLayout(reactContext: ReactContext) : DrawerLayout(reactC ) } + override fun shouldDelayChildPressedState(): Boolean = + hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { try { if (super.onInterceptTouchEvent(ev)) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt index f2b308dd88fd..1e455eb2ba5c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt @@ -36,6 +36,7 @@ import com.facebook.react.common.ReactConstants import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.HasChildPressedStateDelay import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.MeasureSpecAssertions @@ -84,7 +85,8 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : HasFlingAnimator, HasScrollEventThrottle, HasSmoothScroll, - VirtualViewContainer { + VirtualViewContainer, + HasChildPressedStateDelay { private companion object { private val DEBUG_MODE = false && ReactBuildConfig.DEBUG @@ -197,6 +199,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override var stateWrapper: StateWrapper? = null override var scrollEventThrottle: Int = 0 override var lastScrollDispatchTime: Long = 0L + override var hasChildPressedStateDelay: Boolean? = null override val virtualViewContainerState: VirtualViewContainerState get() = @@ -631,6 +634,9 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : } } + override fun shouldDelayChildPressedState(): Boolean = + hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt index c8adcdfd082a..0a592234d2b0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2b0cbc5249ac34ae7f030a9c0fffd1f3>> + * @generated SignedSource<> */ /** @@ -38,6 +38,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.ReactConstants import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.HasChildPressedStateDelay import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.MeasureSpecAssertions @@ -93,7 +94,8 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : HasFlingAnimator, HasScrollEventThrottle, HasSmoothScroll, - VirtualViewContainer { + VirtualViewContainer, + HasChildPressedStateDelay { private companion object { private var scrollerField: java.lang.reflect.Field? = null @@ -107,6 +109,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override var stateWrapper: StateWrapper? = null override var scrollEventThrottle: Int = 0 override var lastScrollDispatchTime: Long = 0L + override var hasChildPressedStateDelay: Boolean? = null public open var pointerEvents: PointerEvents = PointerEvents.AUTO @@ -552,6 +555,9 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : } } + override fun shouldDelayChildPressedState(): Boolean = + hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt index bd32aba8da9e..691a57cbc33c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt @@ -30,6 +30,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.ReactConstants import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.HasChildPressedStateDelay import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.MeasureSpecAssertions @@ -85,7 +86,8 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : HasFlingAnimator, HasScrollEventThrottle, HasSmoothScroll, - VirtualViewContainer { + VirtualViewContainer, + HasChildPressedStateDelay { private companion object { private var scrollerField: java.lang.reflect.Field? = null @@ -99,6 +101,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override var stateWrapper: StateWrapper? = null override var scrollEventThrottle: Int = 0 override var lastScrollDispatchTime: Long = 0L + override var hasChildPressedStateDelay: Boolean? = null public open var pointerEvents: PointerEvents = PointerEvents.AUTO @@ -544,6 +547,9 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : } } + override fun shouldDelayChildPressedState(): Boolean = + hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt index 01431e9b6853..d2c6fc8d6d90 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt @@ -12,13 +12,14 @@ import android.view.ViewConfiguration import android.view.ViewGroup import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.HasChildPressedStateDelay import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.events.NativeGestureUtil import kotlin.math.abs /** Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. */ public open class ReactSwipeRefreshLayout(reactContext: ReactContext) : - SwipeRefreshLayout(reactContext) { + SwipeRefreshLayout(reactContext), HasChildPressedStateDelay { private var didLayout: Boolean = false private var refreshing: Boolean = false @@ -28,6 +29,8 @@ public open class ReactSwipeRefreshLayout(reactContext: ReactContext) : private var intercepted: Boolean = false private var nativeGestureStarted: Boolean = false + override var hasChildPressedStateDelay: Boolean? = null + public override fun setRefreshing(refreshing: Boolean) { this.refreshing = refreshing @@ -79,6 +82,9 @@ public open class ReactSwipeRefreshLayout(reactContext: ReactContext) : parent?.requestDisallowInterceptTouchEvent(disallowIntercept) } + override fun shouldDelayChildPressedState(): Boolean = + hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + public override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) { NativeGestureUtil.notifyNativeGestureStarted(this, ev) From bb3c121073a84cd14553bbee036f325eeb294478 Mon Sep 17 00:00:00 2001 From: Andrew Ghostuhin Date: Fri, 3 Jul 2026 09:22:39 -0700 Subject: [PATCH 08/12] Fix Android text descender clipping with tight lineHeight (#57115) Summary: Fixes https://github.com/react/react-native/issues/49886. Close torin-asakura/workspace#115. Android TextView clips text drawing to the view bounds in its default onDraw path. For explicit lineHeight, CustomLineHeightSpan should keep paragraph layout bounds tied to the requested lineHeight; using font top/bottom to avoid clipping fixes the single-line repro but expands multiline text. This keeps the line-height layout contract intact and draws ordinary non-selectable ReactTextView text directly when overflow is visible, avoiding TextView's internal clipRect. Selectable/linkify text and non-visible overflow continue to use the regular TextView drawing path. ## Changelog: [ANDROID] [FIXED] - Prevent descenders from being clipped when Android text lineHeight matches fontSize Pull Request resolved: https://github.com/react/react-native/pull/57115 Test Plan: Command: ```sh ./gradlew -Preact.internal.useHermesStable=true :packages:react-native:ReactAndroid:testDebugUnitTest --tests com.facebook.react.views.text.internal.span.CustomLineHeightSpanTest --tests com.facebook.react.views.text.ReactTextViewTest ``` Result: ```text BUILD SUCCESSFUL ``` Command: ```sh ./gradlew -Preact.internal.useHermesStable=true :packages:react-native:ReactAndroid:ktfmtCheck ``` Result: ```text BUILD SUCCESSFUL ``` Command: ```sh git diff --check ``` Result: no output. Command: ```sh ./node_modules/.bin/prettier --check packages/rn-tester/js/examples/Text/TextExample.android.js ``` Result: ```text All matched files use Prettier code style! ``` Command: ```sh ./node_modules/.bin/eslint --max-warnings 0 packages/rn-tester/js/examples/Text/TextExample.android.js ``` Result: no output. RNTester visual repro with fontSize: 24, lineHeight: 24, and gjpqy. Visual evidence:
Before this PR ![rn57115-before.png](https://github.com/user-attachments/assets/76c0a1a4-effe-4718-a92e-fb9a20c79a33)
After this PR ![rn57115-after.png](https://github.com/user-attachments/assets/881b3ab3-a273-439d-9155-b4c9f393496d)
Reviewed By: cortinico Differential Revision: D108406457 Pulled By: javache fbshipit-source-id: be5e33ebca61dc24a3aa18376c49cf6690891d07 --- .../react/views/text/ReactTextView.java | 98 ++++++++--- .../react/views/text/ReactTextViewTest.kt | 154 ++++++++++++++++++ .../internal/span/CustomLineHeightSpanTest.kt | 99 +++++++++++ 3 files changed, 330 insertions(+), 21 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpanTest.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index ff4357278512..6b6b848385e5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -218,28 +218,16 @@ protected void onDraw(Canvas canvas) { if (layout != null) { CanvasEffectSpan[] drawSpans = spanned.getSpans(0, spanned.length(), CanvasEffectSpan.class); - if (drawSpans.length > 0) { - canvas.save(); - canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); - for (CanvasEffectSpan span : drawSpans) { - int start = spanned.getSpanStart(span); - int end = spanned.getSpanEnd(span); - span.onPreDraw(start, end, canvas, layout); - } - canvas.restore(); - - super.onDraw(canvas); - - canvas.save(); - canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); - for (CanvasEffectSpan span : drawSpans) { - int start = spanned.getSpanStart(span); - int end = spanned.getSpanEnd(span); - span.onDraw(start, end, canvas, layout); - } - canvas.restore(); + if (shouldDrawLayoutWithoutTextViewClip()) { + drawLayoutWithoutTextViewClip(canvas, spanned, layout, drawSpans); } else { - super.onDraw(canvas); + if (drawSpans.length > 0) { + drawTextEffects(canvas, spanned, layout, drawSpans, true, false); + super.onDraw(canvas); + drawTextEffects(canvas, spanned, layout, drawSpans, false, false); + } else { + super.onDraw(canvas); + } } } else { super.onDraw(canvas); @@ -250,6 +238,74 @@ protected void onDraw(Canvas canvas) { } } + private boolean shouldDrawLayoutWithoutTextViewClip() { + return mOverflow == Overflow.VISIBLE && !mTextIsSelectable && getMovementMethod() == null; + } + + private void drawLayoutWithoutTextViewClip( + Canvas canvas, Spannable spanned, Layout layout, CanvasEffectSpan[] drawSpans) { + getPaint().setColor(getCurrentTextColor()); + getPaint().drawableState = getDrawableState(); + + drawTextEffects(canvas, spanned, layout, drawSpans, true, true); + + canvas.save(); + canvas.translate( + getCompoundPaddingLeft(), getExtendedPaddingTop() + getVerticalGravityOffset(layout)); + layout.draw(canvas); + canvas.restore(); + + drawTextEffects(canvas, spanned, layout, drawSpans, false, true); + } + + private void drawTextEffects( + Canvas canvas, + Spannable spanned, + Layout layout, + CanvasEffectSpan[] drawSpans, + boolean beforeText, + boolean includeVerticalGravityOffset) { + if (drawSpans.length == 0) { + return; + } + + canvas.save(); + canvas.translate( + getCompoundPaddingLeft(), + getExtendedPaddingTop() + + (includeVerticalGravityOffset ? getVerticalGravityOffset(layout) : 0)); + for (CanvasEffectSpan span : drawSpans) { + int start = spanned.getSpanStart(span); + int end = spanned.getSpanEnd(span); + if (beforeText) { + span.onPreDraw(start, end, canvas, layout); + } else { + span.onDraw(start, end, canvas, layout); + } + } + canvas.restore(); + } + + private int getVerticalGravityOffset(Layout layout) { + int availableVerticalSpace = getAvailableVerticalSpace(); + if (layout.getHeight() >= availableVerticalSpace) { + return 0; + } + + int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; + if (verticalGravity == Gravity.BOTTOM) { + return availableVerticalSpace - layout.getHeight(); + } else if (verticalGravity == Gravity.CENTER_VERTICAL) { + return (availableVerticalSpace - layout.getHeight()) / 2; + } + + return 0; + } + + private int getAvailableVerticalSpace() { + return getHeight() - getExtendedPaddingTop() - getExtendedPaddingBottom(); + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { try (SystraceSection s = new SystraceSection("ReactTextView.onMeasure")) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt new file mode 100644 index 000000000000..3cbb6435cf33 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt @@ -0,0 +1,154 @@ +/* + * 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. + */ + +package com.facebook.react.views.text + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ReplacementSpan +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import androidx.core.graphics.createBitmap +import androidx.core.graphics.get +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class ReactTextViewTest { + + @Test + fun drawsGlyphInkOutsideLineHeightWhenOverflowIsVisible() { + val bitmap = drawReactTextViewWithOverflow(null) + + assertThat(hasVisiblePixelBelowViewBounds(bitmap)).isTrue() + } + + @Test + fun bottomGravityDoesNotShiftLayoutUpWhenTextIsTallerThanView() { + val lineHeight = 48 + val viewHeight = 24 + val bitmap = drawReactTextViewWithOverflow(null, lineHeight, viewHeight, Gravity.BOTTOM) + + assertThat(firstVisiblePixelY(bitmap)).isGreaterThanOrEqualTo(lineHeight) + } + + private fun drawReactTextViewWithOverflow(overflow: String?): Bitmap { + return drawReactTextViewWithOverflow(overflow, lineHeight = 24, viewHeight = 24, gravity = null) + } + + private fun drawReactTextViewWithOverflow( + overflow: String?, + lineHeight: Int, + viewHeight: Int, + gravity: Int?, + ): Bitmap { + val width = 200 + val bitmapHeight = 80 + val text = SpannableString("x") + text.setSpan( + OverflowingInkSpan(lineHeight), + 0, + text.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + + val view = TestReactTextView(RuntimeEnvironment.getApplication()) + view.setTextColor(Color.BLACK) + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, 24f) + view.includeFontPadding = true + view.setSpanned(text) + view.text = text + view.setOverflow(overflow) + if (gravity != null) { + view.setGravityVertical(gravity) + } + view.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(viewHeight, View.MeasureSpec.EXACTLY), + ) + view.layout(0, 0, width, viewHeight) + + return createBitmap(width, bitmapHeight).also { + view.drawTextForTest(Canvas(it)) + } + } + + private fun hasVisiblePixelBelowViewBounds(bitmap: Bitmap): Boolean { + for (y in 24 until bitmap.height) { + for (x in 0 until bitmap.width) { + if (Color.alpha(bitmap[x, y]) != 0) { + return true + } + } + } + + return false + } + + private fun firstVisiblePixelY(bitmap: Bitmap): Int { + for (y in 0 until bitmap.height) { + for (x in 0 until bitmap.width) { + if (Color.alpha(bitmap[x, y]) != 0) { + return y + } + } + } + + return bitmap.height + } + + private class TestReactTextView(context: Context) : ReactTextView(context) { + fun drawTextForTest(canvas: Canvas) { + super.draw(canvas) + } + } + + private class OverflowingInkSpan(private val lineHeight: Int) : ReplacementSpan() { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt?, + ): Int { + fm?.ascent = -lineHeight + fm?.descent = 0 + fm?.top = -lineHeight + fm?.bottom = 0 + return lineHeight + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint, + ) { + canvas.drawRect( + x, + y + (lineHeight / 4f), + x + lineHeight, + y + (lineHeight / 2f), + paint, + ) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpanTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpanTest.kt new file mode 100644 index 000000000000..a8c9f84d19e7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpanTest.kt @@ -0,0 +1,99 @@ +/* + * 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. + */ + +package com.facebook.react.views.text.internal.span + +import android.graphics.Paint +import android.text.Layout +import android.text.SpannableString +import android.text.Spanned +import android.text.StaticLayout +import android.text.TextPaint +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CustomLineHeightSpanTest { + + @Test + fun tightLineHeightDoesNotClipFirstOrLastLineFontBounds() { + val span = CustomLineHeightSpan(16f) + val fm = + Paint.FontMetricsInt().apply { + top = -18 + ascent = -14 + descent = 6 + bottom = 8 + } + + span.chooseHeight("gjpqy", 0, 5, 0, 0, fm) + + assertThat(fm.ascent).isEqualTo(-12) + assertThat(fm.descent).isEqualTo(4) + assertThat(fm.top).isEqualTo(-12) + assertThat(fm.bottom).isEqualTo(4) + } + + @Test + fun looseLineHeightStillExpandsFirstAndLastLineBounds() { + val span = CustomLineHeightSpan(24f) + val fm = + Paint.FontMetricsInt().apply { + top = -18 + ascent = -14 + descent = 6 + bottom = 8 + } + + span.chooseHeight("gjpqy", 0, 5, 0, 0, fm) + + assertThat(fm.ascent).isEqualTo(-16) + assertThat(fm.descent).isEqualTo(8) + assertThat(fm.top).isEqualTo(-16) + assertThat(fm.bottom).isEqualTo(8) + } + + @Test + fun tightLineHeightDoesNotExpandStaticLayoutHeightWithFontPadding() { + val layout = buildStaticLayout("gjpqy\ngjpqy\ngjpqy", lineHeight = 24) + + assertThat(layout.lineCount).isEqualTo(3) + assertThat(layout.height).isEqualTo(72) + } + + @Test + fun tightLineHeightDoesNotExpandSingleLineStaticLayoutHeightWithFontPadding() { + val layout = buildStaticLayout("gjpqy", lineHeight = 24) + + assertThat(layout.lineCount).isEqualTo(1) + assertThat(layout.height).isEqualTo(24) + } + + private fun buildStaticLayout(text: String, lineHeight: Int): StaticLayout { + val spannable = SpannableString(text) + spannable.setSpan( + CustomLineHeightSpan(lineHeight.toFloat()), + 0, + text.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + + return StaticLayout.Builder.obtain( + spannable, + 0, + spannable.length, + TextPaint().apply { textSize = 24f }, + 400, + ) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setIncludePad(true) + .setLineSpacing(0f, 1f) + .build() + } +} From 4cd278242bb6b0cd49f153e20a3b4accee86f976 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 3 Jul 2026 09:44:55 -0700 Subject: [PATCH 09/12] Extract react/bridging into its own React-bridging pod (#57354) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57354 Split the `react/bridging` headers out of the `ReactCommon` pod's `turbomodule/bridging` subspec into a standalone `React-bridging` podspec, mirroring the existing `React-bridging` SwiftPM target. This makes bridging a dependency-graph leaf so other modules can depend on it without pulling in `ReactCommon` / `React-cxxreact`. Consumers that previously reached `react/bridging` headers transitively through `ReactCommon` now resolve them through the standalone pod. The CocoaPods header-search-path injection in `update_search_paths` lists `React-bridging` alongside the other core frameworks so the headers resolve everywhere, and pods that link the `LongLivedObject` / `CallbackWrapper` symbols (`React-Fabric`, the nativemodule feature pods, `React-NativeModulesApple`, `React-RCTFBReactNativeSpec`) gain an explicit `React-bridging` dependency. All `ReactCommon/turbomodule/bridging` references in podspecs, the autolinker, the codegen template, and their test snapshots are repointed to `React-bridging`. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D109868509 fbshipit-source-id: 20f90877c45dd39feb04703a45d5ecef6bb165a0 --- packages/react-native/Package.swift | 8 ++-- .../React/React-RCTFBReactNativeSpec.podspec | 2 +- .../ReactCommon/React-Fabric.podspec | 1 + .../ReactCommon/ReactCommon.podspec | 14 +----- .../React-jserrorhandler.podspec | 2 +- .../react/bridging/React-bridging.podspec | 44 +++++++++++++++++++ .../ios/React-NativeModulesApple.podspec | 2 +- .../dom/React-domnativemodule.podspec | 1 + .../React-idlecallbacksnativemodule.podspec | 1 + ...t-intersectionobservernativemodule.podspec | 1 + ...React-mutationobservernativemodule.podspec | 1 + .../React-webperformancenativemodule.podspec | 1 + .../cocoapods/__tests__/codegen_utils-test.rb | 2 +- .../__tests__/new_architecture-test.rb | 4 +- .../scripts/cocoapods/new_architecture.rb | 2 +- .../react-native/scripts/cocoapods/utils.rb | 1 + .../generate-artifacts-executor-test.js.snap | 4 +- .../templates/ReactCodegen.podspec.template | 2 +- .../react-native/scripts/react_native_pods.rb | 1 + 19 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/bridging/React-bridging.podspec diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index d2c487b8970b..00309bef7e6a 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -262,13 +262,13 @@ let reactRuntimeScheduler = RNTarget( dependencies: [.reactNativeDependencies, .reactFeatureFlags, .reactCxxReact, .reactPerfLogger, .reactPerformanceTimeline, .reactRendererConsistency, .reactUtils, .reactRuntimeExecutor] ) -/// ReactCommon.podspec -/// This target represent the ReactCommon/turbomodule/bridging subspec +/// React-bridging.podspec let reactTurboModuleBridging = RNTarget( name: .reactTurboModuleBridging, path: "ReactCommon/react/bridging", + searchPaths: [CallInvokerPath], excludedPaths: ["tests"], - dependencies: [.reactNativeDependencies, .reactPerfLogger, .reactCxxReact, .jsi, .logger] + dependencies: [.reactNativeDependencies, .jsi] ) /// React-jserrorhandler.podspec @@ -904,7 +904,7 @@ extension String { static let reactRCTLinking = "React-RCTLinking" static let reactCoreModules = "React-CoreModules" static let reactRCTAnimatedModuleProvider = "RCTAnimatedModuleProvider" - static let reactTurboModuleBridging = "ReactCommon/turbomodule/bridging" + static let reactTurboModuleBridging = "React-bridging" static let reactTurboModuleCore = "ReactCommon/turbomodule/core" static let reactTurboModuleCoreDefaults = "ReactCommon/turbomodule/core/defaults" static let reactTurboModuleCoreMicrotasks = "ReactCommon/turbomodule/core/microtasks" diff --git a/packages/react-native/React/React-RCTFBReactNativeSpec.podspec b/packages/react-native/React/React-RCTFBReactNativeSpec.podspec index 1abb84e7e473..f9978b78aca4 100644 --- a/packages/react-native/React/React-RCTFBReactNativeSpec.podspec +++ b/packages/react-native/React/React-RCTFBReactNativeSpec.podspec @@ -54,7 +54,7 @@ Pod::Spec.new do |s| s.dependency "React-NativeModulesApple" add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) - add_dependency(s, "ReactCommon", :subspec => "turbomodule/bridging", :additional_framework_paths => ["react/nativemodule/bridging"]) + s.dependency "React-bridging" depend_on_js_engine(s) add_rn_third_party_dependencies(s) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index a92fe2abc49d..183c039786eb 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -45,6 +45,7 @@ Pod::Spec.new do |s| s.dependency "React-featureflags" s.dependency "React-runtimescheduler" s.dependency "React-cxxreact" + s.dependency "React-bridging" add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"]) add_dependency(s, "React-rendererdebug") diff --git a/packages/react-native/ReactCommon/ReactCommon.podspec b/packages/react-native/ReactCommon/ReactCommon.podspec index 83e81864640d..a6a9ce6b5668 100644 --- a/packages/react-native/ReactCommon/ReactCommon.podspec +++ b/packages/react-native/ReactCommon/ReactCommon.podspec @@ -43,27 +43,17 @@ Pod::Spec.new do |s| s.subspec "turbomodule" do |ss| ss.dependency "React-callinvoker", version ss.dependency "React-perflogger", version - ss.dependency "React-cxxreact", version ss.dependency "React-jsi", version ss.dependency "React-logger", version if use_hermes() ss.dependency "hermes-engine" end - ss.subspec "bridging" do |sss| - sss.dependency "React-jsi", version - sss.source_files = podspec_sources("react/bridging/**/*.{cpp,h}", "react/bridging/**/*.h") - sss.exclude_files = "react/bridging/tests" - sss.header_dir = "react/bridging" - sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\"" } - if use_hermes() - sss.dependency "hermes-engine" - end - end - ss.subspec "core" do |sss| sss.source_files = podspec_sources("react/nativemodule/core/ReactCommon/**/*.{cpp,h}", "react/nativemodule/core/ReactCommon/**/*.h") sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_CONFIGURATION_BUILD_DIR)/React-debug/React_debug.framework/Headers\" \"$(PODS_CONFIGURATION_BUILD_DIR)/React-debug/React_featureflags.framework/Headers\" \"$(PODS_CONFIGURATION_BUILD_DIR)/React-utils/React_utils.framework/Headers\"" } + sss.dependency "React-bridging" + sss.dependency "React-cxxreact", version sss.dependency "React-debug", version sss.dependency "React-featureflags", version sss.dependency "React-utils", version diff --git a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec index a5cc33ecc99a..9f6e92d59a20 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec +++ b/packages/react-native/ReactCommon/jserrorhandler/React-jserrorhandler.podspec @@ -38,7 +38,7 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "React-cxxreact" - s.dependency "ReactCommon/turbomodule/bridging" + s.dependency "React-bridging" add_dependency(s, "React-featureflags") add_dependency(s, "React-debug") diff --git a/packages/react-native/ReactCommon/react/bridging/React-bridging.podspec b/packages/react-native/ReactCommon/react/bridging/React-bridging.podspec new file mode 100644 index 000000000000..9e79373a2aae --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridging/React-bridging.podspec @@ -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. + +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 + +Pod::Spec.new do |s| + s.name = "React-bridging" + s.version = version + s.summary = "-" + 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 = podspec_sources("*.{cpp,h}", "*.h") + s.header_dir = "react/bridging" + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "YES", + "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), + "DEFINES_MODULE" => "YES" + } + + resolve_use_frameworks(s, header_mappings_dir: "../..", module_name: "React_bridging") + + s.dependency "React-jsi" + s.dependency "React-callinvoker" + s.dependency "React-timing" + + add_rn_third_party_dependencies(s) + add_rncore_dependency(s) +end diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec index 7fd2869614b6..92bf47ef5764 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.source_files = podspec_sources("ReactCommon/**/*.{mm,cpp,h}", "ReactCommon/**/*.{h}") s.dependency "ReactCommon/turbomodule/core" - s.dependency "ReactCommon/turbomodule/bridging" + s.dependency "React-bridging" s.dependency "React-callinvoker" s.dependency "React-Core" s.dependency "React-cxxreact" diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec index 6119ccb41057..6cc38a3b0fa7 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec @@ -51,6 +51,7 @@ Pod::Spec.new do |s| s.dependency "Yoga" s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-bridging" s.dependency "React-Fabric" s.dependency "React-Fabric/bridging" s.dependency "React-FabricComponents" diff --git a/packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec index 46327b2c74d6..654e412d5fea 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec @@ -48,6 +48,7 @@ Pod::Spec.new do |s| add_rncore_dependency(s) s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-bridging" s.dependency "React-runtimescheduler" add_dependency(s, "React-RCTFBReactNativeSpec") add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"]) diff --git a/packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/React-intersectionobservernativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/React-intersectionobservernativemodule.podspec index 089512b1cf9e..4a98cc332a52 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/React-intersectionobservernativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/React-intersectionobservernativemodule.podspec @@ -55,6 +55,7 @@ Pod::Spec.new do |s| add_rncore_dependency(s) s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-bridging" s.dependency "React-Fabric" s.dependency "React-Fabric/bridging" diff --git a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/React-mutationobservernativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/React-mutationobservernativemodule.podspec index 4ce39514ae20..67b2cbd6052b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/React-mutationobservernativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/React-mutationobservernativemodule.podspec @@ -55,6 +55,7 @@ Pod::Spec.new do |s| add_rncore_dependency(s) s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-bridging" s.dependency "React-Fabric" s.dependency "React-Fabric/bridging" diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec index 97b52ac93a04..e1615a6ba578 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec @@ -52,6 +52,7 @@ Pod::Spec.new do |s| add_rncore_dependency(s) s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-bridging" add_dependency(s, "React-RCTFBReactNativeSpec") add_dependency(s, "React-performancetimeline") diff --git a/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb index ad4a5a62c3ad..3dde2a593588 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb @@ -266,7 +266,7 @@ def get_podspec_no_fabric_no_script "React-Core": [], "React-jsi": [], "React-jsiexecutor": [], - "ReactCommon/turbomodule/bridging": [], + "React-bridging": [], "ReactCommon/turbomodule/core": [], "hermes-engine": [], "React-NativeModulesApple": [], diff --git a/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb b/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb index b062c6e3f243..ba95cfc3331f 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb @@ -152,7 +152,7 @@ def test_installModulesDependencies_whenNewArchEnabledAndNewArchAndNoSearchPaths { :dependency_name => "ReactCodegen" }, { :dependency_name => "RCTRequired" }, { :dependency_name => "RCTTypeSafety" }, - { :dependency_name => "ReactCommon/turbomodule/bridging" }, + { :dependency_name => "React-bridging" }, { :dependency_name => "ReactCommon/turbomodule/core" }, { :dependency_name => "React-NativeModulesApple" }, { :dependency_name => "Yoga" }, @@ -203,7 +203,7 @@ def test_installModulesDependencies_whenNewArchDisabledAndSearchPathsAndCompiler { :dependency_name => "ReactCodegen" }, { :dependency_name => "RCTRequired" }, { :dependency_name => "RCTTypeSafety" }, - { :dependency_name => "ReactCommon/turbomodule/bridging" }, + { :dependency_name => "React-bridging" }, { :dependency_name => "ReactCommon/turbomodule/core" }, { :dependency_name => "React-NativeModulesApple" }, { :dependency_name => "Yoga" }, diff --git a/packages/react-native/scripts/cocoapods/new_architecture.rb b/packages/react-native/scripts/cocoapods/new_architecture.rb index 3873632a7d7b..e4353427a977 100644 --- a/packages/react-native/scripts/cocoapods/new_architecture.rb +++ b/packages/react-native/scripts/cocoapods/new_architecture.rb @@ -119,7 +119,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version = He spec.dependency "RCTRequired" spec.dependency "RCTTypeSafety" - spec.dependency "ReactCommon/turbomodule/bridging" + spec.dependency "React-bridging" spec.dependency "ReactCommon/turbomodule/core" spec.dependency "React-NativeModulesApple" spec.dependency "Yoga" diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 0e388d6a7ef5..43d71ac01ba9 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -333,6 +333,7 @@ def self.update_search_paths(installer) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon-Samples", "ReactCommon_Samples", ["platform/ios"])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-Fabric", "React_Fabric", ["react/renderer/components/view/platform/cxx"], false)) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-NativeModulesApple", "React_NativeModulesApple", [])) + .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-bridging", "React_bridging", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-graphics", "React_graphics", ["react/renderer/graphics/platform/ios"])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-featureflags", "React_featureflags", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-renderercss", "React_renderercss", [])) diff --git a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap index 40e82c15eff7..9ce1c901040e 100644 --- a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap +++ b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap @@ -465,7 +465,7 @@ Pod::Spec.new do |s| s.dependency \\"RCTTypeSafety\\" s.dependency \\"React-Core\\" s.dependency \\"React-jsi\\" - s.dependency \\"ReactCommon/turbomodule/bridging\\" + s.dependency \\"React-bridging\\" s.dependency \\"ReactCommon/turbomodule/core\\" s.dependency \\"React-NativeModulesApple\\" s.dependency 'React-graphics' @@ -948,7 +948,7 @@ Pod::Spec.new do |s| s.dependency \\"RCTTypeSafety\\" s.dependency \\"React-Core\\" s.dependency \\"React-jsi\\" - s.dependency \\"ReactCommon/turbomodule/bridging\\" + s.dependency \\"React-bridging\\" s.dependency \\"ReactCommon/turbomodule/core\\" s.dependency \\"React-NativeModulesApple\\" s.dependency 'React-graphics' diff --git a/packages/react-native/scripts/codegen/templates/ReactCodegen.podspec.template b/packages/react-native/scripts/codegen/templates/ReactCodegen.podspec.template index de434d4b91d8..5909a466ee82 100644 --- a/packages/react-native/scripts/codegen/templates/ReactCodegen.podspec.template +++ b/packages/react-native/scripts/codegen/templates/ReactCodegen.podspec.template @@ -69,7 +69,7 @@ Pod::Spec.new do |s| s.dependency "RCTTypeSafety" s.dependency "React-Core" s.dependency "React-jsi" - s.dependency "ReactCommon/turbomodule/bridging" + s.dependency "React-bridging" s.dependency "ReactCommon/turbomodule/core" s.dependency "React-NativeModulesApple" s.dependency 'React-graphics' diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 0ac0bcc78e04..78ddc33661b9 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -162,6 +162,7 @@ def use_react_native! ( pod 'React-domnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/dom" pod 'React-defaultsnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/defaults" pod 'React-Mapbuffer', :path => "#{prefix}/ReactCommon" + pod 'React-bridging', :path => "#{prefix}/ReactCommon/react/bridging", :modular_headers => true pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler" pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation" pod 'React-RCTFBReactNativeSpec', :path => "#{prefix}/React" From f5dc3991a275eb809dd89873c084bdbb19352b0f Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 3 Jul 2026 11:03:44 -0700 Subject: [PATCH 10/12] Move defaults for `DevSupportManagerFactory.create()` into the interface signature (#57429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/57429 The optional parameters on `DevSupportManagerFactory.create()` (the New Architecture overload) all have canonical defaults, but callers were forced to pass every one explicitly. Move the defaults into the interface signature so callers only specify the parameters that vary — `applicationContext`, `reactInstanceManagerHelper`, `packagerPathForJSBundleName`, and `useDevSupport`. This also lets external factories inject a `customPackagerCommandHandlers` map without having to repeat the eleven other arguments. `ReactHostImpl` is updated to use the shorter form. JVM signatures are unchanged (Kotlin default values on abstract interface methods don't emit bridge methods), so no ABI change for Java callers. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D110599799 fbshipit-source-id: 02d4b886f3131b47ed830a7e603cb60cbdd96b02 --- .../ReactAndroid/api/ReactAndroid.api | 7 +++++++ .../devsupport/DevSupportManagerFactory.kt | 20 +++++++++---------- .../facebook/react/runtime/ReactHostImpl.kt | 11 +++------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 53e1ca1c2631..57ed2ec2aa73 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1950,6 +1950,11 @@ public final class com/facebook/react/devsupport/DevSupportManagerBase$Companion public abstract interface class com/facebook/react/devsupport/DevSupportManagerFactory { public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;Z)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; + public static synthetic fun create$default (Lcom/facebook/react/devsupport/DevSupportManagerFactory;Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;ZILjava/lang/Object;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; +} + +public final class com/facebook/react/devsupport/DevSupportManagerFactory$DefaultImpls { + public static synthetic fun create$default (Lcom/facebook/react/devsupport/DevSupportManagerFactory;Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;ZILjava/lang/Object;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; } public final class com/facebook/react/devsupport/DoubleTapReloadRecognizer { @@ -3017,6 +3022,8 @@ public final class com/facebook/react/runtime/ReactHostImpl : com/facebook/react public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;)V public synthetic fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZZ)V + public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;)V + public synthetic fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun addBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V public fun addReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V public fun createSurface (Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Lcom/facebook/react/interfaces/fabric/ReactSurface; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt index 7210d90a4c87..405ae476deda 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt @@ -48,15 +48,15 @@ public interface DevSupportManagerFactory { public fun create( applicationContext: Context, reactInstanceManagerHelper: ReactInstanceDevHelper, - packagerPathForJSBundleName: String?, - enableOnCreate: Boolean, - redBoxHandler: RedBoxHandler?, - devBundleDownloadListener: DevBundleDownloadListener?, - minNumShakes: Int, - customPackagerCommandHandlers: Map?, - surfaceDelegateFactory: SurfaceDelegateFactory?, - devLoadingViewManager: DevLoadingViewManager?, - pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?, - useDevSupport: Boolean, + packagerPathForJSBundleName: String? = null, + enableOnCreate: Boolean = true, + redBoxHandler: RedBoxHandler? = null, + devBundleDownloadListener: DevBundleDownloadListener? = null, + minNumShakes: Int = 2, + customPackagerCommandHandlers: Map? = null, + surfaceDelegateFactory: SurfaceDelegateFactory? = null, + devLoadingViewManager: DevLoadingViewManager? = null, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? = null, + useDevSupport: Boolean = true, ): DevSupportManager } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 66fa21ae9df3..56ffb4092686 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -113,14 +113,6 @@ public class ReactHostImpl( applicationContext = context.applicationContext, reactInstanceManagerHelper = reactHostImplDevHelper, packagerPathForJSBundleName = reactHostDelegate.jsMainModulePath, - enableOnCreate = true, - redBoxHandler = null, - devBundleDownloadListener = null, - minNumShakes = 2, - customPackagerCommandHandlers = null, - surfaceDelegateFactory = null, - devLoadingViewManager = null, - pausedInDebuggerOverlayManager = null, useDevSupport = useDevSupport, ) .also { devSupportManager -> @@ -156,12 +148,14 @@ public class ReactHostImpl( @Volatile private var hostInvalidated = false + @JvmOverloads public constructor( context: Context, delegate: ReactHostDelegate, componentFactory: ComponentFactory, allowPackagerServerAccess: Boolean, useDevSupport: Boolean, + devSupportManagerFactory: DevSupportManagerFactory? = null, ) : this( context, delegate, @@ -170,6 +164,7 @@ public class ReactHostImpl( Task.UI_THREAD_EXECUTOR, allowPackagerServerAccess, useDevSupport, + devSupportManagerFactory, ) public override val lifecycleState: LifecycleState From 23608d6802acb5c13f5b739e8c034c97cfbfb1d3 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Fri, 3 Jul 2026 12:19:37 -0700 Subject: [PATCH 11/12] Cleanup unused deprecated syntax enforcement opt-out for react-native (#57428) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57428 Changelog: [Internal] Reviewed By: gkz Differential Revision: D110600695 fbshipit-source-id: 4b66eaa667fe01e057e8dba5781b52d2346aa46f --- .flowconfig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.flowconfig b/.flowconfig index 58ca3c06ee61..4d01ae0c0e40 100644 --- a/.flowconfig +++ b/.flowconfig @@ -64,11 +64,6 @@ module.system.haste.module_ref_prefix=m# react.runtime=automatic -experimental.deprecated_utilities.excludes=/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js -experimental.deprecated_utilities.excludes=/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js -experimental.deprecated_colon_extends.excludes=/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js -experimental.deprecated_variance_sigils.excludes=/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js - ban_spread_key_props=true [lints] From e04ff69ab37add0661b3e0a66f0c05917e0f2b8b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 3 Jul 2026 12:20:15 -0700 Subject: [PATCH 12/12] Add missing textAlignVertical on TextInputAndroidProps (Flow) (#57427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/57427 **Motivation** [`textAlignVertical`](https://reactnative.dev/docs/text-style-props#textalignvertical-android) is a real, documented prop which exists on the manual TS types but was not defined in Flow. The prop reaches `` at runtime via a spread operation: https://www.internalfb.com/code/fbsource/[35bb10d088a0]/xplat/js/react-native-github/packages/react-native/Libraries/Components/TextInput/TextInput.js?lines=653-656 **This change** Type missing prop, exposing this API to Flow and the generated Strict API types — annotated with `platform android`. **Sidenote: Should this prop exist?** `` also respects `style: {verticalAlign: 'auto' | 'bottom' | 'middle' | 'top'}`, mapping this to `textAlignVertical` on Android — **they are equivalent**. Therefore, it's tempting to drop `textAlignVertical` entirely, however neither it nor `style.verticalAlign` are implemented on iOS — therefore the explicit prop with `platform android` edges out. This diff upholds the current state of the world. Changelog: [General][Fixed] - **Strict TypeScript API**: Add missing `textAlignVertical` prop on `` Reviewed By: cortinico Differential Revision: D106183984 fbshipit-source-id: 597f8815e41dc959aa44f6fbb4fa8b6a588c2e8b --- .../Libraries/Components/TextInput/TextInput.flow.js | 7 +++++++ packages/react-native/ReactNativeApi.d.ts | 9 +++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index a38c30aa751b..cb3b241f8089 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -506,6 +506,13 @@ export type TextInputAndroidProps = Readonly<{ */ textBreakStrategy?: ?('simple' | 'highQuality' | 'balanced'), + /** + * Align the input text to the top, center, or bottom of the field. + * Defaults to `'auto'`. + * @platform android + */ + textAlignVertical?: ?('auto' | 'top' | 'bottom' | 'center'), + /** * The color of the `TextInput` underline. * @platform android diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 293ae885d55d..c113c8779ced 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<67eb674c33870c82986054f0cee48385>> + * @generated SignedSource<<82e308c1a4f2f713285c7586ff3eb618>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5051,6 +5051,7 @@ declare type TextInputAndroidProps = { readonly rows?: number readonly selectionHandleColor?: ColorValue readonly showSoftInputOnFocus?: boolean + readonly textAlignVertical?: "auto" | "bottom" | "center" | "top" readonly textBreakStrategy?: "balanced" | "highQuality" | "simple" readonly underlineColorAndroid?: ColorValue } @@ -6135,8 +6136,8 @@ export { TaskProvider, // 266dedf2 Text, // f792e51d TextContentType, // 239b3ecc - TextInput, // 1c32d882 - TextInputAndroidProps, // 3f09ce49 + TextInput, // 4d0a088b + TextInputAndroidProps, // 7109938a TextInputBlurEvent, // b77af40e TextInputChangeEvent, // f55eef98 TextInputContentSizeChangeEvent, // a27cd32a @@ -6145,7 +6146,7 @@ export { TextInputIOSProps, // 0d05a855 TextInputInstance, // 5a0c0e0d TextInputKeyPressEvent, // 546c5d07 - TextInputProps, // 08c36ff7 + TextInputProps, // a56c62a0 TextInputSelectionChangeEvent, // e58f2abc TextInputSubmitEditingEvent, // 6bcb2aa5 TextInstance, // 05463a96