diff --git a/quest/m1/ffi-export/README.md b/quest/m1/ffi-export/README.md index 758357a0d..4d7311e07 100644 --- a/quest/m1/ffi-export/README.md +++ b/quest/m1/ffi-export/README.md @@ -61,3 +61,4 @@ than `ctypes`; the others call C natively. moq's packages switch to them in moq' - [Dart bindings](/quest/m1/ffi-export/dart.md) - Dart over the C ABI through `dart:ffi` - [Go: cheaper lent Bytes](/quest/m1/ffi-export/go-bytes.md) - a lent buffer costs no more than a 1 KiB copy, so Go's 2 KiB copy threshold can fall or go - [Lifetimes with no undefined behavior](/quest/m1/ffi-export/lifetimes.md) - errors after `close()` in Kotlin and Go, lends that never end under a call, GC-owned bytes, and C++ smart pointers +- [Kotlin: lent bytes freed before native memory piles up](/quest/m1/ffi-export/kotlin-native-memory.md) - reading many large `Bytes` holds a bounded backlog of Rust's memory even when the GC rarely runs diff --git a/quest/m1/ffi-export/kotlin-native-memory.md b/quest/m1/ffi-export/kotlin-native-memory.md new file mode 100644 index 000000000..3fc089771 --- /dev/null +++ b/quest/m1/ffi-export/kotlin-native-memory.md @@ -0,0 +1,34 @@ +# [M] Kotlin: lent bytes freed before native memory piles up + +## Goal + +A Kotlin program that reads many large `Bytes` from Rust, such as moq's Android player +reading frames, holds no more of Rust's memory than its live `Bytes` reach plus a bounded +backlog, even when it makes little Java garbage, so a GC that never runs leaves no +growing native heap behind it. + +## Plan + +Found by [#432](https://github.com/moq-dev/rust2/pull/432), which made bytes over 1 KiB +GC-owned under [the lifetimes quest](/quest/m1/ffi-export/lifetimes.md): Rust's buffer is +freed by a cleaner once the GC collects its `Native.Lent`, and the JVM does not see the +buffer's size. A scratch loop returning a fresh Rust buffer per call measured 1.6 to 13 µs +per lent `Bytes` from 1025 B to 16 KiB, against 0.15 to 3.4 µs with every size copied, as +unfreed buffers piled up between collections. + +Measure first, at moq's real frame sizes and rates on the JVM and on ART, then pick what +the numbers support. Options to weigh: + +- tell the GC the native size where the platform allows it: ART's + `VMRuntime.registerNativeAllocation` and `NativeAllocationRegistry` are not public API + at API 24, so check which levels expose either, while the JVM has no equivalent; +- a bounded count or byte budget of live lent buffers, past which a call copies instead + of lending, or nudges the GC; +- raising the 1 KiB copy threshold where a copy costs less than the backlog. + +No `close()` comes back: a free while a slice is live is what the lifetimes quest removed. +Go's `Bytes` has the same shape (`runtime.AddCleanup`); note whether its fix applies there. + +## Required + +- [Lifetimes with no undefined behavior](/quest/m1/ffi-export/lifetimes.md) - #432's GC-owned Kotlin bytes land first diff --git a/quest/m3/README.md b/quest/m3/README.md index 85050b626..b6b04628d 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -41,4 +41,6 @@ generic std gap is planned here even when moq never calls it. - [Python's wake on the loop's own thread](/quest/m3/ffi-python-wake.md) - a wake on the event loop's thread resumes the future with no socket pair - [Python's call cost](/quest/m3/ffi-python-calls.md) - a call on an object costs what BoltFFI's does, or the gap is explained - [No unused import in an export's generated crate](/quest/m3/ffi-unused-c-void.md) - the crate rust2-ffi generates builds without warnings when no export uses `void *` +- [Kotlin: bytes compare by content](/quest/m3/ffi-kotlin-bytes-equality.md) - `Bytes` and records holding it compare and hash by their bytes +- [C++: a move out of a shared copy while its future pends](/quest/m3/ffi-cpp-shared-future.md) - the last shared copy moves out by a clone even while a destroyed copy's future pends - [A box's clone_from, and a vector's AsRef and AsMut](/quest/m3/ts-box-vec-conversions.md) - `clone_from` of a `Box` and `AsRef<[T]>` of a `Vec` translate diff --git a/quest/m3/ffi-cpp-shared-future.md b/quest/m3/ffi-cpp-shared-future.md new file mode 100644 index 000000000..736624a0d --- /dev/null +++ b/quest/m3/ffi-cpp-shared-future.md @@ -0,0 +1,25 @@ +# [S] C++: a move out of a shared copy while its future pends + +## Goal + +In the generated C++, a move out of the last copy of a shared object succeeds, as Rust's +`clone` then move would, even while a future made by a `&self` call on a copy since +destroyed still pends, instead of aborting with "a handle is borrowed". + +## Plan + +Found by [#430](https://github.com/moq-dev/rust2/pull/430), whose design this fixes: +types passing its clone guard hold their handle through `std::shared_ptr`, and a move +out of a copy that others share moves out a clone instead. A pending future borrows +Rust's handle without holding a `shared_ptr`, so `use_count` reads 1 and the move takes +the borrowed value, which Rust refuses. Starts once #430 merges, and changes with it if +the maintainer settles its `Arc` decision another way. + +Options to weigh: a future holds a `shared_ptr` copy of the handle it borrows, so +`use_count` sees it; or the move asks Rust whether the handle is borrowed and clones if so. +Prove it with a test that destroys the copy a pending future came from, then moves out +of the last copy. + +## Required + +- [Lifetimes with no undefined behavior](/quest/m1/ffi-export/lifetimes.md) - #430's shared C++ handles land first diff --git a/quest/m3/ffi-kotlin-bytes-equality.md b/quest/m3/ffi-kotlin-bytes-equality.md new file mode 100644 index 000000000..1c0be2d3d --- /dev/null +++ b/quest/m3/ffi-kotlin-bytes-equality.md @@ -0,0 +1,21 @@ +# [S] Kotlin: bytes compare by content + +## Goal + +Kotlin's `Bytes` compares and hashes by its bytes, as Rust's `Bytes` and `Vec` do, so +a generated data class holding bytes, such as a record with a payload, equals another +record with the same fields, and works as a map key. + +## Plan + +Found by [#432](https://github.com/moq-dev/rust2/pull/432): `Bytes` has no `equals` or +`hashCode`, so it compares by identity and a record holding bytes equals only itself. +This predates #432. + +Content equality must read lent bytes through the same JNI calls that keep their buffer +alive during a read, and cost no more crossings than the comparison needs: compare sizes +first, and copy or compare in bulk rather than per byte. Cache the hash only if a +measurement shows it matters. Prove it with a test comparing copied, lent, sliced, and +Kotlin-held bytes, and records holding each. + +Public API: breaking for code that relied on identity; it gets a `MIGRATE.md` entry.