diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK index 8427bb186ba..422a55dd530 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK @@ -4,6 +4,7 @@ rn_android_library( name = "mapbuffer", srcs = glob([ "*.java", + "*.kt", ]), autoglob = False, is_androidx = True, @@ -11,7 +12,9 @@ rn_android_library( "pfh:ReactNative_CommonInfrastructurePlaceholde", "supermodule:xplat/default/public.react_native.infra", ], + language = "KOTLIN", provided_deps = [], + pure_kotlin = False, required_for_source_only_abi = True, visibility = [ "PUBLIC", diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt new file mode 100644 index 00000000000..545f27d95ea --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt @@ -0,0 +1,170 @@ +/* + * 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.common.mapbuffer + +/** + * MapBuffer is an optimized sparse array format for transferring props-like data between C++ and + * JNI. It is designed to: + * - be compact to optimize space when sparse (sparse is the common case). + * - be accessible through JNI with zero/minimal copying. + * - work recursively for nested maps/arrays. + * - support dynamic types that map to JSON. + * - have minimal APK size and build time impact. + * + * See for more information and native implementation. + * + * Limitations: + * - Keys are usually sized as 2 bytes, with each buffer supporting up to 65536 entries as a result. + * - O(log(N)) random key access for native buffers due to selected structure. Faster access can be + * achieved by retrieving [MapBuffer.Entry] with [entryAt] on known offsets. + */ +interface MapBuffer : Iterable { + companion object { + /** + * Key are represented as 2 byte values, and typed as Int for ease of access. The serialization + * format only allows to store [UShort] values. + */ + internal val KEY_RANGE = IntRange(UShort.MIN_VALUE.toInt(), UShort.MAX_VALUE.toInt()) + } + + /** + * Data types supported by [MapBuffer]. Keep in sync with definition in + * ``, as enum serialization relies on correct order. + */ + enum class DataType { + BOOL, + INT, + DOUBLE, + STRING, + MAP + } + + /** + * Number of elements inserted into current MapBuffer. + * @return number of elements in the [MapBuffer] + */ + val count: Int + + /** + * Checks whether entry for given key exists in MapBuffer. + * @param key key to lookup the entry + * @return whether entry for the given key exists in the MapBuffer. + */ + fun contains(key: Int): Boolean + + /** + * Provides offset of the key to use for [entryAt], for cases when offset is not statically known + * but can be cached. + * @param key key to lookup offset for + * @return offset for the given key to be used for entry access, -1 if key wasn't found. + */ + fun getKeyOffset(key: Int): Int + + /** + * Provides parsed access to a MapBuffer without additional lookups for provided offset. + * @param offset offset of entry in the MapBuffer structure. Can be looked up for known keys with + * [getKeyOffset]. + * @return parsed entry for structured access for given offset + */ + fun entryAt(offset: Int): MapBuffer.Entry + + /** + * Provides parsed [DataType] annotation associated with the given key. + * @param key key to lookup type for + * @return data type of the given key. + * @throws IllegalArgumentException if the key doesn't exists + */ + fun getType(key: Int): DataType + + /** + * Provides parsed [Boolean] value if the entry for given key exists with [DataType.BOOL] type + * @param key key to lookup [Boolean] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exists + * @throws IllegalStateException if the data type doesn't match + */ + fun getBoolean(key: Int): Boolean + + /** + * Provides parsed [Int] value if the entry for given key exists with [DataType.INT] type + * @param key key to lookup [Int] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exists + * @throws IllegalStateException if the data type doesn't match + */ + fun getInt(key: Int): Int + + /** + * Provides parsed [Double] value if the entry for given key exists with [DataType.DOUBLE] type + * @param key key to lookup [Double] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exists + * @throws IllegalStateException if the data type doesn't match + */ + fun getDouble(key: Int): Double + + /** + * Provides parsed [String] value if the entry for given key exists with [DataType.STRING] type + * @param key key to lookup [String] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exists + * @throws IllegalStateException if the data type doesn't match + */ + fun getString(key: Int): String + + /** + * Provides parsed [MapBuffer] value if the entry for given key exists with [DataType.MAP] type + * @param key key to lookup [MapBuffer] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exists + * @throws IllegalStateException if the data type doesn't match + */ + fun getMapBuffer(key: Int): MapBuffer + + /** Iterable entry representing parsed MapBuffer values */ + interface Entry { + /** + * Key of the given entry. Usually represented as 2 byte unsigned integer with the value range + * of [0,65536) + */ + val key: Int + + /** Parsed [DataType] of the entry */ + val type: DataType + + /** + * Entry value represented as [Boolean] + * @throws IllegalStateException if the data type doesn't match [DataType.BOOL] + */ + val booleanValue: Boolean + + /** + * Entry value represented as [Int] + * @throws IllegalStateException if the data type doesn't match [DataType.INT] + */ + val intValue: Int + + /** + * Entry value represented as [Double] + * @throws IllegalStateException if the data type doesn't match [DataType.DOUBLE] + */ + val doubleValue: Double + + /** + * Entry value represented as [String] + * @throws IllegalStateException if the data type doesn't match [DataType.STRING] + */ + val stringValue: String + + /** + * Entry value represented as [MapBuffer] + * @throws IllegalStateException if the data type doesn't match [DataType.MAP] + */ + val mapBufferValue: MapBuffer + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt new file mode 100644 index 00000000000..81656037c3a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt @@ -0,0 +1,170 @@ +/* + * 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.common.mapbuffer + +import android.util.SparseArray +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.common.mapbuffer.MapBuffer.Companion.KEY_RANGE +import com.facebook.react.common.mapbuffer.MapBuffer.DataType +import javax.annotation.concurrent.NotThreadSafe + +/** + * Implementation of writeable Java-only MapBuffer, which can be used to send information through + * JNI. + * + * See [MapBuffer] for more details + */ +@NotThreadSafe +@DoNotStrip +class WritableMapBuffer : MapBuffer { + private val values: SparseArray = SparseArray() + + /* + * Write methods + */ + + /** + * Adds a boolean value for given key to the MapBuffer. + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: Boolean): WritableMapBuffer = putInternal(key, value) + + /** + * Adds an int value for given key to the MapBuffer. + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: Int): WritableMapBuffer = putInternal(key, value) + + /** + * Adds a double value for given key to the MapBuffer. + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: Double): WritableMapBuffer = putInternal(key, value) + + /** + * Adds a string value for given key to the MapBuffer. + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: String): WritableMapBuffer = putInternal(key, value) + + /** + * Adds a [MapBuffer] 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: MapBuffer): 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." + } + + values.put(key, value) + return this + } + + /* + * Read methods + */ + + override val count: Int + get() = values.size() + + override fun contains(key: Int): Boolean = values.get(key) != null + + override fun getKeyOffset(key: Int): Int = values.indexOfKey(key) + + override fun entryAt(offset: Int): MapBuffer.Entry = MapBufferEntry(offset) + + override fun getType(key: Int): DataType { + val value = values.get(key) + require(value != null) { "Key not found: $key" } + return value.dataType(key) + } + + override fun getBoolean(key: Int): Boolean = verifyValue(key, values.get(key)) + + override fun getInt(key: Int): Int = verifyValue(key, values.get(key)) + + override fun getDouble(key: Int): Double = verifyValue(key, values.get(key)) + + override fun getString(key: Int): String = verifyValue(key, values.get(key)) + + override fun getMapBuffer(key: Int): MapBuffer = 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" } + check(value is T) { + "Expected ${T::class.java} for key: $key, found ${value.javaClass} instead." + } + return value + } + + private fun Any.dataType(key: Int): DataType { + return when (val value = this) { + is Boolean -> DataType.BOOL + is Int -> DataType.INT + is Double -> DataType.DOUBLE + is String -> DataType.STRING + is MapBuffer -> DataType.MAP + else -> throw IllegalStateException("Key $key has value of unknown type: ${value.javaClass}") + } + } + + override fun iterator(): Iterator = + object : Iterator { + var count = 0 + override fun hasNext(): Boolean = count < values.size() + override fun next(): MapBuffer.Entry = MapBufferEntry(count++) + } + + private inner class MapBufferEntry(private val index: Int) : MapBuffer.Entry { + override val key: Int = values.keyAt(index) + override val type: DataType = values.valueAt(index).dataType(key) + override val booleanValue: Boolean + get() = verifyValue(key, values.valueAt(index)) + override val intValue: Int + get() = verifyValue(key, values.valueAt(index)) + override val doubleValue: Double + get() = verifyValue(key, values.valueAt(index)) + override val stringValue: String + get() = verifyValue(key, values.valueAt(index)) + override val mapBufferValue: MapBuffer + get() = verifyValue(key, values.valueAt(index)) + } + + /* + * JNI hooks + */ + + @DoNotStrip + @Suppress("UNUSED") + /** JNI hook for MapBuffer to retrieve sorted keys from this class. */ + private fun getKeys(): IntArray = IntArray(values.size()) { values.keyAt(it) } + + @DoNotStrip + @Suppress("UNUSED") + /** JNI hook for MapBuffer to retrieve sorted values from this class. */ + private fun getValues(): Array = Array(values.size()) { values.valueAt(it) } + + companion object { + init { + ReadableMapBufferSoLoader.staticInit() + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/.clang-tidy b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/.clang-tidy new file mode 100644 index 00000000000..796a6244e78 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/.clang-tidy @@ -0,0 +1,3 @@ +--- +InheritParentConfig: true +... diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.cpp b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.cpp new file mode 100644 index 00000000000..3ce30f15f40 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "JWritableMapBuffer.h" +#include + +namespace facebook::react { + +MapBuffer JWritableMapBuffer::getMapBuffer() { + static const auto getKeys = + javaClassStatic()->getMethod("getKeys"); + static const auto getValues = + javaClassStatic()->getMethod()>( + "getValues"); + + auto keyArray = getKeys(self()); + auto values = getValues(self()); + + auto keys = keyArray->pin(); + + MapBufferBuilder builder; + + auto size = keys.size(); + for (int i = 0; i < size; i++) { + auto key = keys[i]; + jni::local_ref value = values->getElement(i); + + static const auto booleanClass = jni::JBoolean::javaClassStatic(); + static const auto integerClass = jni::JInteger::javaClassStatic(); + static const auto doubleClass = jni::JDouble::javaClassStatic(); + static const auto stringClass = jni::JString::javaClassStatic(); + static const auto readableMapClass = ReadableMapBuffer::javaClassStatic(); + static const auto writableMapClass = JWritableMapBuffer::javaClassStatic(); + + if (value->isInstanceOf(booleanClass)) { + auto element = jni::static_ref_cast(value); + builder.putBool(key, element->value()); + } else if (value->isInstanceOf(integerClass)) { + auto element = jni::static_ref_cast(value); + builder.putInt(key, element->value()); + } else if (value->isInstanceOf(doubleClass)) { + auto element = jni::static_ref_cast(value); + builder.putDouble(key, element->value()); + } else if (value->isInstanceOf(stringClass)) { + auto element = jni::static_ref_cast(value); + builder.putString(key, element->toStdString()); + } else if (value->isInstanceOf(readableMapClass)) { + auto element = + jni::static_ref_cast(value); + builder.putMapBuffer(key, MapBuffer(element->cthis()->data())); + } else if (value->isInstanceOf(writableMapClass)) { + auto element = + jni::static_ref_cast(value); + builder.putMapBuffer(key, element->getMapBuffer()); + } + } + + return builder.build(); +} + +} // namespace facebook::react diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.h b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.h new file mode 100644 index 00000000000..23aeae26a41 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/JWritableMapBuffer.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class JWritableMapBuffer : public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = + "Lcom/facebook/react/common/mapbuffer/WritableMapBuffer;"; + + MapBuffer getMapBuffer(); +}; + +} // namespace facebook::react diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp index 070ab435852..2eae838c186 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp @@ -7,6 +7,7 @@ #include +#include "JWritableMapBuffer.h" #include "ReadableMapBuffer.h" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp index 2a6d305e91f..92523c5d5e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp @@ -33,6 +33,10 @@ jni::local_ref ReadableMapBuffer::importByteBuffer() { serializedData_.data(), serializedData_.size()); } +std::vector ReadableMapBuffer::data() const { + return serializedData_; +} + jni::local_ref ReadableMapBuffer::createWithContents(MapBuffer &&map) { return newObjectCxxArgs(std::move(map)); diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h index 24452a71a2f..7d81cc265fe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h @@ -30,6 +30,8 @@ class ReadableMapBuffer : public jni::HybridClass { jni::local_ref importByteBuffer(); + std::vector data() const; + private: std::vector serializedData_; }; diff --git a/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index fe683058512..070d551e47d 100644 --- a/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -26,17 +26,17 @@ class ReadableMapBuffer; /** * MapBuffer is an optimized sparse array format for transferring props-like - * between C++ and other VMs. The implementation of this map is optimized to: + * objects between C++ and other VMs. The implementation of this map is optimized to: * - be compact to optimize space when sparse (sparse is the common case). * - be accessible through JNI with zero/minimal copying via ByteBuffer. - * - Have excellent C++ single-write and many-read performance by maximizing + * - have excellent C++ single-write and many-read performance by maximizing * CPU cache performance through compactness, data locality, and fixed offsets * where possible. * - be optimized for iteration and intersection against other maps, but with * reasonably good random access as well. - * - Work recursively for nested maps/arrays. - * - Supports dynamic types that map to JSON. - * - Don't require mutability - single-write on creation. + * - work recursively for nested maps/arrays. + * - support dynamic types that map to JSON. + * - don't require mutability/copy - single-write on creation and move semantics. * - have minimal APK size and build time impact. * * MapBuffer data is stored in a continuous chunk of memory (bytes_ field below) with the following layout: