diff --git a/quest/m1/ffi-export/README.md b/quest/m1/ffi-export/README.md index 8eccadb9b..dd92f1ae6 100644 --- a/quest/m1/ffi-export/README.md +++ b/quest/m1/ffi-export/README.md @@ -60,5 +60,4 @@ than `ctypes`; the others call C natively. moq's packages switch to them in moq' - [Dart: calls into a held implementation while the isolate waits](/quest/m1/ffi-export/dart-threads.md) - an implementation Rust kept and calls from another thread runs, or fails loud, while the isolate waits in a call - [Dart: cheaper ready futures](/quest/m1/ffi-export/dart-futures.md) - a read ready at its first poll costs no more than in Python or Go - [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 index 3fc089771..a237642d4 100644 --- a/quest/m1/ffi-export/kotlin-native-memory.md +++ b/quest/m1/ffi-export/kotlin-native-memory.md @@ -10,7 +10,7 @@ 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 +GC-owned: 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 @@ -28,7 +28,3 @@ the numbers support. Options to weigh: 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/m1/ffi-export/lifetimes.md b/quest/m1/ffi-export/lifetimes.md deleted file mode 100644 index 9120900b8..000000000 --- a/quest/m1/ffi-export/lifetimes.md +++ /dev/null @@ -1,30 +0,0 @@ -# [M] Lifetimes with no undefined behavior - -## Goal - -No generated binding in a garbage-collected or reference-counted language lets a program -reach undefined behavior through an object or its bytes: Kotlin's and Go's `close()` leave -every later call an error, a lent object's lend never ends under a call, and returned bytes -are owned by the language's memory manager, so no view outlives its buffer. C++ owns -handles through the standard library's smart pointers. Each rule is proven by a test that -uses an object after its free, races a free against a call, and keeps a view past its owner. - -## Plan - -Decided with the maintainer on 2026-11-01, from an audit of each generator: avoid an -explicit `close()` where the language frees deterministically, since without Rust's -`close(self)` a program can keep a reference to a closed object. A `close()` stays only -where the language's GC can't free promptly, and then every call after it returns or throws -an error, never a crash or undefined behavior. - -- **Go**: settled in [#411](https://github.com/moq-dev/rust2/pull/411): objects keep `Close`, - with every later call returning an error, and bytes are a GC-owned `Bytes` with no raw - slice, read through methods and copied by `Copy()`. -- **A lend's end**: Swift, Python, Kotlin, and Go close an object Rust lent a closure once - the closure returns, and Rust frees the lent handle once its own check finds no borrow. - A call on another thread that held the binding's handle before that close but has not - yet borrowed in Rust then reaches the freed handle. Closing a lent handle that a call - still holds must fail loud, as Rust's own check does for a call already inside Rust. - Reproduce it deterministically first, such as through a test hook between the binding's - hold and the C call. -- **C**: out of scope; its `_free` follows C's norms. diff --git a/quest/m3/ffi-cpp-shared-future.md b/quest/m3/ffi-cpp-shared-future.md index 736624a0d..b3b77f4dc 100644 --- a/quest/m3/ffi-cpp-shared-future.md +++ b/quest/m3/ffi-cpp-shared-future.md @@ -12,14 +12,9 @@ Found by [#430](https://github.com/moq-dev/rust2/pull/430), whose design this fi 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. +the borrowed value, which Rust refuses. 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