fix: replace ThreadLocal with KMP-compatible ScratchLocal expect/actual

ThreadLocal.withInitial is a JVM-only API that doesn't compile on
Kotlin/Native or iOS targets. Replace all usages in commonMain
(FieldP.kt, Point.kt) with a new ScratchLocal expect/actual:

- jvmMain/androidMain: delegates to java.lang.ThreadLocal (same behavior)
- nativeMain: holds value directly (Kotlin/Native coroutines are
  cooperative, scratch buffers don't need thread isolation)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-06 21:05:48 +00:00
parent b60a0bcae8
commit 034c1ab2d1
6 changed files with 132 additions and 3 deletions
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/** Android: delegates to java.lang.ThreadLocal for per-thread scratch buffers. */
internal actual class ScratchLocal<T> actual constructor(
initializer: () -> T,
) {
private val tl = ThreadLocal.withInitial(initializer)
actual fun get(): T = tl.get()
}
@@ -48,11 +48,11 @@ internal object FieldP {
-1L, // 0xFFFFFFFFFFFFFFFF
)
private val wide = ThreadLocal.withInitial { LongArray(8) }
private val wide = ScratchLocal { LongArray(8) }
// Pre-allocated scratch for inv/sqrt addition chains (11 field elements).
// Avoids 11 LongArray(4) allocations per inv/sqrt call.
private val chainScratch = ThreadLocal.withInitial { Array(11) { LongArray(4) } }
private val chainScratch = ScratchLocal { Array(11) { LongArray(4) } }
/** Get a thread-local wide buffer. Call once at the top-level entry point, then pass through. */
fun getWide(): LongArray = wide.get()
@@ -348,7 +348,7 @@ internal object ECPoint {
val batchZInv3 = LongArray(4)
}
private val scratch = ThreadLocal.withInitial { PointScratch() }
private val scratch = ScratchLocal { PointScratch() }
/** Get thread-local scratch. Call once at the top-level entry point. */
internal fun getScratch(): PointScratch = scratch.get()
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/**
* KMP-compatible thread-local storage.
*
* On JVM/Android: delegates to java.lang.ThreadLocal for true per-thread isolation.
* On Native: single-threaded assumption — just holds the value directly.
* (Kotlin/Native has its own threading model; for the secp256k1 use case,
* scratch buffers don't need thread isolation since coroutines are cooperative.)
*/
internal expect class ScratchLocal<T>(
initializer: () -> T,
) {
fun get(): T
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/** JVM: delegates to java.lang.ThreadLocal for per-thread scratch buffers. */
internal actual class ScratchLocal<T> actual constructor(
initializer: () -> T,
) {
private val tl = ThreadLocal.withInitial(initializer)
actual fun get(): T = tl.get()
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/**
* Native: holds value directly (no thread-local needed).
* Kotlin/Native coroutines are cooperative — scratch buffers are safe to share
* within a single thread's call stack.
*/
internal actual class ScratchLocal<T> actual constructor(
initializer: () -> T,
) {
private val value: T = initializer()
actual fun get(): T = value
}