Skip to content

Commit 56300c3

Browse files
committed
Use Handle for passing foreign objects to Rust
Consolidated the various handle maps into a single implementation for each language. This handle map works basically the same as all the others, but it's API is based on the `HandleAlloc` trait. Handles have a couple properties: * All foreign handles are odd, which allows us to distinguish between Rust and foreign handles. * For handles store a map ID that can detect when a handle is used with the wrong map. Made all languages always use the handle maps for passing objects. No more trying to leak pointers from to foreign objects. Started updating the ForeignExecutor code to use handles, but this is still a WIP while the ForeignExecutor type is in it's limbo state.
1 parent 8de9af0 commit 56300c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+375
-521
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
- The `rust_future_continuation_callback_set` FFI function was removed. `rust_future_poll` now
2020
inputs the callback pointer. External bindings authors will need to update their code.
2121
- The object handle FFI has changed. External bindings generators will need to update their code to
22-
use the new handle system.
22+
use the new handle system:
23+
* A single `FfiType::Handle` is used for all object handles.
24+
* `FfiType::Handle` is always a 64-bit int.
25+
* Foreign handles must always set the lowest bit of that int.
2326

2427
## v0.25.0 (backend crates: v0.25.0) - (_2023-10-18_)
2528

fixtures/foreign-executor/tests/bindings/test_foreign_executor.kts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,3 @@ runBlocking {
3737
assert(result.delayMs <= 200U)
3838
tester.close()
3939
}
40-
41-
// Test that we cleanup when dropping a ForeignExecutor handles
42-
assert(FfiConverterForeignExecutor.handleCount() == 0)
43-
val tester = ForeignExecutorTester(coroutineScope)
44-
val tester2 = ForeignExecutorTester.newFromSequence(listOf(coroutineScope))
45-
tester.close()
46-
tester2.close()
47-
assert(FfiConverterForeignExecutor.handleCount() == 0)

uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,10 @@ impl KotlinCodeOracle {
298298
}
299299
FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(),
300300
FfiType::ForeignCallback => "ForeignCallback".to_string(),
301-
FfiType::ForeignExecutorHandle => "USize".to_string(),
302301
FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback".to_string(),
303302
FfiType::RustFutureContinuationCallback => {
304303
"UniFffiRustFutureContinuationCallbackType".to_string()
305304
}
306-
FfiType::RustFutureContinuationData => "USize".to_string(),
307305
}
308306
}
309307

uniffi_bindgen/src/bindings/kotlin/templates/Async.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
internal const val UNIFFI_RUST_FUTURE_POLL_READY = 0.toShort()
44
internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toShort()
55

6-
internal val uniffiContinuationHandleMap = UniFfiHandleMap<CancellableContinuation<Short>>()
6+
internal val uniffiContinuationHandleMap = UniffiHandleMap<CancellableContinuation<Short>>()
77

88
// FFI type for Rust future continuations
99
internal object uniffiRustFutureContinuationCallback: UniFffiRustFutureContinuationCallbackType {
10-
override fun callback(continuationHandle: USize, pollResult: Short) {
11-
uniffiContinuationHandleMap.remove(continuationHandle)?.resume(pollResult)
10+
override fun callback(continuationHandle: UniffiHandle, pollResult: Short) {
11+
uniffiContinuationHandleMap.consumeHandle(continuationHandle).resume(pollResult)
1212
}
1313
}
1414

1515
internal suspend fun<T, F, E: Exception> uniffiRustCallAsync(
1616
rustFuture: UniffiHandle,
17-
pollFunc: (UniffiHandle, UniFffiRustFutureContinuationCallbackType, USize) -> Unit,
17+
pollFunc: (UniffiHandle, UniFffiRustFutureContinuationCallbackType, UniffiHandle) -> Unit,
1818
completeFunc: (UniffiHandle, RustCallStatus) -> F,
1919
freeFunc: (UniffiHandle) -> Unit,
2020
liftFunc: (F) -> T,
@@ -26,7 +26,7 @@ internal suspend fun<T, F, E: Exception> uniffiRustCallAsync(
2626
pollFunc(
2727
rustFuture,
2828
uniffiRustFutureContinuationCallback,
29-
uniffiContinuationHandleMap.insert(continuation)
29+
uniffiContinuationHandleMap.newHandle(continuation)
3030
)
3131
}
3232
} while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY);

uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
// Implement the foreign callback handler for {{ interface_name }}
44
internal class {{ callback_handler_class }} : ForeignCallback {
55
@Suppress("TooGenericExceptionCaught")
6-
override fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int {
6+
override fun invoke(handle: UniffiHandle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int {
77
val cb = {{ ffi_converter_name }}.handleMap.get(handle)
88
return when (method) {
99
IDX_CALLBACK_FREE -> {
10-
{{ ffi_converter_name }}.handleMap.remove(handle)
10+
{{ ffi_converter_name }}.handleMap.consumeHandle(handle)
1111

1212
// Successful return
1313
// See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs`

uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,8 @@
11
{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }}
22
{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }}
3-
{{- self.add_import("kotlin.concurrent.withLock") }}
4-
5-
internal typealias Handle = Long
6-
internal class ConcurrentHandleMap<T>(
7-
private val leftMap: MutableMap<Handle, T> = mutableMapOf(),
8-
) {
9-
private val lock = java.util.concurrent.locks.ReentrantLock()
10-
private val currentHandle = AtomicLong(0L)
11-
private val stride = 1L
12-
13-
fun insert(obj: T): Handle =
14-
lock.withLock {
15-
currentHandle.getAndAdd(stride)
16-
.also { handle ->
17-
leftMap[handle] = obj
18-
}
19-
}
20-
21-
fun get(handle: Handle) = lock.withLock {
22-
leftMap[handle] ?: throw InternalException("No callback in handlemap; this is a Uniffi bug")
23-
}
24-
25-
fun delete(handle: Handle) {
26-
this.remove(handle)
27-
}
28-
29-
fun remove(handle: Handle): T? =
30-
lock.withLock {
31-
leftMap.remove(handle)
32-
}
33-
}
343

354
interface ForeignCallback : com.sun.jna.Callback {
36-
public fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int
5+
public fun invoke(handle: UniffiHandle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int
376
}
387

398
// Magic number for the Rust proxy to call using the same mechanism as every other method,
@@ -44,20 +13,16 @@ internal const val UNIFFI_CALLBACK_SUCCESS = 0
4413
internal const val UNIFFI_CALLBACK_ERROR = 1
4514
internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2
4615

47-
public abstract class FfiConverterCallbackInterface<CallbackInterface>: FfiConverter<CallbackInterface, Handle> {
48-
internal val handleMap = ConcurrentHandleMap<CallbackInterface>()
49-
50-
internal fun drop(handle: Handle) {
51-
handleMap.remove(handle)
52-
}
16+
public abstract class FfiConverterCallbackInterface<CallbackInterface>: FfiConverter<CallbackInterface, UniffiHandle> {
17+
internal val handleMap = UniffiHandleMap<CallbackInterface>()
5318

54-
override fun lift(value: Handle): CallbackInterface {
19+
override fun lift(value: UniffiHandle): CallbackInterface {
5520
return handleMap.get(value)
5621
}
5722

5823
override fun read(buf: ByteBuffer) = lift(buf.getLong())
5924

60-
override fun lower(value: CallbackInterface) = handleMap.insert(value)
25+
override fun lower(value: CallbackInterface) = handleMap.newHandle(value)
6126

6227
override fun allocationSize(value: CallbackInterface) = 8
6328

uniffi_bindgen/src/bindings/kotlin/templates/ForeignExecutorTemplate.kt

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal interface UniFfiRustTaskCallback : com.sun.jna.Callback {
1616
}
1717

1818
internal object UniFfiForeignExecutorCallback : com.sun.jna.Callback {
19-
fun callback(handle: USize, delayMs: Int, rustTask: UniFfiRustTaskCallback?, rustTaskData: Pointer?) : Byte {
19+
fun callback(handle: UniffiHandle, delayMs: Int, rustTask: UniFfiRustTaskCallback?, rustTaskData: Pointer?) : Byte {
2020
if (rustTask == null) {
2121
FfiConverterForeignExecutor.drop(handle)
2222
return UNIFFI_FOREIGN_EXECUTOR_CALLBACK_SUCCESS
@@ -42,11 +42,11 @@ internal object UniFfiForeignExecutorCallback : com.sun.jna.Callback {
4242
}
4343
}
4444

45-
public object FfiConverterForeignExecutor: FfiConverter<CoroutineScope, USize> {
46-
internal val handleMap = UniFfiHandleMap<CoroutineScope>()
45+
public object FfiConverterForeignExecutor: FfiConverter<CoroutineScope, UniffiHandle> {
46+
internal val handleMap = UniffiHandleMap<CoroutineScope>()
4747

48-
internal fun drop(handle: USize) {
49-
handleMap.remove(handle)
48+
internal fun drop(handle: UniffiHandle) {
49+
handleMap.consumeHandle(handle)
5050
}
5151

5252
internal fun register(lib: _UniFFILib) {
@@ -58,26 +58,21 @@ public object FfiConverterForeignExecutor: FfiConverter<CoroutineScope, USize> {
5858
{% endmatch %}
5959
}
6060
61-
// Number of live handles, exposed so we can test the memory management
62-
public fun handleCount() : Int {
63-
return handleMap.size
64-
}
65-
66-
override fun allocationSize(value: CoroutineScope) = USize.size
61+
override fun allocationSize(value: CoroutineScope) = 8
6762
68-
override fun lift(value: USize): CoroutineScope {
69-
return handleMap.get(value) ?: throw RuntimeException("unknown handle in FfiConverterForeignExecutor.lift")
63+
override fun lift(value: UniffiHandle): CoroutineScope {
64+
return handleMap.get(value)
7065
}
7166
7267
override fun read(buf: ByteBuffer): CoroutineScope {
73-
return lift(USize.readFromBuffer(buf))
68+
return lift(buf.getLong())
7469
}
7570
76-
override fun lower(value: CoroutineScope): USize {
77-
return handleMap.insert(value)
71+
override fun lower(value: CoroutineScope): UniffiHandle {
72+
return handleMap.newHandle(value)
7873
}
7974
8075
override fun write(value: CoroutineScope, buf: ByteBuffer) {
81-
lower(value).writeToBuffer(buf)
76+
buf.putLong(lower(value))
8277
}
8378
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
internal class UniffiHandleMap<T> {
2+
private val lock = ReentrantReadWriteLock()
3+
private var mapId: Long = UniffiHandleMap.nextMapId()
4+
private val map: MutableMap<Long, T> = mutableMapOf()
5+
// Note: Foreign handles are always odd
6+
private var keyCounter = 1L
7+
8+
private fun nextKey(): Long = keyCounter.also {
9+
keyCounter = (keyCounter + 2L).and(0xFFFF_FFFF_FFFFL)
10+
}
11+
12+
private fun makeHandle(key: Long): UniffiHandle = key.or(mapId)
13+
14+
private fun key(handle: UniffiHandle): Long {
15+
if (handle.and(0x7FFF_0000_0000_0000L) != mapId) {
16+
throw InternalException("Handle map ID mismatch")
17+
}
18+
return handle.and(0xFFFF_FFFF_FFFFL)
19+
}
20+
21+
fun newHandle(obj: T): UniffiHandle = lock.writeLock().withLock {
22+
val key = nextKey()
23+
map[key] = obj
24+
makeHandle(key)
25+
}
26+
27+
fun get(handle: UniffiHandle) = lock.readLock().withLock {
28+
map[key(handle)] ?: throw InternalException("Missing key in handlemap: was the handle used after being freed?")
29+
}
30+
31+
fun cloneHandle(handle: UniffiHandle): UniffiHandle = lock.writeLock().withLock {
32+
val obj = map[key(handle)] ?: throw InternalException("Missing key in handlemap: was the handle used after being freed?")
33+
val clone = nextKey()
34+
map[clone] = obj
35+
makeHandle(clone)
36+
}
37+
38+
fun consumeHandle(handle: UniffiHandle): T = lock.writeLock().withLock {
39+
map.remove(key(handle)) ?: throw InternalException("Missing key in handlemap: was the handle used after being freed?")
40+
}
41+
42+
companion object {
43+
// Generate map IDs that are likely to be unique
44+
private var mapIdCounter: Long = {{ ci.namespace_hash() }}.and(0x7FFF)
45+
46+
// Map ID, shifted into the top 16 bits
47+
internal fun nextMapId(): Long = mapIdCounter.shl(48).also {
48+
// On Kotlin, map ids are only 15 bits to get around signed/unsigned issues
49+
mapIdCounter = ((mapIdCounter + 1).and(0x7FFF))
50+
}
51+
}
52+
}
53+

uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -78,89 +78,7 @@ private inline fun <U> rustCall(callback: (RustCallStatus) -> U): U {
7878
return rustCallWithError(NullCallStatusErrorHandler, callback);
7979
}
8080

81-
// IntegerType that matches Rust's `usize` / C's `size_t`
82-
public class USize(value: Long = 0) : IntegerType(Native.SIZE_T_SIZE, value, true) {
83-
// This is needed to fill in the gaps of IntegerType's implementation of Number for Kotlin.
84-
override fun toByte() = toInt().toByte()
85-
// Needed until https://youtrack.jetbrains.com/issue/KT-47902 is fixed.
86-
@Deprecated("`toInt().toChar()` is deprecated")
87-
override fun toChar() = toInt().toChar()
88-
override fun toShort() = toInt().toShort()
89-
90-
fun writeToBuffer(buf: ByteBuffer) {
91-
// Make sure we always write usize integers using native byte-order, since they may be
92-
// casted to pointer values
93-
buf.order(ByteOrder.nativeOrder())
94-
try {
95-
when (Native.SIZE_T_SIZE) {
96-
4 -> buf.putInt(toInt())
97-
8 -> buf.putLong(toLong())
98-
else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}")
99-
}
100-
} finally {
101-
buf.order(ByteOrder.BIG_ENDIAN)
102-
}
103-
}
104-
105-
companion object {
106-
val size: Int
107-
get() = Native.SIZE_T_SIZE
108-
109-
fun readFromBuffer(buf: ByteBuffer) : USize {
110-
// Make sure we always read usize integers using native byte-order, since they may be
111-
// casted from pointer values
112-
buf.order(ByteOrder.nativeOrder())
113-
try {
114-
return when (Native.SIZE_T_SIZE) {
115-
4 -> USize(buf.getInt().toLong())
116-
8 -> USize(buf.getLong())
117-
else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}")
118-
}
119-
} finally {
120-
buf.order(ByteOrder.BIG_ENDIAN)
121-
}
122-
}
123-
}
124-
}
125-
126-
127-
// Map handles to objects
128-
//
129-
// This is used when the Rust code expects an opaque pointer to represent some foreign object.
130-
// Normally we would pass a pointer to the object, but JNA doesn't support getting a pointer from an
131-
// object reference , nor does it support leaking a reference to Rust.
132-
//
133-
// Instead, this class maps USize values to objects so that we can pass a pointer-sized type to
134-
// Rust when it needs an opaque pointer.
135-
//
136-
// TODO: refactor callbacks to use this class
137-
internal class UniFfiHandleMap<T: Any> {
138-
private val map = ConcurrentHashMap<USize, T>()
139-
// Use AtomicInteger for our counter, since we may be on a 32-bit system. 4 billion possible
140-
// values seems like enough. If somehow we generate 4 billion handles, then this will wrap
141-
// around back to zero and we can assume the first handle generated will have been dropped by
142-
// then.
143-
private val counter = java.util.concurrent.atomic.AtomicInteger(0)
144-
145-
val size: Int
146-
get() = map.size
147-
148-
fun insert(obj: T): USize {
149-
val handle = USize(counter.getAndAdd(1).toLong())
150-
map.put(handle, obj)
151-
return handle
152-
}
153-
154-
fun get(handle: USize): T? {
155-
return map.get(handle)
156-
}
157-
158-
fun remove(handle: USize): T? {
159-
return map.remove(handle)
160-
}
161-
}
162-
16381
// FFI type for Rust future continuations
16482
internal interface UniFffiRustFutureContinuationCallbackType : com.sun.jna.Callback {
165-
fun callback(continuationHandle: USize, pollResult: Short);
83+
fun callback(continuationHandle: Long, pollResult: Short);
16684
}

uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ inline fun <T : Disposable?, R> T.use(block: (T) -> R) =
3030
}
3131
}
3232

33+
// Wraps `UniffiHandle` to pass to object constructors.
34+
//
35+
// This class exists because `UniffiHandle` is a typealias to `Long`. If the object constructor
36+
// inputs `UniffiHandle` directly and the user defines a primary constructor than inputs a single
37+
// `Long` or `ULong` input, then we get JVM signature conflicts. To avoid this, we pass this type
38+
// in instead.
39+
//
40+
// Let's try to remove this when we update the code based on ADR-0008.
41+
data class UniffiHandleWrapper(val handle: UniffiHandle)
42+
3343
// The base class for all UniFFI Object types.
3444
//
3545
// This class provides core operations for working with the Rust handle to the live Rust struct on

0 commit comments

Comments
 (0)