Skip to content

Commit

Permalink
MapBuffer interface and JVM -> C++ conversion
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Andrei Shikov authored and facebook-github-bot committed Mar 31, 2022
1 parent 8adedfe commit cf6f3b6
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ rn_android_library(
name = "mapbuffer",
srcs = glob([
"*.java",
"*.kt",
]),
autoglob = False,
is_androidx = True,
labels = [
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
InheritParentConfig: true
...
Loading

0 comments on commit cf6f3b6

Please sign in to comment.