MapBuffer interface and JVM -> C++ conversion

Summary:
Creates a `WritableMapBuffer` abstraction to pass data from JVM to C++, similarly to `ReadableMapBuffer`. This part also defines a Kotlin interface for both `Readable/WritableMapBuffer` to allow to use them interchangeably on Java side.

`WritableMapBuffer` is using Android's `SparseArray` which has almost identical structure to `MapBuffer`, with `log(N)` random access and instant sequential access.

To avoid paying the cost of JNI transfer, the data is only transferred when requested by native `JWritableMapBuffer::getMapBuffer`. `WritableMapBuffer` also owns it data, meaning it cannot be "consumed" as `WritableNativeMap`, with C++ usually receiving copy of the data on conversion. This allows to use `WritableMapBuffer` as JVM-only implementation of `MapBuffer` interface as well, e.g. for testing (although Robolectric will still be required due to `SparseArray` used as storage)

Changelog: [Android][Added] - MapBuffer implementation for JVM -> C++ communication

Reviewed By: mdvacca

Differential Revision: D35014011

fbshipit-source-id: 8430212bf6152b966cde8e6f483b4f2dab369e4e
This commit is contained in:
Andrei Shikov 2022-03-30 20:27:23 -07:00 коммит произвёл Facebook GitHub Bot
Родитель 8adedfeb15
Коммит cf6f3b680b
10 изменённых файлов: 446 добавлений и 5 удалений

Просмотреть файл

@ -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",

Просмотреть файл

@ -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 <react/renderer/mapbuffer/MapBuffer.h> 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<MapBuffer.Entry> {
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
* `<react/renderer/mapbuffer/MapBuffer.h>`, 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
}
}

Просмотреть файл

@ -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<Any> = SparseArray<Any>()
/*
* 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 <reified T> 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<MapBuffer.Entry> =
object : Iterator<MapBuffer.Entry> {
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<Any> = Array(values.size()) { values.valueAt(it) }
companion object {
init {
ReadableMapBufferSoLoader.staticInit()
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
---
InheritParentConfig: true
...

Просмотреть файл

@ -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 <react/renderer/mapbuffer/MapBufferBuilder.h>
namespace facebook::react {
MapBuffer JWritableMapBuffer::getMapBuffer() {
static const auto getKeys =
javaClassStatic()->getMethod<jni::JArrayInt()>("getKeys");
static const auto getValues =
javaClassStatic()->getMethod<jni::JArrayClass<jni::JObject>()>(
"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<jni::JObject> 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<jni::JBoolean>(value);
builder.putBool(key, element->value());
} else if (value->isInstanceOf(integerClass)) {
auto element = jni::static_ref_cast<jni::JInteger>(value);
builder.putInt(key, element->value());
} else if (value->isInstanceOf(doubleClass)) {
auto element = jni::static_ref_cast<jni::JDouble>(value);
builder.putDouble(key, element->value());
} else if (value->isInstanceOf(stringClass)) {
auto element = jni::static_ref_cast<jni::JString>(value);
builder.putString(key, element->toStdString());
} else if (value->isInstanceOf(readableMapClass)) {
auto element =
jni::static_ref_cast<ReadableMapBuffer::jhybridobject>(value);
builder.putMapBuffer(key, MapBuffer(element->cthis()->data()));
} else if (value->isInstanceOf(writableMapClass)) {
auto element =
jni::static_ref_cast<JWritableMapBuffer::javaobject>(value);
builder.putMapBuffer(key, element->getMapBuffer());
}
}
return builder.build();
}
} // namespace facebook::react

Просмотреть файл

@ -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 <fbjni/fbjni.h>
#include <react/common/mapbuffer/ReadableMapBuffer.h>
namespace facebook::react {
class JWritableMapBuffer : public jni::JavaClass<JWritableMapBuffer> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/facebook/react/common/mapbuffer/WritableMapBuffer;";
MapBuffer getMapBuffer();
};
} // namespace facebook::react

Просмотреть файл

@ -7,6 +7,7 @@
#include <fbjni/fbjni.h>
#include "JWritableMapBuffer.h"
#include "ReadableMapBuffer.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {

Просмотреть файл

@ -33,6 +33,10 @@ jni::local_ref<jni::JByteBuffer> ReadableMapBuffer::importByteBuffer() {
serializedData_.data(), serializedData_.size());
}
std::vector<uint8_t> ReadableMapBuffer::data() const {
return serializedData_;
}
jni::local_ref<ReadableMapBuffer::jhybridobject>
ReadableMapBuffer::createWithContents(MapBuffer &&map) {
return newObjectCxxArgs(std::move(map));

Просмотреть файл

@ -30,6 +30,8 @@ class ReadableMapBuffer : public jni::HybridClass<ReadableMapBuffer> {
jni::local_ref<jni::JByteBuffer> importByteBuffer();
std::vector<uint8_t> data() const;
private:
std::vector<uint8_t> serializedData_;
};

Просмотреть файл

@ -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: