Skip to content

Commit cf6f3b6

Browse files
Andrei Shikovfacebook-github-bot
Andrei Shikov
authored andcommitted
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
1 parent 8adedfe commit cf6f3b6

File tree

10 files changed

+446
-5
lines changed

10 files changed

+446
-5
lines changed

ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK

+3
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ rn_android_library(
44
name = "mapbuffer",
55
srcs = glob([
66
"*.java",
7+
"*.kt",
78
]),
89
autoglob = False,
910
is_androidx = True,
1011
labels = [
1112
"pfh:ReactNative_CommonInfrastructurePlaceholde",
1213
"supermodule:xplat/default/public.react_native.infra",
1314
],
15+
language = "KOTLIN",
1416
provided_deps = [],
17+
pure_kotlin = False,
1518
required_for_source_only_abi = True,
1619
visibility = [
1720
"PUBLIC",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.common.mapbuffer
9+
10+
/**
11+
* MapBuffer is an optimized sparse array format for transferring props-like data between C++ and
12+
* JNI. It is designed to:
13+
* - be compact to optimize space when sparse (sparse is the common case).
14+
* - be accessible through JNI with zero/minimal copying.
15+
* - work recursively for nested maps/arrays.
16+
* - support dynamic types that map to JSON.
17+
* - have minimal APK size and build time impact.
18+
*
19+
* See <react/renderer/mapbuffer/MapBuffer.h> for more information and native implementation.
20+
*
21+
* Limitations:
22+
* - Keys are usually sized as 2 bytes, with each buffer supporting up to 65536 entries as a result.
23+
* - O(log(N)) random key access for native buffers due to selected structure. Faster access can be
24+
* achieved by retrieving [MapBuffer.Entry] with [entryAt] on known offsets.
25+
*/
26+
interface MapBuffer : Iterable<MapBuffer.Entry> {
27+
companion object {
28+
/**
29+
* Key are represented as 2 byte values, and typed as Int for ease of access. The serialization
30+
* format only allows to store [UShort] values.
31+
*/
32+
internal val KEY_RANGE = IntRange(UShort.MIN_VALUE.toInt(), UShort.MAX_VALUE.toInt())
33+
}
34+
35+
/**
36+
* Data types supported by [MapBuffer]. Keep in sync with definition in
37+
* `<react/renderer/mapbuffer/MapBuffer.h>`, as enum serialization relies on correct order.
38+
*/
39+
enum class DataType {
40+
BOOL,
41+
INT,
42+
DOUBLE,
43+
STRING,
44+
MAP
45+
}
46+
47+
/**
48+
* Number of elements inserted into current MapBuffer.
49+
* @return number of elements in the [MapBuffer]
50+
*/
51+
val count: Int
52+
53+
/**
54+
* Checks whether entry for given key exists in MapBuffer.
55+
* @param key key to lookup the entry
56+
* @return whether entry for the given key exists in the MapBuffer.
57+
*/
58+
fun contains(key: Int): Boolean
59+
60+
/**
61+
* Provides offset of the key to use for [entryAt], for cases when offset is not statically known
62+
* but can be cached.
63+
* @param key key to lookup offset for
64+
* @return offset for the given key to be used for entry access, -1 if key wasn't found.
65+
*/
66+
fun getKeyOffset(key: Int): Int
67+
68+
/**
69+
* Provides parsed access to a MapBuffer without additional lookups for provided offset.
70+
* @param offset offset of entry in the MapBuffer structure. Can be looked up for known keys with
71+
* [getKeyOffset].
72+
* @return parsed entry for structured access for given offset
73+
*/
74+
fun entryAt(offset: Int): MapBuffer.Entry
75+
76+
/**
77+
* Provides parsed [DataType] annotation associated with the given key.
78+
* @param key key to lookup type for
79+
* @return data type of the given key.
80+
* @throws IllegalArgumentException if the key doesn't exists
81+
*/
82+
fun getType(key: Int): DataType
83+
84+
/**
85+
* Provides parsed [Boolean] value if the entry for given key exists with [DataType.BOOL] type
86+
* @param key key to lookup [Boolean] value for
87+
* @return value associated with the requested key
88+
* @throws IllegalArgumentException if the key doesn't exists
89+
* @throws IllegalStateException if the data type doesn't match
90+
*/
91+
fun getBoolean(key: Int): Boolean
92+
93+
/**
94+
* Provides parsed [Int] value if the entry for given key exists with [DataType.INT] type
95+
* @param key key to lookup [Int] value for
96+
* @return value associated with the requested key
97+
* @throws IllegalArgumentException if the key doesn't exists
98+
* @throws IllegalStateException if the data type doesn't match
99+
*/
100+
fun getInt(key: Int): Int
101+
102+
/**
103+
* Provides parsed [Double] value if the entry for given key exists with [DataType.DOUBLE] type
104+
* @param key key to lookup [Double] value for
105+
* @return value associated with the requested key
106+
* @throws IllegalArgumentException if the key doesn't exists
107+
* @throws IllegalStateException if the data type doesn't match
108+
*/
109+
fun getDouble(key: Int): Double
110+
111+
/**
112+
* Provides parsed [String] value if the entry for given key exists with [DataType.STRING] type
113+
* @param key key to lookup [String] value for
114+
* @return value associated with the requested key
115+
* @throws IllegalArgumentException if the key doesn't exists
116+
* @throws IllegalStateException if the data type doesn't match
117+
*/
118+
fun getString(key: Int): String
119+
120+
/**
121+
* Provides parsed [MapBuffer] value if the entry for given key exists with [DataType.MAP] type
122+
* @param key key to lookup [MapBuffer] value for
123+
* @return value associated with the requested key
124+
* @throws IllegalArgumentException if the key doesn't exists
125+
* @throws IllegalStateException if the data type doesn't match
126+
*/
127+
fun getMapBuffer(key: Int): MapBuffer
128+
129+
/** Iterable entry representing parsed MapBuffer values */
130+
interface Entry {
131+
/**
132+
* Key of the given entry. Usually represented as 2 byte unsigned integer with the value range
133+
* of [0,65536)
134+
*/
135+
val key: Int
136+
137+
/** Parsed [DataType] of the entry */
138+
val type: DataType
139+
140+
/**
141+
* Entry value represented as [Boolean]
142+
* @throws IllegalStateException if the data type doesn't match [DataType.BOOL]
143+
*/
144+
val booleanValue: Boolean
145+
146+
/**
147+
* Entry value represented as [Int]
148+
* @throws IllegalStateException if the data type doesn't match [DataType.INT]
149+
*/
150+
val intValue: Int
151+
152+
/**
153+
* Entry value represented as [Double]
154+
* @throws IllegalStateException if the data type doesn't match [DataType.DOUBLE]
155+
*/
156+
val doubleValue: Double
157+
158+
/**
159+
* Entry value represented as [String]
160+
* @throws IllegalStateException if the data type doesn't match [DataType.STRING]
161+
*/
162+
val stringValue: String
163+
164+
/**
165+
* Entry value represented as [MapBuffer]
166+
* @throws IllegalStateException if the data type doesn't match [DataType.MAP]
167+
*/
168+
val mapBufferValue: MapBuffer
169+
}
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.common.mapbuffer
9+
10+
import android.util.SparseArray
11+
import com.facebook.proguard.annotations.DoNotStrip
12+
import com.facebook.react.common.mapbuffer.MapBuffer.Companion.KEY_RANGE
13+
import com.facebook.react.common.mapbuffer.MapBuffer.DataType
14+
import javax.annotation.concurrent.NotThreadSafe
15+
16+
/**
17+
* Implementation of writeable Java-only MapBuffer, which can be used to send information through
18+
* JNI.
19+
*
20+
* See [MapBuffer] for more details
21+
*/
22+
@NotThreadSafe
23+
@DoNotStrip
24+
class WritableMapBuffer : MapBuffer {
25+
private val values: SparseArray<Any> = SparseArray<Any>()
26+
27+
/*
28+
* Write methods
29+
*/
30+
31+
/**
32+
* Adds a boolean value for given key to the MapBuffer.
33+
* @param key entry key
34+
* @param value entry value
35+
* @throws IllegalArgumentException if key is out of [UShort] range
36+
*/
37+
fun put(key: Int, value: Boolean): WritableMapBuffer = putInternal(key, value)
38+
39+
/**
40+
* Adds an int value for given key to the MapBuffer.
41+
* @param key entry key
42+
* @param value entry value
43+
* @throws IllegalArgumentException if key is out of [UShort] range
44+
*/
45+
fun put(key: Int, value: Int): WritableMapBuffer = putInternal(key, value)
46+
47+
/**
48+
* Adds a double value for given key to the MapBuffer.
49+
* @param key entry key
50+
* @param value entry value
51+
* @throws IllegalArgumentException if key is out of [UShort] range
52+
*/
53+
fun put(key: Int, value: Double): WritableMapBuffer = putInternal(key, value)
54+
55+
/**
56+
* Adds a string value for given key to the MapBuffer.
57+
* @param key entry key
58+
* @param value entry value
59+
* @throws IllegalArgumentException if key is out of [UShort] range
60+
*/
61+
fun put(key: Int, value: String): WritableMapBuffer = putInternal(key, value)
62+
63+
/**
64+
* Adds a [MapBuffer] value for given key to the current MapBuffer.
65+
* @param key entry key
66+
* @param value entry value
67+
* @throws IllegalArgumentException if key is out of [UShort] range
68+
*/
69+
fun put(key: Int, value: MapBuffer): WritableMapBuffer = putInternal(key, value)
70+
71+
private fun putInternal(key: Int, value: Any): WritableMapBuffer {
72+
require(key in KEY_RANGE) {
73+
"Only integers in [${UShort.MIN_VALUE};${UShort.MAX_VALUE}] range are allowed for keys."
74+
}
75+
76+
values.put(key, value)
77+
return this
78+
}
79+
80+
/*
81+
* Read methods
82+
*/
83+
84+
override val count: Int
85+
get() = values.size()
86+
87+
override fun contains(key: Int): Boolean = values.get(key) != null
88+
89+
override fun getKeyOffset(key: Int): Int = values.indexOfKey(key)
90+
91+
override fun entryAt(offset: Int): MapBuffer.Entry = MapBufferEntry(offset)
92+
93+
override fun getType(key: Int): DataType {
94+
val value = values.get(key)
95+
require(value != null) { "Key not found: $key" }
96+
return value.dataType(key)
97+
}
98+
99+
override fun getBoolean(key: Int): Boolean = verifyValue(key, values.get(key))
100+
101+
override fun getInt(key: Int): Int = verifyValue(key, values.get(key))
102+
103+
override fun getDouble(key: Int): Double = verifyValue(key, values.get(key))
104+
105+
override fun getString(key: Int): String = verifyValue(key, values.get(key))
106+
107+
override fun getMapBuffer(key: Int): MapBuffer = verifyValue(key, values.get(key))
108+
109+
/** Generalizes verification of the value types based on the requested type. */
110+
private inline fun <reified T> verifyValue(key: Int, value: Any?): T {
111+
require(value != null) { "Key not found: $key" }
112+
check(value is T) {
113+
"Expected ${T::class.java} for key: $key, found ${value.javaClass} instead."
114+
}
115+
return value
116+
}
117+
118+
private fun Any.dataType(key: Int): DataType {
119+
return when (val value = this) {
120+
is Boolean -> DataType.BOOL
121+
is Int -> DataType.INT
122+
is Double -> DataType.DOUBLE
123+
is String -> DataType.STRING
124+
is MapBuffer -> DataType.MAP
125+
else -> throw IllegalStateException("Key $key has value of unknown type: ${value.javaClass}")
126+
}
127+
}
128+
129+
override fun iterator(): Iterator<MapBuffer.Entry> =
130+
object : Iterator<MapBuffer.Entry> {
131+
var count = 0
132+
override fun hasNext(): Boolean = count < values.size()
133+
override fun next(): MapBuffer.Entry = MapBufferEntry(count++)
134+
}
135+
136+
private inner class MapBufferEntry(private val index: Int) : MapBuffer.Entry {
137+
override val key: Int = values.keyAt(index)
138+
override val type: DataType = values.valueAt(index).dataType(key)
139+
override val booleanValue: Boolean
140+
get() = verifyValue(key, values.valueAt(index))
141+
override val intValue: Int
142+
get() = verifyValue(key, values.valueAt(index))
143+
override val doubleValue: Double
144+
get() = verifyValue(key, values.valueAt(index))
145+
override val stringValue: String
146+
get() = verifyValue(key, values.valueAt(index))
147+
override val mapBufferValue: MapBuffer
148+
get() = verifyValue(key, values.valueAt(index))
149+
}
150+
151+
/*
152+
* JNI hooks
153+
*/
154+
155+
@DoNotStrip
156+
@Suppress("UNUSED")
157+
/** JNI hook for MapBuffer to retrieve sorted keys from this class. */
158+
private fun getKeys(): IntArray = IntArray(values.size()) { values.keyAt(it) }
159+
160+
@DoNotStrip
161+
@Suppress("UNUSED")
162+
/** JNI hook for MapBuffer to retrieve sorted values from this class. */
163+
private fun getValues(): Array<Any> = Array(values.size()) { values.valueAt(it) }
164+
165+
companion object {
166+
init {
167+
ReadableMapBufferSoLoader.staticInit()
168+
}
169+
}
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
InheritParentConfig: true
3+
...

0 commit comments

Comments
 (0)