Skip to content

Commit 3de393c

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 c01483c commit 3de393c

Some content is hidden

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

53 files changed

+443
-532
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
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.
26+
- The `NoPointer` singleton was renamed to `NoHandle`
2327

2428
### What's new?
2529

fixtures/coverall/tests/bindings/test_coverall.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,11 +446,11 @@ Coveralls("test_bytes").use { coveralls ->
446446

447447
// Test fakes using open classes
448448

449-
class FakePatch(private val color: Color): Patch(NoPointer) {
449+
class FakePatch(private val color: Color): Patch(NoHandle) {
450450
override fun `getColor`(): Color = color
451451
}
452452

453-
class FakeCoveralls(private val name: String) : Coveralls(NoPointer) {
453+
class FakeCoveralls(private val name: String) : Coveralls(NoHandle) {
454454
private val repairs = mutableListOf<Repair>()
455455

456456
override fun `addPatch`(patch: Patch) {

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)

fixtures/uitests/tests/ui/interface_trait_not_sync_and_send.stderr

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
error[E0277]: `(dyn Trait + 'static)` cannot be shared between threads safely
2+
--> $OUT_DIR[uniffi_uitests]/trait.uniffi.rs
3+
|
4+
| #[::uniffi::export_for_udl]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Trait + 'static)` cannot be shared between threads safely
6+
|
7+
= help: the trait `Sync` is not implemented for `(dyn Trait + 'static)`
8+
note: required by a bound in `HandleAlloc`
9+
--> $WORKSPACE/uniffi_core/src/ffi_converter_traits.rs
10+
|
11+
| pub unsafe trait HandleAlloc<UT>: Send + Sync {
12+
| ^^^^ required by this bound in `HandleAlloc`
13+
= note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info)
14+
15+
error[E0277]: `(dyn Trait + 'static)` cannot be sent between threads safely
16+
--> $OUT_DIR[uniffi_uitests]/trait.uniffi.rs
17+
|
18+
| #[::uniffi::export_for_udl]
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Trait + 'static)` cannot be sent between threads safely
20+
|
21+
= help: the trait `Send` is not implemented for `(dyn Trait + 'static)`
22+
note: required by a bound in `HandleAlloc`
23+
--> $WORKSPACE/uniffi_core/src/ffi_converter_traits.rs
24+
|
25+
| pub unsafe trait HandleAlloc<UT>: Send + Sync {
26+
| ^^^^ required by this bound in `HandleAlloc`
27+
= note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info)
28+
129
error[E0277]: `(dyn Trait + 'static)` cannot be shared between threads safely
230
--> $OUT_DIR[uniffi_uitests]/trait.uniffi.rs
331
|
@@ -26,6 +54,34 @@ note: required by a bound in `FfiConverterArc`
2654
| ^^^^ required by this bound in `FfiConverterArc`
2755
= note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info)
2856

57+
error[E0277]: `(dyn ProcMacroTrait + 'static)` cannot be shared between threads safely
58+
--> tests/ui/interface_trait_not_sync_and_send.rs:11:1
59+
|
60+
11 | #[uniffi::export]
61+
| ^^^^^^^^^^^^^^^^^ `(dyn ProcMacroTrait + 'static)` cannot be shared between threads safely
62+
|
63+
= help: the trait `Sync` is not implemented for `(dyn ProcMacroTrait + 'static)`
64+
note: required by a bound in `HandleAlloc`
65+
--> $WORKSPACE/uniffi_core/src/ffi_converter_traits.rs
66+
|
67+
| pub unsafe trait HandleAlloc<UT>: Send + Sync {
68+
| ^^^^ required by this bound in `HandleAlloc`
69+
= note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info)
70+
71+
error[E0277]: `(dyn ProcMacroTrait + 'static)` cannot be sent between threads safely
72+
--> tests/ui/interface_trait_not_sync_and_send.rs:11:1
73+
|
74+
11 | #[uniffi::export]
75+
| ^^^^^^^^^^^^^^^^^ `(dyn ProcMacroTrait + 'static)` cannot be sent between threads safely
76+
|
77+
= help: the trait `Send` is not implemented for `(dyn ProcMacroTrait + 'static)`
78+
note: required by a bound in `HandleAlloc`
79+
--> $WORKSPACE/uniffi_core/src/ffi_converter_traits.rs
80+
|
81+
| pub unsafe trait HandleAlloc<UT>: Send + Sync {
82+
| ^^^^ required by this bound in `HandleAlloc`
83+
= note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info)
84+
2985
error[E0277]: `(dyn ProcMacroTrait + 'static)` cannot be shared between threads safely
3086
--> tests/ui/interface_trait_not_sync_and_send.rs:11:1
3187
|

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,10 @@ impl KotlinCodeOracle {
333333
}
334334
FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(),
335335
FfiType::ForeignCallback => "ForeignCallback".to_string(),
336-
FfiType::ForeignExecutorHandle => "USize".to_string(),
337336
FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback".to_string(),
338337
FfiType::RustFutureContinuationCallback => {
339338
"UniFffiRustFutureContinuationCallbackType".to_string()
340339
}
341-
FfiType::RustFutureContinuationData => "USize".to_string(),
342340
}
343341
}
344342

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+

0 commit comments

Comments
 (0)