Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions quest/m1/ffi-export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions quest/m1/ffi-export/kotlin-native-memory.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>` and `AsRef<[T]>` of a `Vec` translate
25 changes: 25 additions & 0 deletions quest/m3/ffi-cpp-shared-future.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions quest/m3/ffi-kotlin-bytes-equality.md
Original file line number Diff line number Diff line change
@@ -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<u8>` 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.