-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
8adedfe
commit cf6f3b6
Showing
10 changed files
with
446 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
170 changes: 170 additions & 0 deletions
170
ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
.../src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/.clang-tidy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
--- | ||
InheritParentConfig: true | ||
... |
Oops, something went wrong.