Skip to content

Instantly share code, notes, and snippets.

@gmazzo
Last active November 26, 2022 18:41
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gmazzo/03acdb2e6b59d0394ac69d521e51fea1 to your computer and use it in GitHub Desktop.
Save gmazzo/03acdb2e6b59d0394ac69d521e51fea1 to your computer and use it in GitHub Desktop.
A Kotlin/Native thread-safe `List`, `Set` and `Map` implementations
package kotlin.native.concurrent
fun <T> AtomicReference<T>.computeAndSet(transform: T.() -> T) = with(value) {
@Suppress("ControlFlowWithEmptyBody")
while (!compareAndSet(this, transform().freeze()));
this
}
package kotlin.native.concurrent
/**
* Helper class to allow to have a [MutableList] inside a frozen native object.
* It uses an [AtomicReference] of [List] underneath and sets a new one at every mutation
*
* [Iterator]s and [subList] will return a snapshot of current data and changes won't be reflected
*/
class NativeMutableList<E> private constructor(
private val ref: AtomicReference<List<E>>
) : MutableList<E>, List<E> by ref.value {
constructor() : this(emptyList())
constructor(list: List<E>) : this(AtomicReference(list))
override fun clear() {
ref.value = emptyList()
}
override fun add(element: E): Boolean {
ref.computeAndSet { this + element }
return true
}
override fun add(index: Int, element: E) {
ref.computeAndSet { take(index) + element + subList(index, size) }
}
override fun addAll(index: Int, elements: Collection<E>): Boolean {
ref.computeAndSet { take(index) + elements + subList(index, size) }
return true
}
override fun addAll(elements: Collection<E>): Boolean {
ref.computeAndSet { this + elements }
return true
}
override fun remove(element: E) =
ref.computeAndSet { this - element }.size != size
override fun removeAll(elements: Collection<E>) =
ref.computeAndSet { this - elements }.size != size
override fun removeAt(index: Int) =
ref.computeAndSet { take(index) + subList(index + 1, size) }[index]
override fun retainAll(elements: Collection<E>) =
ref.computeAndSet { filter { it in elements } }.size != size
override fun set(index: Int, element: E) =
ref.computeAndSet { take(index) + element + subList(index + 1, size) }[index]
override fun iterator() =
toMutableList().iterator()
override fun listIterator() =
toMutableList().listIterator()
override fun listIterator(index: Int) =
toMutableList().listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int) =
ref.value.subList(fromIndex, toIndex).toMutableList()
override fun hashCode() =
ref.value.hashCode()
override fun equals(other: Any?) =
ref.value == (other as? NativeMutableList<*>)?.ref?.value
override fun toString() =
ref.value.toString()
}
package kotlin.native.concurrent
/**
* Helper class to allow to have a [MutableMap] inside a frozen native object.
* It uses an [AtomicReference] of [Map] underneath and sets a new one at every mutation
*
* [Iterator]s will return a snapshot of current data and changes won't be reflected
*/
class NativeMutableMap<K, V> private constructor(
private val ref: AtomicReference<Map<K, V>>
) : MutableMap<K, V>, Map<K, V> by ref.value {
constructor() : this(emptyMap())
constructor(map: Map<K, V>) : this(AtomicReference(map))
override fun clear() {
ref.value = emptyMap()
}
override fun put(key: K, value: V) =
ref.computeAndSet { this + (key to value) }[key]
override fun putAll(from: Map<out K, V>) {
ref.computeAndSet { this + from }
}
override fun remove(key: K): V? =
ref.computeAndSet { this - key }[key]
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = ref.value.toMutableMap().entries
override val keys
get() = ref.value.toMutableMap().keys
override val values
get() = ref.value.toMutableMap().values
override fun hashCode() =
ref.value.hashCode()
override fun equals(other: Any?) =
ref.value == (other as? NativeMutableMap<*, *>)?.ref?.value
override fun toString() =
ref.value.toString()
}
package kotlin.native.concurrent
/**
* Helper class to allow to have a [MutableMap] inside a frozen native object.
* It uses an [AtomicReference] of [Map] underneath and sets a new one at every mutation
*
* [Iterator]s will return a snapshot of current data and changes won't be reflected
*/
class NativeMutableMap<K, V> private constructor(
private val ref: AtomicReference<Map<K, V>>
) : MutableMap<K, V>, Map<K, V> by ref.value {
constructor() : this(emptyMap())
constructor(map: Map<K, V>) : this(AtomicReference(map))
override fun clear() {
ref.value = emptyMap()
}
override fun put(key: K, value: V) =
ref.computeAndSet { this + (key to value) }[key]
override fun putAll(from: Map<out K, V>) {
ref.computeAndSet { this + from }
}
override fun remove(key: K): V? =
ref.computeAndSet { this - key }[key]
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = ref.value.toMutableMap().entries
override val keys
get() = ref.value.toMutableMap().keys
override val values
get() = ref.value.toMutableMap().values
override fun hashCode() =
ref.value.hashCode()
override fun equals(other: Any?) =
ref.value == (other as? NativeMutableMap<*, *>)?.ref?.value
override fun toString() =
ref.value.toString()
}
@carlosrafp
Copy link

Hi, Tried your code in a project where I have to communicate java with a kotlin-native shared library using JNI. I encountered a concurrency access error using NativeMutableMap and NativeMutableList in a frozen (@SharedImmutable) class called by different java threads. I fixed the issue overriding the get method in both classes:

`
\ NativeMutableMap

override fun get(key: K): V?{
return ref.value[key]
}

\ NativeMutableList.
override fun get (index: Int): E {
return ref.value[index]
}
`
Thanks for the nice piece of code, by the way

@gmazzo
Copy link
Author

gmazzo commented Dec 27, 2021

Hey thanks! Can you elaborate why the override will fix the issue? That's exactly what Map<K, V> by ref.value is doing underneath

@carlosrafp
Copy link

Let me show you a sample:

On K/N side, I have a global val, defined by a class that contains a NativeMutableMap:

@SharedImmutable
val frozenVault = Vault().freeze() // global val

class Vault {
private val connectionList: NativeMutableMap<Int, Connection> =
NativeMutableMap()

fun getConnection (index: Int): Connection? {
return connectionList[index]
}
fun addConnection (index: Int, con: Connection){
connectionList[index] = con
}

}

As you can see, it is a map of index to a connection class

class Connection (login: String, password: String) {
private val name : String = login
private val pass : String = password
private val cookies: NativeMutableList = NativeMutableList()
private val userId: AtomicInt = AtomicInt(0)

fun doLogin(): Int{
….
// login -> authenticate cookie
cookies.add(cookie)
}
fun post(payload: ByteArray): ByteArray? {
val actualCookie = cookies[0]
… do post with actualCookie
}
….
}

Each Connection class has its own credentials and cookies and can perform request operations and renew cookies as needed

Different Java threads call functions of KN side that require specific Connection class. Different java threads often use/share the same Connection class, and the vault stores them.

As the vault is frozen and SharedImmutable it can be accessed in any thread.
If I don’t override ‘get’ fuction of NativeMutableMap i can’t access and use the stored Connection classes -> returns null

Also, i cant access cookies inside Connection class without overriding NativeMutableList 'get' function as I get a "kotlin.IndexOutOfBoundsException: Empty list doesn't contain element at index 0."

As vault val is frozen, everything inside it is also frozen, as expected. The IDE compiles normally without overriding the get function and doesnt make any warning, Maybe the need of overriding 'get' to fix runtime issue only occurs in java <-> JNI <-> KN communication... I'm using kotlin("multiplatform") version "1.6.0" on windows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment