From 6e3fb933b6e5acced2d0303693a00861ca1d2132 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:07:30 -0800 Subject: [PATCH 01/12] chore(quest): claim quest/m1/ffi-export/refusals/small-shapes.md Quest-Claim: 369b964a0792f4f7 From b42d794eea23d9d046f55229f52178497c1e8d27 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:12:44 -0800 Subject: [PATCH 02/12] feat(ffi): pass no C argument for a () parameter, and export an empty enum as an object Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 3 ++- ffi/fixtures/api/examples/transcript.rs | 1 + ffi/fixtures/api/src/lib.rs | 5 ++++ ffi/fixtures/reject/expected.txt | 1 + ffi/fixtures/reject/src/lib.rs | 3 +++ ffi/src/collect.rs | 17 +++++++++++--- ffi/src/glue.rs | 4 ++++ ffi/src/ir.rs | 2 ++ ffi/tests/api.c | 1 + ffi/tests/api.cpp | 1 + ffi/tests/api.dart | 1 + ffi/tests/api.go | 1 + ffi/tests/api.kt | 1 + ffi/tests/api.py | 1 + ffi/tests/api.swift | 1 + quest/m1/ffi-export/refusals/small-shapes.md | 24 +++++++++++++++++++- 16 files changed, 62 insertions(+), 5 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 8125eb80d..5fdc20857 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -57,7 +57,7 @@ its types map by role; a type no path reaches is refused. | Rust | C | | --- | --- | | a struct whose fields are all `pub`, not `#[non_exhaustive]`, which map, with no `Drop` of its own | a record: a struct, copied across | -| an enum whose fields map, with no `Drop` of its own | a C enum without fields, or a struct of a tag and a union | +| an enum with variants whose fields map, with no `Drop` of its own | a C enum without fields, or a struct of a tag and a union | | any other type, if `Send` | an object: an opaque handle to an `Arc`, freed with `_free` | | a type whose only generic parameters are lifetimes, such as `Path<'a>` | an object holding its `'static` form | | a generic type, at each instance it infers | that instance as any other type, with each method whose impl and own bounds it meets; any other instance is refused | @@ -65,6 +65,7 @@ its types map by role; a type no path reaches is refused. | a `pub` trait | a callback interface: a table of C functions and their data | | a type parameter or `impl` bounded by `Fn`, `FnMut`, or `FnOnce`, which no lifetime bound or return lets outlive the call | a C function and its `void *` data, which Rust calls only during the call; any other closure is refused | | `fn` and inherent `pub` methods | functions named through their type | +| a `()` parameter of a function, not of a callback's method | nothing: C passes no argument for it | | `Result` | `bool f(..., T *out, E *err)` | | a returned tuple `(A, B)` | one out pointer each: `void f(..., A *out0, B *out1)` | | a tuple inside another type, or as a field or parameter | `_tuple__`: a struct of fields `_0`, `_1`, and so on; of references only returned | diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index b58b4ae3c..4bff6f786 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -176,6 +176,7 @@ fn main() { println!("unnamed [{}]", api::unnamed().as_str()); println!("total {}", api::total(2..5)); + println!("reach {}", api::reach(2, (), 5)); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); println!( diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 6c7843334..bca294ef4 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -388,6 +388,11 @@ pub fn total(range: Range) -> u64 { range.sum() } +/// How far `start` reaches to `end`, past a `()` C does not pass. +pub fn reach(start: u32, _: (), end: u32) -> u32 { + end - start +} + /// A range, widened by `by` at each end. pub fn widen(range: Range, by: u64) -> Range { range.start.saturating_sub(by)..range.end + by diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 1dfe156be..3927e2226 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -77,6 +77,7 @@ fixtures/reject/src/lib.rs:661:1: rust2-ffi: cannot export fn `lent`: foreign ty fixtures/reject/src/lib.rs:679:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:68:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:63:2: note: fn `reject::audio_encoder::finish` is here +fixtures/reject/src/lib.rs:690:1: rust2-ffi: cannot export enum `Void`: object no export returns; exclude it in the config to leave it out fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export struct `Wrap`: generic type; exclude it in the config to leave it out fixtures/reject/src/lib.rs:72:1: rust2-ffi: cannot export fn `letters`: Range `char`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:81:2: rust2-ffi: cannot export method `Text::new`: 'static borrow `&'a str`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 5b4c72a44..79f264373 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -685,3 +685,6 @@ pub fn halves() -> impl Iterator { pub fn mixed(a: Option<((u8, u16), u32)>, b: Option<((u8, u16, u32),)>) -> bool { a.is_some() && b.is_some() } + +/// No value is one, so C never holds a handle to it. +pub enum Void {} diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index f420b69df..36f1e97d0 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -1132,6 +1132,7 @@ impl<'tcx> Collector<'tcx> { name: "input".to_string(), ty: Type::Ref(Mutability::Not, Box::new(Type::Str)), }], + units: Vec::new(), ok: ty.clone(), err: Some(Type::String), future: false, @@ -1165,6 +1166,7 @@ impl<'tcx> Collector<'tcx> { docs: "The document as JSON text.".to_string(), receiver: Some(Receiver::Ref), params: Vec::new(), + units: Vec::new(), ok: Type::String, err: None, future: false, @@ -1230,9 +1232,8 @@ impl<'tcx> Collector<'tcx> { false => self.object(site), } } - DefKind::Enum if tcx.adt_def(def_id).variants().is_empty() => { - Decl::Refused(why("enum without variants", "")) - } + // No value is one, so it is a handle C never holds, which no C enum is. + DefKind::Enum if tcx.adt_def(def_id).variants().is_empty() => self.object(site), DefKind::Enum => Decl::Enum(0), _ => Decl::Callback(0), } @@ -1670,6 +1671,7 @@ impl<'tcx> Collector<'tcx> { docs: docs(tcx, field.did), receiver: Some(receiver), params, + units: Vec::new(), ok, err: None, future: false, @@ -2013,7 +2015,14 @@ impl<'tcx> Collector<'tcx> { }; let callback = tcx.def_kind(tcx.parent(def_id)) == DefKind::Trait; let mut params = Vec::new(); + let mut units = Vec::new(); for (i, ty) in inputs { + // C passes nothing for `()`, the one value of its type. A callback's stays + // refused: the glue's implementation of its trait names every parameter. + if ty.is_unit() && !callback { + units.push(params.len() + units.len()); + continue; + } let name = names .get(i) .copied() @@ -2079,6 +2088,7 @@ impl<'tcx> Collector<'tcx> { docs: docs(tcx, def_id), receiver, params, + units, ok, err, future, @@ -3241,6 +3251,7 @@ impl<'tcx> Collector<'tcx> { docs: docs(tcx, def_id), receiver: None, params: Vec::new(), + units: Vec::new(), ok, err: None, future: false, diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 58307c0d7..90baaf4a4 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -1178,6 +1178,10 @@ impl Gen<'_> { stmts.push(stmt); args.push(arg); } + let first = args.len() - function.params.len(); + for unit in &function.units { + args.insert(first + unit, "()".to_string()); + } let mut callee = match &owner { Some((_, owner_type)) => format!("<{}>::{}", self.rust(owner_type), function.name()), None => self.path(&function.path), diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index d85445324..81d49f1ae 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -183,6 +183,8 @@ pub struct Function { pub docs: String, pub receiver: Option, pub params: Vec, + /// Where among Rust's parameters each `()` one sits, which C does not pass. + pub units: Vec, /// What it returns, or for a future, what the future does. pub ok: Type, /// The error of a `Result` it returns. diff --git a/ffi/tests/api.c b/ffi/tests/api.c index aab6e5855..a8938a996 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -396,6 +396,7 @@ static int script(void) { api_Name_free(unnamed); printf("total %llu\n", (unsigned long long)api_total((api_range_u64){.start = 2, .end = 5})); + printf("reach %u\n", (unsigned)api_reach(2, 5)); api_range_u64 wide = api_widen((api_range_u64){.start = 2, .end = 5}, 1); printf("widen %llu %llu\n", (unsigned long long)wide.start, (unsigned long long)wide.end); api_bounds_u64 ranges[] = { diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 4871c1137..fb253d166 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -324,6 +324,7 @@ int script() { } std::printf("total %llu\n", ull(api::total({2, 5}))); + std::printf("reach %u\n", unsigned(api::reach(2, 5))); api::range_u64 wide = api::widen({2, 5}, 1); std::printf("widen %llu %llu\n", ull(wide.start), ull(wide.end)); api::bounds_u64 ranges[] = { diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 114294f25..473d699f8 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -222,6 +222,7 @@ Future script() async { print('unnamed [${api.unnamed().asStr()}]'); print('total ${api.total(const api.RangeU64(2, 5))}'); + print('reach ${api.reach(2, 5)}'); final wide = api.widen(const api.RangeU64(2, 5), 1); print('widen ${wide.start} ${wide.end}'); final counts = [ diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 6f4a54b48..55e2c3ea5 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -277,6 +277,7 @@ func script() { fmt.Printf("unnamed [%s]\n", must(api.Unnamed().AsStr())) fmt.Printf("total %d\n", api.Total(rt.RangeU64{Start: 2, End: 5})) + fmt.Printf("reach %d\n", api.Reach(2, 5)) wide := api.Widen(rt.RangeU64{Start: 2, End: 5}, 1) fmt.Printf("widen %d %d\n", wide.Start, wide.End) included := func(n uint64) rt.BoundU64 { return rt.BoundU64{Kind: rt.BoundIncluded, Value: n} } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 196ef96d8..67a2e1593 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -151,6 +151,7 @@ fun script() { println("unnamed [${unnamed().asStr()}]") println("total ${total(RangeU64(2u, 5u))}") + println("reach ${reach(2u, 5u)}") val wide = widen(RangeU64(2u, 5u), 1u) println("widen ${wide.start} ${wide.end}") val counts = listOf( diff --git a/ffi/tests/api.py b/ffi/tests/api.py index adbd5e5c6..cace7535f 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -219,6 +219,7 @@ def edit(counter): print(f"unnamed [{api.unnamed().as_str()}]") print(f"total {api.total(range(2, 5))}") + print(f"reach {api.reach(2, 5)}") wide = api.widen(range(2, 5), 1) print(f"widen {wide.start} {wide.stop}") inc, exc, unb = api.Bound.INCLUDED, api.Bound.EXCLUDED, api.Bound.UNBOUNDED diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 396368d0d..768ed0447 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -263,6 +263,7 @@ func script() { print("unnamed [\(unnamed().asStr())]") print("total \(total(RangeU64(start: 2, end: 5)))") + print("reach \(reach(2, 5))") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") let counts = [ diff --git a/quest/m1/ffi-export/refusals/small-shapes.md b/quest/m1/ffi-export/refusals/small-shapes.md index 2aacdfd1f..5d7c7ec77 100644 --- a/quest/m1/ffi-export/refusals/small-shapes.md +++ b/quest/m1/ffi-export/refusals/small-shapes.md @@ -1,4 +1,4 @@ -# [S] Small shapes export +# [M] Small shapes export ## Goal @@ -12,3 +12,25 @@ that is not `Send`. Each is its own small mapping; what has no C shape, such as an object that is not `Send` under the C ABI's any-thread calls, keeps its refusal and goes to the maintainer as moq's signature to change. + +✅ A `()` parameter is passed by nothing in C, clearing `container::Producer::set`'s four +instances, and an enum without variants is an object, which no export returns, so it leaves +with [the unholdable rule](/quest/m1/ffi-export/refusals/unholdable.md). + +Left at moq `ffcb7cd9`, outside the moq-tokio items narrowing the stand-in drops (the +arrays, `Result` and `!` off a return, and `worker::Group::finished`'s future): + +- `u128`: `Timestamp::as_scale`, `as_millis`, `as_micros`, `as_nanos`. std returns it too + (`Duration::as_nanos`), so it maps by rule: proposed as `_u128`, two `uint64_t` + halves in C, and each language's arbitrary-precision integer above it. +- const generics: `group::Producer::write_frames` and `group::Consumer::read_frames` over + `frame::Buffer`. The IR keeps only type arguments; a const argument + in `Type`, inferred at its default as a type parameter's is, exports `Buffer<8>` and the + methods at it. +- not `Send`: `clock::Entered` and `clock::Rng` pin a thread, and `catalog::Guard` holds a + `MutexGuard`; no C shape, so moq's signatures change or they stay refused. + +## Waiting + +- Maintainer: choose the `u128` mapping, and whether moq changes or keeps refused + `Entered`, `Rng`, and `Guard`; check the PR's decisions are answered From c26e26d328677934a8519d744af71e1cd4cd07ea Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 20:43:17 -0800 Subject: [PATCH 03/12] feat(ffi): export u128 as two u64 halves, and each language's big integer Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 2 ++ ffi/fixtures/api/src/lib.rs | 5 ++++ ffi/runtime/ffi_cgo.go | 22 +++++++++++++- ffi/runtime/jni.rs | 11 +++++++ ffi/runtime/python.rs | 33 ++++++++++++++++++++ ffi/runtime/rt.dart | 19 ++++++++++++ ffi/runtime/rt.kt | 13 ++++++++ ffi/runtime/rt.rs | 21 +++++++++++++ ffi/runtime/rt.swift | 40 +++++++++++++++++++++++++ ffi/src/abi.rs | 5 ++++ ffi/src/c.rs | 4 +++ ffi/src/collect.rs | 1 + ffi/src/cpp.rs | 10 +++++-- ffi/src/dart.rs | 7 ++++- ffi/src/glue.rs | 4 +++ ffi/src/go.rs | 10 +++++-- ffi/src/ir.rs | 2 ++ ffi/src/kotlin.rs | 8 +++-- ffi/src/python.rs | 4 ++- ffi/src/swift.rs | 5 ++++ ffi/tests/api.c | 14 +++++++++ ffi/tests/api.cpp | 9 ++++++ ffi/tests/api.dart | 8 +++++ ffi/tests/api.go | 16 ++++++++++ ffi/tests/api.kt | 6 ++++ ffi/tests/api.py | 4 +++ ffi/tests/api.swift | 7 +++++ 27 files changed, 281 insertions(+), 9 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index a45944d6e..4ab18147c 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -196,6 +196,8 @@ fn main() { println!("total {}", api::total(2..5)); println!("reach {}", api::reach(2, (), 5)); + let scaled = |v, by| api::scaled(v, by).map_or("none".to_string(), |v| format!("{v:032x}")); + println!("scaled {} {} {}", scaled(u128::MAX, 1), scaled((1 << 64) + 1, 1 << 63), scaled(1 << 127, 2)); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); println!( diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 6d8cf0a5d..6272fb32c 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -414,6 +414,11 @@ pub fn reach(start: u32, _: (), end: u32) -> u32 { end - start } +/// `v` times `by`, unless past `u128::MAX`. +pub fn scaled(v: u128, by: u64) -> Option { + v.checked_mul(by.into()) +} + /// A range, widened by `by` at each end. pub fn widen(range: Range, by: u64) -> Range { range.start.saturating_sub(by)..range.end + by diff --git a/ffi/runtime/ffi_cgo.go b/ffi/runtime/ffi_cgo.go index c7d8df558..5f90cef8a 100644 --- a/ffi/runtime/ffi_cgo.go +++ b/ffi/runtime/ffi_cgo.go @@ -1,5 +1,5 @@ // What each generated package converts its C types with, which cgo makes its own, after -// the generated code names the export's C types as cBuf, cWaker, cDuration, and cAddr, its +// the generated code names the export's C types as cBuf, cWaker, cU128, cDuration, and cAddr, its // buffer's free as cBufFree, and the runtime's callbacks into Go as goCall and goRelease: // the arena a call lowers nested values into, values across the C ABI, and what a future // waits on. @@ -105,6 +105,26 @@ func lentBytesOption_(buf cBuf) *[]byte { return &b } +// lowerU128_ is a u128 as Rust holds it, from 0 through 2^128 - 1. +func lowerU128_(v *big.Int) cU128 { + if v.Sign() < 0 || v.BitLen() > 128 { + panic("an int out of range for Rust's u128") + } + var b [16]byte + v.FillBytes(b[:]) + var hi, lo uint64 + for i := range 8 { + hi = hi<<8 | uint64(b[i]) + lo = lo<<8 | uint64(b[8+i]) + } + return cU128{lo: C.uint64_t(lo), hi: C.uint64_t(hi)} +} + +func liftU128_(c cU128) *big.Int { + hi := new(big.Int).SetUint64(uint64(c.hi)) + return hi.Lsh(hi, 64).Or(hi, new(big.Int).SetUint64(uint64(c.lo))) +} + // lowerDuration_ is a Duration as Rust holds it, which has no negative one. func lowerDuration_(d time.Duration) cDuration { if d < 0 { diff --git a/ffi/runtime/jni.rs b/ffi/runtime/jni.rs index 40546c604..2f740dc08 100644 --- a/ffi/runtime/jni.rs +++ b/ffi/runtime/jni.rs @@ -723,6 +723,17 @@ where } } +impl Wire for rt::U128 { + fn read(r: &mut Reader<'_>) -> Self { + Self { lo: r.get(), hi: r.get() } + } + + fn write(self, w: &mut Writer) { + w.put(self.lo); + w.put(self.hi); + } +} + impl Wire for rt::Duration { fn read(r: &mut Reader<'_>) -> Self { Self { diff --git a/ffi/runtime/python.rs b/ffi/runtime/python.rs index c7bd9b6a6..a01a44f60 100644 --- a/ffi/runtime/python.rs +++ b/ffi/runtime/python.rs @@ -589,6 +589,39 @@ pub fn uint_out(value: u64) -> R { unsafe { Obj::new(PyLong_FromUnsignedLongLong(value)) } } +/// A `u128`, from any `int` or object with `__index__` in its range. +pub fn u128_in(obj: *mut PyObject) -> R { + // SAFETY: the GIL is held, and `obj` is live. + unsafe { + let index = Obj::new(PyNumber_Index(obj))?; + let shift = uint_out(64)?; + let high = Obj::new(PyNumber_Rshift(index.ptr(), shift.ptr()))?; + // A negative int's high half is negative, which raises here as one past `u64` does. + let hi = PyLong_AsUnsignedLongLong(high.ptr()); + if hi == u64::MAX && !PyErr_Occurred().is_null() { + PyErr_Clear(); + return overflow_int::(); + } + let lo = PyLong_AsUnsignedLongLongMask(index.ptr()); + if lo == u64::MAX && !PyErr_Occurred().is_null() { + return Err(Raised); + } + Ok(rt::U128 { lo, hi }) + } +} + +pub fn u128_out(value: rt::U128) -> R { + let lo = uint_out(value.lo)?; + if value.hi == 0 { + return Ok(lo); + } + // SAFETY: the GIL is held, and each operand is live. + unsafe { + let high = Obj::new(PyNumber_Lshift(uint_out(value.hi)?.ptr(), uint_out(64)?.ptr()))?; + Obj::new(PyNumber_Or(high.ptr(), lo.ptr())) + } +} + pub fn float_in(obj: *mut PyObject) -> R { // SAFETY: the GIL is held, and `obj` is live. let value = unsafe { PyFloat_AsDouble(obj) }; diff --git a/ffi/runtime/rt.dart b/ffi/runtime/rt.dart index 81b16281b..4316e4c2e 100644 --- a/ffi/runtime/rt.dart +++ b/ffi/runtime/rt.dart @@ -170,6 +170,13 @@ final class _Buf extends ffi.Struct { external ffi.Pointer owner; } +final class _U128 extends ffi.Struct { + @ffi.Uint64() + external int lo; + @ffi.Uint64() + external int hi; +} + final class _Duration extends ffi.Struct { @ffi.Uint64() external int secs; @@ -587,6 +594,18 @@ final class _Native { String scalar(int code) => String.fromCharCode(code); + void u128(_U128 dst, BigInt value) { + if (value.isNegative || value.bitLength > 128) { + throw ArgumentError.value(value, 'value', "out of range for Rust's u128"); + } + // Each half as the int that holds its 64 bits. + dst.lo = value.toSigned(64).toInt(); + dst.hi = (value >> 64).toSigned(64).toInt(); + } + + BigInt u128Of(_U128 c) => + BigInt.from(c.hi).toUnsigned(64) << 64 | BigInt.from(c.lo).toUnsigned(64); + /// Rust's `Duration::MAX`, and any longer than Dart's longest, is that. static const _longest = Duration(microseconds: 0x7fffffffffffffff); diff --git a/ffi/runtime/rt.kt b/ffi/runtime/rt.kt index 95ae6521b..211d73490 100644 --- a/ffi/runtime/rt.kt +++ b/ffi/runtime/rt.kt @@ -96,6 +96,13 @@ class Writer { socketAddr(v) } + /** A `u128`, from 0 through 2^128 - 1. */ + fun u128(v: java.math.BigInteger) { + kotlin.require(v.signum() >= 0 && v.bitLength() <= 128) { "$v is out of range for Rust's u128" } + i64(v.toLong()) + i64(v.shiftRight(64).toLong()) + } + fun duration(v: kotlin.time.Duration) { kotlin.require(!v.isNegative()) { "a negative duration has no Rust Duration" } v.toComponents { secs, nanos -> @@ -194,6 +201,12 @@ class Reader(bytes: ByteArray) { return java.net.InetSocketAddress(address, port) } + fun u128(): java.math.BigInteger { + val lo = b.getLong() + val hi = b.getLong() + return java.math.BigInteger(1, java.nio.ByteBuffer.allocate(16).putLong(hi).putLong(lo).array()) + } + fun duration(): kotlin.time.Duration { val secs = b.getLong() val nanos = b.getInt() diff --git a/ffi/runtime/rt.rs b/ffi/runtime/rt.rs index 78ea8377d..4084fa948 100644 --- a/ffi/runtime/rt.rs +++ b/ffi/runtime/rt.rs @@ -144,6 +144,27 @@ impl Buf { } } +/// `u128`, as its two halves. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct U128 { + pub lo: u64, + pub hi: u64, +} + +impl U128 { + pub fn lift(self) -> u128 { + u128::from(self.hi) << 64 | u128::from(self.lo) + } + + pub fn lower(v: u128) -> Self { + Self { + lo: v as u64, + hi: (v >> 64) as u64, + } + } +} + #[repr(C)] #[derive(Clone, Copy)] pub struct Duration { diff --git a/ffi/runtime/rt.swift b/ffi/runtime/rt.swift index fa23883d4..ec2107f2d 100644 --- a/ffi/runtime/rt.swift +++ b/ffi/runtime/rt.swift @@ -20,6 +20,38 @@ public struct SocketAddr: Hashable, Sendable { } } +/// A Rust `u128`, as its two halves, until the deployment target allows Swift's `UInt128`. +public struct U128: Hashable, Comparable, Sendable, CustomStringConvertible { + public var high: UInt64 + public var low: UInt64 + + public init(high: UInt64, low: UInt64) { + self.high = high + self.low = low + } + + public init(_ value: UInt64) { + self.init(high: 0, low: value) + } + + public static func < (a: U128, b: U128) -> Bool { + (a.high, a.low) < (b.high, b.low) + } + + /// Its decimal digits. + public var description: Swift.String { + if high == 0 { + return Swift.String(low) + } + // Split off the last 19 digits, as 10^19 is the largest power of ten a UInt64 holds. + let base: UInt64 = 10_000_000_000_000_000_000 + let (q, r) = high.quotientAndRemainder(dividingBy: base) + let (lowQ, digits) = base.dividingFullWidth((r, low)) + let tail = Swift.String(digits) + return U128(high: q, low: lowQ).description + Swift.String(repeating: "0", count: 19 - tail.count) + tail + } +} + extension Native { /// Checked before any other call: a Swift file and a library from different builds /// must not mix. @@ -182,6 +214,14 @@ extension Native { return half * 2 + .seconds(Int64(value.secs & 1)) + .nanoseconds(Int64(value.nanos)) } + static func u128(_ value: U128) -> CU128 { + CU128(lo: value.low, hi: value.high) + } + + static func u128(_ value: CU128) -> U128 { + U128(high: value.hi, low: value.lo) + } + static func socketAddr(_ addr: SocketAddr) -> CAddr { let v6: Bool switch addr.ip.count { diff --git a/ffi/src/abi.rs b/ffi/src/abi.rs index fb17fd7e3..8fa622d8e 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -31,6 +31,8 @@ pub enum Ty { Char, /// `_buf`: a view of bytes passed in, an owned buffer returned. Buf, + /// `_u128` + U128, /// `_duration` Duration, /// `_socket_addr` @@ -330,6 +332,7 @@ impl<'a> Abi<'a> { Type::F32 => "f32".to_string(), Type::F64 => "f64".to_string(), Type::Char => "char".to_string(), + Type::U128 => "u128".to_string(), Type::Duration => "duration".to_string(), Type::SocketAddr(_) => "socket_addr".to_string(), Type::Option(inner) => format!("option_{}", self.key(inner)), @@ -388,6 +391,7 @@ impl<'a> Abi<'a> { Type::F64 => Ty::F64, Type::Char => Ty::Char, Type::Str | Type::String | Type::Slice | Type::Vec | Type::Bytes | Type::Path | Type::PathBuf => Ty::Buf, + Type::U128 => Ty::U128, Type::Duration => Ty::Duration, Type::SocketAddr(_) => Ty::SocketAddr, Type::Waker => Ty::Waker, @@ -554,6 +558,7 @@ impl<'a> Abi<'a> { "contract", "buf", "buf_free", + "u128", "duration", "socket_addr", "waker", diff --git a/ffi/src/c.rs b/ffi/src/c.rs index b7ef05793..ca88bbdd2 100644 --- a/ffi/src/c.rs +++ b/ffi/src/c.rs @@ -75,6 +75,9 @@ impl Header<'_> { self.line(format!( "/* Bytes. Passed in, a view Rust reads during the call and never frees. Returned,\n * an owned buffer to free once with {p}_buf_free. A null ptr is None. */\ntypedef struct {p}_buf {{\n\tconst uint8_t *ptr;\n\tsize_t len;\n\tvoid *owner;\n}} {p}_buf;\n\nvoid {p}_buf_free({p}_buf buf);\n" )); + self.line(format!( + "/* A u128: lo holds its low 64 bits and hi its high 64. */\ntypedef struct {p}_u128 {{\n\tuint64_t lo;\n\tuint64_t hi;\n}} {p}_u128;\n" + )); self.line(format!( "typedef struct {p}_duration {{\n\tuint64_t secs;\n\tuint32_t nanos;\n}} {p}_duration;\n" )); @@ -454,6 +457,7 @@ pub fn decl(abi: &Abi<'_>, ty: &Ty, name: &str) -> String { Ty::F64 => space("double"), Ty::Char => space("uint32_t"), Ty::Buf => space(&abi.rt("buf")), + Ty::U128 => space(&abi.rt("u128")), Ty::Duration => space(&abi.rt("duration")), Ty::SocketAddr => space(&abi.rt("socket_addr")), Ty::Struct(n) | Ty::Enum(n) => space(n), diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index e300137e8..46bf1c916 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -3352,6 +3352,7 @@ impl<'tcx> Collector<'tcx> { ty::Char => Type::Char, ty::Float(ty::FloatTy::F32) => Type::F32, ty::Float(ty::FloatTy::F64) => Type::F64, + ty::Uint(ty::UintTy::U128) => Type::U128, ty::Int(_) | ty::Uint(_) => Type::Int(int(ty).ok_or_else(|| why("128-bit integer", ty.to_string()))?), ty::Ref(_, inner, m) => { let inner_ty = match inner.kind() { diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index 317e91a85..592f67277 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -109,6 +109,7 @@ impl<'a> Header<'a> { Type::Str | Type::String | Type::Slice | Type::Vec | Type::Bytes | Type::Path | Type::PathBuf => { format!("::{}::buf", self.ns) } + Type::U128 => format!("::{}::u128", self.ns), Type::Duration => format!("::{}::duration", self.ns), Type::SocketAddr(_) => format!("::{}::socket_addr", self.ns), Type::List(_, inner) => format!("::std::vector<{}>", self.owned(inner)), @@ -244,7 +245,9 @@ impl<'a> Header<'a> { Type::Unit => unreachable!("no value is unit"), Type::Bool | Type::Int(_) | Type::F32 | Type::F64 => e.to_string(), Type::Char => format!("static_cast({e})"), - Type::Duration | Type::SocketAddr(_) | Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => e.to_string(), + Type::U128 | Type::Duration | Type::SocketAddr(_) | Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => { + e.to_string() + } Type::Waker => format!("{d}::give({d}::access::raw({e}))"), Type::List(_, inner) => { let x = format!("x{}_", lists(inner)); @@ -305,7 +308,9 @@ impl<'a> Header<'a> { match ty { Type::Bool | Type::Int(_) | Type::F32 | Type::F64 => e.to_string(), Type::Char => format!("static_cast({e})"), - Type::Duration | Type::SocketAddr(_) | Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => e.to_string(), + Type::U128 | Type::Duration | Type::SocketAddr(_) | Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => { + e.to_string() + } Type::List(_, inner) => { let c = format!("c{}_", lists(inner)); format!( @@ -420,6 +425,7 @@ impl<'a> Header<'a> { upper = p.to_uppercase() )); let mut aliases = vec![ + ("u128".to_string(), format!("{p}_u128")), ("duration".to_string(), format!("{p}_duration")), ("socket_addr".to_string(), format!("{p}_socket_addr")), ]; diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index 006d467dc..9a3203405 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -219,6 +219,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result Gen<'a> { Type::F32 | Type::F64 => "double".to_string(), Type::Char | Type::Str | Type::String | Type::Path | Type::PathBuf => "String".to_string(), Type::Slice | Type::Vec | Type::Bytes => "typed.Uint8List".to_string(), + Type::U128 => "BigInt".to_string(), Type::Duration => "Duration".to_string(), Type::SocketAddr(_) => "SocketAddr".to_string(), Type::List(_, inner) => format!("List<{}>", self.dt(inner)), @@ -492,7 +494,7 @@ impl<'a> Gen<'a> { /// Whether `dart:ffi` passes a C type as a Dart scalar, not a struct. fn scalar(ty: &Ty) -> bool { - !matches!(ty, Ty::Buf | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::Waker) + !matches!(ty, Ty::Buf | Ty::U128 | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::Waker) } /// The class of a C struct, defined once. @@ -500,6 +502,7 @@ impl<'a> Gen<'a> { let abi = self.abi; let name = match ty { Ty::Buf => return "_Buf".to_string(), + Ty::U128 => return "_U128".to_string(), Ty::Duration => return "_Duration".to_string(), Ty::SocketAddr => return "_Addr".to_string(), Ty::Waker => return "_Waker".to_string(), @@ -741,6 +744,7 @@ impl<'a> Gen<'a> { match ty.base() { Type::Str | Type::String | Type::Path | Type::PathBuf => format!("a_.string({dst}, {e});"), Type::Slice | Type::Vec | Type::Bytes => format!("a_.bytes({dst}, {e});"), + Type::U128 => format!("_n.u128({dst}, {e});"), Type::Duration => format!("_n.duration({dst}, {e});"), Type::SocketAddr(family) => { let family = match family { @@ -1014,6 +1018,7 @@ impl<'a> Gen<'a> { Type::Slice | Type::Vec | Type::Bytes => format!("_n.bytes({e})"), Type::Bool | Type::F32 | Type::F64 | Type::Int(_) => e.to_string(), Type::Char => format!("_n.scalar({e})"), + Type::U128 => format!("_n.u128Of({e})"), Type::Duration => format!("_n.durationOf({e})"), Type::SocketAddr(_) => format!("_n.addrOf({e})"), Type::Option(inner) => match inner.base() { diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 28d2e68ea..51336bedb 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -54,6 +54,7 @@ pub fn rs(ty: &Ty) -> String { Ty::F64 => "f64".to_string(), Ty::Char => "u32".to_string(), Ty::Buf => "rt::Buf".to_string(), + Ty::U128 => "rt::U128".to_string(), Ty::Duration => "rt::Duration".to_string(), Ty::SocketAddr => "rt::SocketAddr".to_string(), Ty::Struct(name) => name.clone(), @@ -327,6 +328,7 @@ impl Gen<'_> { Type::Unit => "()".to_string(), Type::Bool => "bool".to_string(), Type::Int(int) => int.rust().to_string(), + Type::U128 => "u128".to_string(), Type::F32 => "f32".to_string(), Type::F64 => "f64".to_string(), Type::Char => "char".to_string(), @@ -372,6 +374,7 @@ impl Gen<'_> { Type::Slice | Type::Vec => format!("unsafe {{ {e}.bytes(w) }}.to_vec()"), Type::Bytes => format!("::bytes::Bytes::copy_from_slice(unsafe {{ {e}.bytes(w) }})"), Type::Path | Type::PathBuf => format!("unsafe {{ {e}.path(w) }}.to_path_buf()"), + Type::U128 => format!("{e}.lift()"), Type::Duration | Type::SocketAddr(Family::Any) => format!("{e}.lift(w)"), Type::SocketAddr(Family::V4) => format!("{e}.lift_v4(w)"), Type::SocketAddr(Family::V6) => format!("{e}.lift_v6(w)"), @@ -481,6 +484,7 @@ impl Gen<'_> { Type::Bytes => format!("rt::Buf::owned({e})"), Type::Path => format!("rt::Buf::path_buf({e}.to_path_buf())"), Type::PathBuf => format!("rt::Buf::path_buf({e})"), + Type::U128 => format!("rt::U128::lower({e})"), Type::Duration => format!("rt::Duration::lower({e})"), Type::SocketAddr(_) => format!("rt::SocketAddr::lower(::std::net::SocketAddr::from({e}))"), Type::List(_, inner) => format!( diff --git a/ffi/src/go.rs b/ffi/src/go.rs index c0b949900..afb1710f9 100644 --- a/ffi/src/go.rs +++ b/ffi/src/go.rs @@ -44,6 +44,7 @@ const CONVERSIONS: &str = include_str!("../runtime/ffi_cgo.go"); /// The standard library's packages the generated items name. const STD: &[(&str, &str)] = &[ + ("big", "math/big"), ("context", "context"), ("cgo", "runtime/cgo"), ("netip", "net/netip"), @@ -411,9 +412,9 @@ impl<'a> Gen<'a> { "// Code generated by rust2-ffi. DO NOT EDIT.\n\n\ package {name}\n\n\ /*\n#cgo CFLAGS: -I${{SRCDIR}}/../../include\n#include \n#include \"{p}.h\"\n*/\nimport \"C\"\n\n\ - import (\n\t\"context\"\n\t\"math\"\n\t\"net\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"runtime/cgo\"\n\t\"strconv\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"{rt}\"\n)\n\n\ + import (\n\t\"context\"\n\t\"math\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"runtime/cgo\"\n\t\"strconv\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"{rt}\"\n)\n\n\ // The runtime's names for the export's C types and functions.\n\ - type (\n\tcBuf = C.{p}_buf\n\tcWaker = C.{p}_waker\n\tcDuration = C.{p}_duration\n\tcAddr = C.{p}_socket_addr\n)\n\n\ + type (\n\tcBuf = C.{p}_buf\n\tcWaker = C.{p}_waker\n\tcU128 = C.{p}_u128\n\tcDuration = C.{p}_duration\n\tcAddr = C.{p}_socket_addr\n)\n\n\ var (\n\tgoCall = (*[0]byte)(rt.GoCall_())\n\tgoRelease = (*[0]byte)(rt.GoRelease_())\n)\n\n\ func cBufFree(buf cBuf) {{\n\tC.{p}_buf_free(buf)\n}}\n\n" ); @@ -536,6 +537,7 @@ impl<'a> Gen<'a> { Type::Char => "rune".to_string(), Type::Str | Type::String | Type::Path | Type::PathBuf => "string".to_string(), Type::Slice | Type::Vec | Type::Bytes => self.qualify(Packages::RT, "Bytes"), + Type::U128 => "*big.Int".to_string(), Type::Duration => "time.Duration".to_string(), Type::SocketAddr(_) => "netip.AddrPort".to_string(), Type::List(_, inner) => format!("[]{}", self.go(inner)), @@ -611,6 +613,7 @@ impl<'a> Gen<'a> { Ty::F64 => "C.double".to_string(), Ty::Char => "C.uint32_t".to_string(), Ty::Buf => "cBuf".to_string(), + Ty::U128 => "cU128".to_string(), Ty::Duration => "cDuration".to_string(), Ty::SocketAddr => "cAddr".to_string(), Ty::Struct(n) | Ty::Enum(n) => format!("C.{n}"), @@ -689,6 +692,7 @@ impl<'a> Gen<'a> { Type::F32 => format!("C.float({e})"), Type::F64 => format!("C.double({e})"), Type::Char => format!("C.uint32_t({e})"), + Type::U128 => format!("lowerU128_({e})"), Type::Duration => format!("lowerDuration_({e})"), Type::SocketAddr(Family::Any) => format!("lowerAddr_({e})"), Type::SocketAddr(Family::V4) => format!("lowerAddrV4_({e})"), @@ -816,6 +820,7 @@ impl<'a> Gen<'a> { Type::F32 => format!("float32({e})"), Type::F64 => format!("float64({e})"), Type::Char => format!("rune({e})"), + Type::U128 => format!("liftU128_({e})"), Type::Duration => format!("liftDuration_({e})"), Type::SocketAddr(_) => format!("liftAddr_({e})"), Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => { @@ -2311,6 +2316,7 @@ fn local(name: &str) -> String { "strconv", "sync", "atomic", + "big", "time", "unsafe", "self", diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index ed4d07d7d..cc9079293 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -276,6 +276,8 @@ pub enum Type { Unit, Bool, Int(Int), + /// `u128`, which C passes as its two `u64` halves. + U128, F32, F64, Char, diff --git a/ffi/src/kotlin.rs b/ffi/src/kotlin.rs index f63eb7845..9808bb509 100644 --- a/ffi/src/kotlin.rs +++ b/ffi/src/kotlin.rs @@ -368,6 +368,7 @@ impl<'a> Gen<'a> { Type::Str | Type::String => "String".to_string(), Type::Slice | Type::Vec | Type::Bytes => "Bytes".to_string(), Type::Path | Type::PathBuf => "String".to_string(), + Type::U128 => "java.math.BigInteger".to_string(), Type::Duration => "kotlin.time.Duration".to_string(), Type::SocketAddr(_) => "java.net.InetSocketAddress".to_string(), Type::List(_, inner) => format!("kotlin.collections.List<{}>", self.kt(inner)), @@ -420,6 +421,7 @@ impl<'a> Gen<'a> { Type::Char => format!("{w}.i32({e})"), Type::Str | Type::String | Type::Path | Type::PathBuf => format!("{w}.string({e})"), Type::Slice | Type::Vec | Type::Bytes => format!("{w}.bytes({e})"), + Type::U128 => format!("{w}.u128({e})"), Type::Duration => format!("{w}.duration({e})"), Type::SocketAddr(Family::Any) => format!("{w}.socketAddr({e})"), Type::SocketAddr(Family::V4) => format!("{w}.socketAddrV4({e})"), @@ -486,6 +488,7 @@ impl<'a> Gen<'a> { Type::Path | Type::PathBuf => format!("{r}.path()!!"), Type::Slice | Type::Vec | Type::Bytes if owned => format!("{r}.owned()!!"), Type::Slice | Type::Vec | Type::Bytes => format!("{r}.bytes()!!"), + Type::U128 => format!("{r}.u128()"), Type::Duration => format!("{r}.duration()"), Type::SocketAddr(_) => format!("{r}.socketAddr()"), Type::List(_, inner) => { @@ -640,7 +643,7 @@ impl<'a> Gen<'a> { "crate::{name} {{ vtable: &VTABLE_{name}, data: rt::global(env, {p}) }}" )); } - Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::In(_) => { + Ty::U128 | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::In(_) => { let w = self.fresh("w"); // Taken by value, each object in a list moves out, as one passed alone does. if moved(abi, ty) { @@ -1894,7 +1897,7 @@ fn jni_type(c: &Ty, ty: &Type) -> (&'static str, &'static str) { Ty::Handle { .. } | Ty::Future(_) => ("jlong", "Long"), Ty::Buf => ("jbyteArray", "ByteArray?"), Ty::Struct(_) if matches!(ty.base(), Type::Callback(..)) => ("jobject", "Any"), - Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::In(_) => ("jbyteArray", "ByteArray"), + Ty::U128 | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::In(_) => ("jbyteArray", "ByteArray"), Ty::Fn(..) => ("jobject", "Any"), Ty::Waker => ("jobject", "Waker"), Ty::Poll | Ty::Out(_) | Ty::Data => unreachable!("{c:?} never crosses JNI"), @@ -1963,6 +1966,7 @@ pub(crate) fn rust_c(abi: &Abi<'_>, c: &Ty) -> String { Ty::Char => "u32".to_string(), Ty::Enum(_) => "i32".to_string(), Ty::Buf => "crate::rt::Buf".to_string(), + Ty::U128 => "crate::rt::U128".to_string(), Ty::Duration => "crate::rt::Duration".to_string(), Ty::SocketAddr => "crate::rt::SocketAddr".to_string(), Ty::Struct(name) => format!("crate::{name}"), diff --git a/ffi/src/python.rs b/ffi/src/python.rs index d51b75ad4..af4d3b167 100644 --- a/ffi/src/python.rs +++ b/ffi/src/python.rs @@ -400,7 +400,7 @@ impl<'a> Gen<'a> { match ty { Type::Unit => "None".to_string(), Type::Bool => "bool".to_string(), - Type::Int(_) => "int".to_string(), + Type::Int(_) | Type::U128 => "int".to_string(), Type::F32 | Type::F64 => "float".to_string(), Type::Char | Type::Str | Type::String | Type::Path | Type::PathBuf => "str".to_string(), Type::Slice | Type::Vec | Type::Bytes => "memoryview".to_string(), @@ -478,6 +478,7 @@ impl<'a> Gen<'a> { Type::Str | Type::String => format!("rt::str_in({o})"), Type::Slice | Type::Vec | Type::Bytes => format!("rt::bytes_in({o}, s)"), Type::Path | Type::PathBuf => format!("rt::path_in({o}, s)"), + Type::U128 => format!("rt::u128_in({o})"), Type::Duration => format!("rt::duration_in({o})"), Type::SocketAddr(family) => format!("rt::addr_in({o}, {})", family_of(*family)), Type::List(..) => format!("r_{}({o}, s, {m})", self.c_struct(ty)), @@ -542,6 +543,7 @@ impl<'a> Gen<'a> { Type::Str | Type::String => format!("rt::str_out({v})"), Type::Slice | Type::Vec | Type::Bytes => format!("rt::bytes_out({v})"), Type::Path | Type::PathBuf => format!("rt::path_out({v})"), + Type::U128 => format!("rt::u128_out({v})"), Type::Duration => format!("rt::duration_out({v})"), Type::SocketAddr(_) => format!("rt::addr_out({v})"), Type::List(..) => format!("w_{}({v})", self.c_struct(ty)), diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs index da4d491f9..142528283 100644 --- a/ffi/src/swift.rs +++ b/ffi/src/swift.rs @@ -68,6 +68,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result Swift.Bool {{\n\t\t{p}_contract() == 0x{contract:016x} && {upper}_CONTRACT == 0x{contract:016x}\n\t}}\n\n\ @@ -236,6 +237,7 @@ impl<'a> Gen<'a> { Type::Char => "Swift.Unicode.Scalar".to_string(), Type::Str | Type::String | Type::Path | Type::PathBuf => "Swift.String".to_string(), Type::Slice | Type::Vec | Type::Bytes => "Data".to_string(), + Type::U128 => "U128".to_string(), Type::Duration => "Swift.Duration".to_string(), Type::SocketAddr(_) => "SocketAddr".to_string(), Type::List(_, inner) => format!("[{}]", self.sw(inner)), @@ -272,6 +274,7 @@ impl<'a> Gen<'a> { Ty::F64 => "Swift.Double".to_string(), Ty::Char => "Swift.UInt32".to_string(), Ty::Buf => abi.rt("buf"), + Ty::U128 => abi.rt("u128"), Ty::Duration => abi.rt("duration"), Ty::SocketAddr => abi.rt("socket_addr"), Ty::Struct(n) | Ty::Enum(n) => n.clone(), @@ -327,6 +330,7 @@ impl<'a> Gen<'a> { Type::Bool | Type::F32 | Type::F64 => e.to_string(), Type::Int(int) => lower_int(*int, e), Type::Char => format!("{e}.value"), + Type::U128 => format!("Native.u128({e})"), Type::Duration => format!("Native.duration({e})"), Type::SocketAddr(_) => format!("Native.socketAddr({e})"), Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => format!("{e}._lower()"), @@ -384,6 +388,7 @@ impl<'a> Gen<'a> { Type::Bool | Type::F32 | Type::F64 => e.to_string(), Type::Int(int) => lift_int(*int, e), Type::Char => format!("Native.scalar({e})"), + Type::U128 => format!("Native.u128({e})"), Type::Duration => format!("Native.duration({e})"), Type::SocketAddr(_) => format!("Native.socketAddr({e})"), Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => format!("{}._lift({e})", self.sw(ty)), diff --git a/ffi/tests/api.c b/ffi/tests/api.c index 3a7dc0e67..9b2ead5e2 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -432,6 +432,20 @@ static int script(void) { printf("total %llu\n", (unsigned long long)api_total((api_range_u64){.start = 2, .end = 5})); printf("reach %u\n", (unsigned)api_reach(2, 5)); + api_option_u128 scaled[] = { + api_scaled((api_u128){.lo = UINT64_MAX, .hi = UINT64_MAX}, 1), + api_scaled((api_u128){.lo = 1, .hi = 1}, 1ULL << 63), + api_scaled((api_u128){.hi = 1ULL << 63}, 2), + }; + printf("scaled"); + for (size_t i = 0; i < 3; i++) { + if (scaled[i].some) { + printf(" %016llx%016llx", (unsigned long long)scaled[i].value.hi, (unsigned long long)scaled[i].value.lo); + } else { + printf(" none"); + } + } + printf("\n"); api_range_u64 wide = api_widen((api_range_u64){.start = 2, .end = 5}, 1); printf("widen %llu %llu\n", (unsigned long long)wide.start, (unsigned long long)wide.end); api_bounds_u64 ranges[] = { diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index fe19e427d..da0191f02 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -354,6 +354,15 @@ int script() { std::printf("total %llu\n", ull(api::total({2, 5}))); std::printf("reach %u\n", unsigned(api::reach(2, 5))); + std::printf("scaled"); + for (auto v : {api::scaled({UINT64_MAX, UINT64_MAX}, 1), api::scaled({1, 1}, 1ULL << 63), api::scaled({0, 1ULL << 63}, 2)}) { + if (v) { + std::printf(" %016llx%016llx", ull(v->hi), ull(v->lo)); + } else { + std::printf(" none"); + } + } + std::printf("\n"); api::range_u64 wide = api::widen({2, 5}, 1); std::printf("widen %llu %llu\n", ull(wide.start), ull(wide.end)); api::bounds_u64 ranges[] = { diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 3f536ff41..3da0a1bb7 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -239,6 +239,12 @@ Future script() async { print('total ${api.total(const api.RangeU64(2, 5))}'); print('reach ${api.reach(2, 5)}'); + final scaled = [ + api.scaled((BigInt.one << 128) - BigInt.one, 1), + api.scaled((BigInt.one << 64) + BigInt.one, 1 << 63), + api.scaled(BigInt.one << 127, 2), + ]; + print('scaled ${scaled.map((v) => v?.toRadixString(16).padLeft(32, '0') ?? 'none').join(' ')}'); final wide = api.widen(const api.RangeU64(2, 5), 1); print('widen ${wide.start} ${wide.end}'); final counts = [ @@ -860,6 +866,8 @@ void bounds() { throws(() => api.nextChar(String.fromCharCode(0xd800)), 'a low surrogate'); throws(() => api.nextChar(String.fromCharCode(0xdfff)), 'a high surrogate'); check(api.nextChar('\u{d7ff}') == null, 'the scalar before the surrogates'); + throws(() => api.scaled(BigInt.one << 128, 1), 'an int past u128'); + throws(() => api.scaled(-BigInt.one, 1), 'a negative int for a u128'); // Dart's longest Duration is 9223372036854 seconds and 775807 microseconds. for (final us in [9223372036854000000, 9223372036854775806]) { diff --git a/ffi/tests/api.go b/ffi/tests/api.go index d4d37fc10..5cc52c786 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "math" + "math/big" "net/netip" "os" "runtime" @@ -301,6 +302,21 @@ func script() { fmt.Printf("total %d\n", api.Total(rt.RangeU64{Start: 2, End: 5})) fmt.Printf("reach %d\n", api.Reach(2, 5)) + one := big.NewInt(1) + pow := func(n uint) *big.Int { return new(big.Int).Lsh(one, n) } + fmt.Print("scaled") + for _, v := range []*big.Int{ + api.Scaled(new(big.Int).Sub(pow(128), one), 1), + api.Scaled(new(big.Int).Add(pow(64), one), 1<<63), + api.Scaled(pow(127), 2), + } { + if v == nil { + fmt.Print(" none") + } else { + fmt.Printf(" %032x", v) + } + } + fmt.Println() wide := api.Widen(rt.RangeU64{Start: 2, End: 5}, 1) fmt.Printf("widen %d %d\n", wide.Start, wide.End) included := func(n uint64) rt.BoundU64 { return rt.BoundU64{Kind: rt.BoundIncluded, Value: n} } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index be754f9d7..1282195a7 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -166,6 +166,12 @@ fun script() { println("total ${total(RangeU64(2u, 5u))}") println("reach ${reach(2u, 5u)}") + val two = java.math.BigInteger.TWO + val scaled = listOf(scaled(two.pow(128) - java.math.BigInteger.ONE, 1u), scaled(two.pow(64) + java.math.BigInteger.ONE, 1uL shl 63), scaled(two.pow(127), 2u)) + println("scaled " + scaled.joinToString(" ") { it?.toString(16)?.padStart(32, '0') ?: "none" }) + for (v in listOf(two.pow(128), java.math.BigInteger.ONE.negate())) { + check(runCatching { scaled(v, 1u) }.exceptionOrNull() is IllegalArgumentException, "$v is no u128") + } val wide = widen(RangeU64(2u, 5u), 1u) println("widen ${wide.start} ${wide.end}") val counts = listOf( diff --git a/ffi/tests/api.py b/ffi/tests/api.py index 584311a6d..10ce630e5 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -232,6 +232,8 @@ def edit(counter): print(f"total {api.total(range(2, 5))}") print(f"reach {api.reach(2, 5)}") + scaled = [api.scaled(2**128 - 1, 1), api.scaled(2**64 + 1, 2**63), api.scaled(2**127, 2)] + print("scaled " + " ".join("none" if v is None else f"{v:032x}" for v in scaled)) wide = api.widen(range(2, 5), 1) print(f"widen {wide.start} {wide.stop}") inc, exc, unb = api.Bound.INCLUDED, api.Bound.EXCLUDED, api.Bound.UNBOUNDED @@ -758,6 +760,8 @@ def raises(error, f, what): raises(OverflowError, lambda: api.widths(2**64), "an int past u64 raises") raises(OverflowError, lambda: api.widths(-1), "a negative int for a u64 raises") raises(TypeError, lambda: api.title(1), "an int is no bool") + raises(OverflowError, lambda: api.scaled(2**128, 1), "an int past u128 raises") + raises(OverflowError, lambda: api.scaled(-1, 1), "a negative int for a u128 raises") # A borrowed tuple is one C value, not one out per element. check(api.corner(True) == (1, 1), "a borrowed tuple returns whole") diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index c66aec94e..cdeee1e80 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -280,6 +280,13 @@ func script() { print("total \(total(RangeU64(start: 2, end: 5)))") print("reach \(reach(2, 5))") + let scaledHex = [ + scaled(U128(high: .max, low: .max), 1), scaled(U128(high: 1, low: 1), 1 << 63), scaled(U128(high: 1 << 63, low: 0), 2), + ].map { v in + v.map { v in [v.high, v.low].map { h in Swift.String(repeating: "0", count: 16 - Swift.String(h, radix: 16).count) + Swift.String(h, radix: 16) }.joined() } ?? "none" + } + print("scaled \(scaledHex.joined(separator: " "))") + check(U128(high: .max, low: .max).description == "340282366920938463463374607431768211455", "a u128's decimal digits") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") let counts = [ From 9299d34bd75ab14b3750b1d08ea5c42c8673a7c5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 01:44:42 -0800 Subject: [PATCH 04/12] fix(ffi): u128's Python overflow, and move the empty enum to the api census Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 16 +++++---- ffi/fixtures/api/src/lib.rs | 4 +++ ffi/fixtures/reject/src/lib.rs | 3 -- ffi/runtime/python.rs | 2 +- ffi/tests/api.go | 8 ++--- ffi/tests/fixtures.rs | 1 + quest/m1/ffi-export/refusals/README.md | 2 +- quest/m1/ffi-export/refusals/small-shapes.md | 35 ++++++++++++-------- 8 files changed, 42 insertions(+), 29 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 44247bed9..4a4e26211 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -75,6 +75,7 @@ re-exports its items instead, which export. A type no path reaches is refused. | `async fn`, or a return that implements `Future` | a future: `f` makes it, `f_poll` polls it, `f_future_free` drops it; a function that is not `async` runs at the call, as in Rust | | `poll_x`, when `x` exports as a future | left out: the future is its poll | | `bool`, integers but 128-bit, floats, `char` | `bool`, `` types, `float`, `double`, a `uint32_t` code point | +| `u128` | `_u128`: its low and high 64 bits | | `&str`, `String`, `&[u8]`, `Vec`, `bytes::Bytes` | `_buf`: a view passed in, an owned buffer returned | | `&Path`, `PathBuf` | `_buf` of the path's bytes as the OS holds them on Unix, which need not be UTF-8, else UTF-8 | | `Duration` | `_duration` | @@ -232,7 +233,7 @@ exceptions and RTTI on or off: it throws nothing and reads no type information. from. From C++20, either converts to `std::span`. A `view` or span lives only as long as the bytes it views, and a `view` lent to a callback only for the call; reading one past that is C++'s own undefined behavior. A `Vec` is a `std::vector`, of - `view`s for bytes passed in, and a socket address `socket_addr`, the C struct. `Option` is `std::optional`, a + `view`s for bytes passed in, a socket address `socket_addr`, the C struct, and a `u128` `u128`, the C struct. `Option` is `std::optional`, a tuple `std::tuple`, and `Result` a `result`, whose `value()` aborts on an error. - **Traits** are abstract classes, passed as a `std::unique_ptr` that Rust deletes. @@ -290,7 +291,7 @@ in as a `ByteArray`, bytes returned as their owner, and anything else encoded in Kotlin's for a record passed in. A `Bytes` equals and hashes by its contents, wherever each keeps them, so a record holding bytes does too; Rust's lent ones are copied across in one call to compare. Strings are `String`, `Option` `T?`, a tuple `Pair`, - `Triple`, or else a generated `Tuple`, though a tuple of one returned whole is its element, `Duration` `kotlin.time.Duration`, and an `Err` a thrown `Exception` + `Triple`, or else a generated `Tuple`, though a tuple of one returned whole is its element, `u128` `java.math.BigInteger`, which throws `IllegalArgumentException` passed in out of its range, `Duration` `kotlin.time.Duration`, and an `Err` a thrown `Exception` holding it, whose message is its `Display`. - A **path** is a `String`: one Rust returns that is not UTF-8 throws. A `SocketAddr`, `SocketAddrV4`, or `SocketAddrV6` is a `java.net.InetSocketAddress`, which must be @@ -347,7 +348,7 @@ caller imports it with no ignores. place, and others, which Python may change while Rust reads, are copied. Bytes Rust returns, or a record holds, are a read-only `memoryview` lending Rust's buffer until it and its views are released. Those Rust lends a closure or trait method are copied into - `bytes`. Strings are `str`, `Option` `T | None`, a tuple a `tuple`, `Duration` + `bytes`. Strings are `str`, `u128` an `int`, `Option` `T | None`, a tuple a `tuple`, `Duration` `datetime.timedelta`, truncated to the microsecond, with `timedelta.max` for any longer and for `Duration::MAX`, a `Range` a `range`, and an `Err` a raised `Exception` holding it in `error`, whose `str` is its `Display`. @@ -404,7 +405,8 @@ in either language mode. another value, they are copied in. Bytes Rust returns lend its buffer until Swift releases the `Data`, though `Data` copies a few bytes inline, freeing Rust's at once. Strings are `String`, copied, `char` `Unicode.Scalar`, `usize` `UInt`, `Option` `T?`, - a tuple a Swift tuple, though a tuple of one is its element, and `Duration` + a tuple a Swift tuple, though a tuple of one is its element, `u128` a `U128` of its + `high` and `low` halves until the deployment target allows `UInt128`, and `Duration` `Swift.Duration`. An `Err` is thrown: an exported type as itself, which conforms to `Swift.Error`, and any other in a generated `Error` holding it. - A **path** is a `String`: one Rust returns that is not UTF-8 traps. A `SocketAddr`, @@ -494,7 +496,8 @@ reads a go.mod. freed at the call. A path is a `string` of the OS's bytes, which need not be UTF-8. `Option` is `*T`, or for an object its pointer, nil for `None`. A tuple is `rt.Tuple2[A, B]` and so on, but a tuple of one is its - element, and one returned whole is the function's results. `Duration` is + element, and one returned whole is the function's results. A `u128` is a `*big.Int`, which + panics passed in out of its range. `Duration` is `time.Duration`: a negative one passed in, or one Rust returns past its 292 years, panics. A socket address is a `netip.AddrPort`, whose zone names an IPv6 scope by its number or its interface's name; one Rust returns with an IPv6 flow label, which @@ -562,7 +565,8 @@ the library's own URI, mapped to the library by a build hook or by `--native-ass returns is copied into a `String`. A path is a `String`, and one Rust returns that is not UTF-8 aborts, since a throw would leave the rest of a list or record being lifted unfreed. `Option` is `T?`, a tuple a Dart record, and a `Vec` or `&[T]` a - `List`. `Duration` is `Duration`, truncated to the microsecond: a negative one passed + `List`. A `u128` is a `BigInt`, which throws an `ArgumentError` passed in out of + its range. `Duration` is `Duration`, truncated to the microsecond: a negative one passed in throws an `ArgumentError`, and Rust's longest is Dart's. A socket address is a `SocketAddr` of an `InternetAddress`, a port, and IPv6's flow label and scope; one passed in of the wrong family throws an `ArgumentError`, and a port, label, or scope out of its diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 6272fb32c..412444353 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -985,6 +985,10 @@ pub fn walk(steps: u64, mut visit: impl FnMut(&Cursor) -> u64) -> u64 { (0..steps).map(|at| visit(&Cursor { at })).sum() } +/// An enum without variants: no value is one, so C never holds one, as an object no export +/// returns, and the bindings leave it out. +pub enum Void {} + /// An object no export returns but its own method, so C never holds one: left out of the /// bindings, with what takes it, and listed in the census. pub struct Lease(u8); diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 0ee0cbf5f..a44f704a5 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -688,6 +688,3 @@ pub use ::dep as renamed; /// A crate whole, which the config excludes, so nothing refuses it. pub use ::dep as Shed; - -/// No value is one, so C never holds a handle to it. -pub enum Void {} diff --git a/ffi/runtime/python.rs b/ffi/runtime/python.rs index a01a44f60..e780e9116 100644 --- a/ffi/runtime/python.rs +++ b/ffi/runtime/python.rs @@ -600,7 +600,7 @@ pub fn u128_in(obj: *mut PyObject) -> R { let hi = PyLong_AsUnsignedLongLong(high.ptr()); if hi == u64::MAX && !PyErr_Occurred().is_null() { PyErr_Clear(); - return overflow_int::(); + return overflow("int out of range for Rust's u128"); } let lo = PyLong_AsUnsignedLongLongMask(index.ptr()); if lo == u64::MAX && !PyErr_Occurred().is_null() { diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 5cc52c786..84158f175 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -302,12 +302,12 @@ func script() { fmt.Printf("total %d\n", api.Total(rt.RangeU64{Start: 2, End: 5})) fmt.Printf("reach %d\n", api.Reach(2, 5)) - one := big.NewInt(1) - pow := func(n uint) *big.Int { return new(big.Int).Lsh(one, n) } + bigOne := big.NewInt(1) + pow := func(n uint) *big.Int { return new(big.Int).Lsh(bigOne, n) } fmt.Print("scaled") for _, v := range []*big.Int{ - api.Scaled(new(big.Int).Sub(pow(128), one), 1), - api.Scaled(new(big.Int).Add(pow(64), one), 1<<63), + api.Scaled(new(big.Int).Sub(pow(128), bigOne), 1), + api.Scaled(new(big.Int).Add(pow(64), bigOne), 1<<63), api.Scaled(pow(127), 2), } { if v == nil { diff --git a/ffi/tests/fixtures.rs b/ffi/tests/fixtures.rs index f1fb9c580..4a51d18a4 100644 --- a/ffi/tests/fixtures.rs +++ b/ffi/tests/fixtures.rs @@ -1126,6 +1126,7 @@ fn relative_target() { let text = std::fs::read_to_string(census.join("api-fixture.md")).unwrap(); for line in [ "- `Lease`: object no export returns", + "- `Void`: object no export returns", "- `release`: type C never holds `Lease`", "- `Unnamed`: generic type no export instantiates", ] { diff --git a/quest/m1/ffi-export/refusals/README.md b/quest/m1/ffi-export/refusals/README.md index 6a7f0ff65..6d4d9945b 100644 --- a/quest/m1/ffi-export/refusals/README.md +++ b/quest/m1/ffi-export/refusals/README.md @@ -68,5 +68,5 @@ Public API: the exporter's command line and the C API of every item that exports - [Standard and common types export by role](/quest/m1/ffi-export/refusals/std-types.md) - `Filter`, serde_json's map iterators, `Instant`, and tracing's levels, once their maintainer decisions land - [Poll and Context export](/quest/m1/ffi-export/refusals/poll.md) - a `poll_*` with no async twin takes a waker, as futures' polls do - [Borrow shapes export](/quest/m1/ffi-export/refusals/borrows.md) - `Option<&T>`, `&mut` buffers, `&Bytes`, `Arc`, `'static` borrows, and borrowed returns that are not `Clone` -- [Small shapes export](/quest/m1/ffi-export/refusals/small-shapes.md) - `u128`, arrays, const generics, unit and `Result` off a return, `!`, empty enums, and what is not `Send` +- [Small shapes export](/quest/m1/ffi-export/refusals/small-shapes.md) - const generics, once generics lands - [Bump the moq pin past its re-export and reachability fixes](/quest/m1/ffi-export/refusals/moq-pin.md) - the census has no whole-crate re-export refusal and no unreachable type of moq's own diff --git a/quest/m1/ffi-export/refusals/small-shapes.md b/quest/m1/ffi-export/refusals/small-shapes.md index 508693728..b680bb4ed 100644 --- a/quest/m1/ffi-export/refusals/small-shapes.md +++ b/quest/m1/ffi-export/refusals/small-shapes.md @@ -1,4 +1,4 @@ -# [M] Small shapes export +# [S] Small shapes export ## Goal @@ -10,28 +10,35 @@ the objects and futures that are not `Send`, which ## Plan -Each is its own small mapping; what has no C shape, such as an object that is not `Send` -under the C ABI's any-thread calls, keeps its refusal and goes to the maintainer as moq's -signature to change. +Each is its own small mapping. Changing moq is a non-goal: what has no C shape yet gets +one here or in the quest that owns it, never a request for moq to change its signature. + +Decided with the maintainer on 2026-11-09: + +- ✅ `u128` maps by rule as `_u128`, two `uint64_t` halves in C, with each + language's exact big integer above it: Python `int`, Kotlin `BigInteger`, Dart `BigInt`, + Go `*big.Int`, Swift a `U128` struct until its deployment target allows `UInt128`, and + C++ the C struct. Rejected: leaving it out. Clears `Timestamp::as_scale`, `as_millis`, + `as_micros`, and `as_nanos`. `i128` stays refused: no consumer, and its sign is a + conversion of its own in each language. +- ✅ Not `Send` (`clock::Entered`, `clock::Rng`, `catalog::Guard`) belongs to + [not-send](/quest/m1/ffi-export/not-send.md)'s thread check. +- ✅ Const generics land here once [generics](/quest/m1/ffi-export/refusals/generics.md) + does, since both edit instance inference. Rejected: a separate quest. ✅ A `()` parameter is passed by nothing in C, clearing `container::Producer::set`'s four -instances, and an enum without variants is an object, which no export returns, so it leaves -with [the unholdable rule](/quest/m1/ffi-export/refusals/unholdable.md). +instances, and an enum without variants is an object no export returns, which the bindings +leave out. Left at moq `ffcb7cd9`, outside the moq-tokio items narrowing the stand-in drops (the arrays, `Result` and `!` off a return, and `worker::Group::finished`'s future): -- `u128`: `Timestamp::as_scale`, `as_millis`, `as_micros`, `as_nanos`. std returns it too - (`Duration::as_nanos`), so it maps by rule: proposed as `_u128`, two `uint64_t` - halves in C, and each language's arbitrary-precision integer above it. - const generics: `group::Producer::write_frames` and `group::Consumer::read_frames` over `frame::Buffer`. The IR keeps only type arguments; a const argument in `Type`, inferred at its default as a type parameter's is, exports `Buffer<8>` and the methods at it. -- not `Send`: `clock::Entered` and `clock::Rng` pin a thread, and `catalog::Guard` holds a - `MutexGuard`; no C shape, so moq's signatures change or they stay refused. -## Waiting +## Required -- Maintainer: choose the `u128` mapping, and whether moq changes or keeps refused - `Entered`, `Rng`, and `Guard`; check the PR's decisions are answered +- [Generics](/quest/m1/ffi-export/refusals/generics.md) - instance inference, which const + arguments extend From 51ff40d93e88ec6b428faf7dfe54436cf53477ec Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 02:06:08 -0800 Subject: [PATCH 05/12] fix(ffi): Go's Option is a nil-able *big.Int, and format Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 4 ++-- ffi/fixtures/api/examples/transcript.rs | 7 ++++++- ffi/src/dart.rs | 5 ++++- ffi/src/go.rs | 18 ++++++++++++------ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 4a4e26211..cef047c10 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -494,8 +494,8 @@ reads a go.mod. returns freed memory, such as [mimalloc](https://crates.io/crates/mimalloc), declared in the exported crate, holds it near what is lent. `Copy()` copies it out. Text Rust returns is copied once into a `string`, and Rust's freed at the call. A path is a `string` - of the OS's bytes, which need not be UTF-8. `Option` is `*T`, or for an object its - pointer, nil for `None`. A tuple is `rt.Tuple2[A, B]` and so on, but a tuple of one is its + of the OS's bytes, which need not be UTF-8. `Option` is `*T`, or for an object or + `u128` its pointer, nil for `None`. A tuple is `rt.Tuple2[A, B]` and so on, but a tuple of one is its element, and one returned whole is the function's results. A `u128` is a `*big.Int`, which panics passed in out of its range. `Duration` is `time.Duration`: a negative one passed in, or one Rust returns past its 292 years, diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index 4ab18147c..bd4f03e30 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -197,7 +197,12 @@ fn main() { println!("total {}", api::total(2..5)); println!("reach {}", api::reach(2, (), 5)); let scaled = |v, by| api::scaled(v, by).map_or("none".to_string(), |v| format!("{v:032x}")); - println!("scaled {} {} {}", scaled(u128::MAX, 1), scaled((1 << 64) + 1, 1 << 63), scaled(1 << 127, 2)); + println!( + "scaled {} {} {}", + scaled(u128::MAX, 1), + scaled((1 << 64) + 1, 1 << 63), + scaled(1 << 127, 2) + ); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); println!( diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index 9a3203405..41f265963 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -494,7 +494,10 @@ impl<'a> Gen<'a> { /// Whether `dart:ffi` passes a C type as a Dart scalar, not a struct. fn scalar(ty: &Ty) -> bool { - !matches!(ty, Ty::Buf | Ty::U128 | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::Waker) + !matches!( + ty, + Ty::Buf | Ty::U128 | Ty::Duration | Ty::SocketAddr | Ty::Struct(_) | Ty::Waker + ) } /// The class of a C struct, defined once. diff --git a/ffi/src/go.rs b/ffi/src/go.rs index afb1710f9..f9be0c737 100644 --- a/ffi/src/go.rs +++ b/ffi/src/go.rs @@ -544,7 +544,7 @@ impl<'a> Gen<'a> { Type::Waker => "func()".to_string(), Type::Option(inner) => match inner.base() { // Already a pointer, nil for none. - Type::Object(_) => self.go(inner), + Type::Object(_) | Type::U128 => self.go(inner), _ => format!("*{}", self.go(inner)), }, Type::Box(inner) | Type::Ref(_, inner) => self.go(inner), @@ -731,11 +731,14 @@ impl<'a> Gen<'a> { let arena = self.arena(inner); let inner = (**inner).clone(); self.helper(&format!("lower_{c}"), |g| { - let go = g.go(&inner); - let value = g.lower(&inner, "*v"); + // A Go pointer already, which nil is none of. + let (go, value) = match inner.base() { + Type::U128 => (g.go(&inner), g.lower(&inner, "v")), + _ => (format!("*{}", g.go(&inner)), g.lower(&inner, "*v")), + }; let param = g.arena_param(&inner); format!( - "func lower_{c}({param}v *{go}) C.{c} {{\n\tif v == nil {{\n\t\treturn C.{c}{{}}\n\t}}\n\treturn C.{c}{{some: true, value: {value}}}\n}}\n\n" + "func lower_{c}({param}v {go}) C.{c} {{\n\tif v == nil {{\n\t\treturn C.{c}{{}}\n\t}}\n\treturn C.{c}{{some: true, value: {value}}}\n}}\n\n" ) }); match arena { @@ -855,10 +858,13 @@ impl<'a> Gen<'a> { let c = abi.option(inner); let inner = (**inner).clone(); self.helper(&format!("lift_{c}"), |g| { - let go = g.go(&inner); let value = g.lift(&inner, "c.value"); + let (go, v) = match inner.base() { + Type::U128 => (g.go(&inner), "v"), + _ => (format!("*{}", g.go(&inner)), "&v"), + }; format!( - "func lift_{c}(c C.{c}) *{go} {{\n\tif !c.some {{\n\t\treturn nil\n\t}}\n\tv := {value}\n\treturn &v\n}}\n\n" + "func lift_{c}(c C.{c}) {go} {{\n\tif !c.some {{\n\t\treturn nil\n\t}}\n\tv := {value}\n\treturn {v}\n}}\n\n" ) }); format!("lift_{c}({e})") From bde3fee49ce63a1423da62087f6ce28c1b88ac5d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 11:14:20 -0800 Subject: [PATCH 06/12] feat(ffi): export const generics at their inferred values A const parameter of a generic type takes its default, as a type parameter does, and a function's takes each value an instance it names has there. The IR carries a const argument as Type::Const; a C name appends it, as api_drain_4. Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 26 +-- ffi/fixtures/api/examples/transcript.rs | 12 ++ ffi/fixtures/api/src/lib.rs | 35 ++++ ffi/fixtures/reject/expected.txt | 5 +- ffi/fixtures/reject/src/lib.rs | 8 +- ffi/src/abi.rs | 6 +- ffi/src/collect.rs | 227 +++++++++++++++++++----- ffi/src/cpp.rs | 2 + ffi/src/dart.rs | 1 + ffi/src/glue.rs | 7 +- ffi/src/go.rs | 2 + ffi/src/ir.rs | 26 ++- ffi/src/kotlin.rs | 1 + ffi/src/python.rs | 3 + ffi/src/swift.rs | 1 + ffi/tests/api.c | 15 ++ ffi/tests/api.cpp | 15 ++ ffi/tests/api.dart | 7 + ffi/tests/api.go | 15 ++ ffi/tests/api.kt | 8 + ffi/tests/api.py | 5 + ffi/tests/api.swift | 5 + 22 files changed, 364 insertions(+), 68 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 2143b4b57..d32a44d4e 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -99,17 +99,19 @@ re-exports its items instead, which export. A type no path reaches is refused. | `impl IntoIterator`, where `X` is no type parameter | `Vec`, or `&[Y]` where `X` is `&Y` | | `impl io::Read` | `&[u8]` | | another type parameter | `serde_json::Value` for a serde `Serialize` or `Deserialize` bound, else the first of `String`, `Bytes`, and `Vec` meeting its bounds, or a foreign implementation of its one exported trait; else an instance of the function at each type the exports implement a trait bounding it for, through a blanket impl at each type implementing what the impl requires, as `&X` where that meets its bounds too and no bound, its own or one a supertrait implies, makes it outlive the call, so C keeps its handle, and, as an argument of a generic type, at each argument of an instance that meets its bounds and makes each generic type the signature names an instance | +| a const parameter of a function | an instance of the function at each value an instance of a generic type the signature passes it to has there, where each generic type the signature then names is an instance | +| a const argument | a `bool` or an integer below 128 bits; any other is refused | A generic type infers the instances it exports at, with no config: the one at its -defaults, if each type parameter has one; the one each inherent impl is of, such as -`impl Pair`; each choice, for each parameter, among its default and the types the -exports implement a trait bounding it for, one of the exports', which are `()`, a -primitive, `String`, or an exported type or an instance of one, such as -`impl Seed for Wrap`; and each instance a signature, `pub` field, or constant -names at any choice its bounds allow, whether or not the rest of it maps, which may name -more. Inference finishes before any type is mapped, so mapping never adds an instance. -An instance that holds an instance of a type inside it, such as `Cell>`, is -refused, as it could name ever deeper ones. +defaults, if each type and const parameter has one, as `frame::Buffer` at `N = 8`; the +one each inherent impl is of, such as `impl Pair`; each choice, for each parameter, +among its default and the types the exports implement a trait bounding it for, one of +the exports', which are `()`, a primitive, `String`, or an exported type or an instance +of one, such as `impl Seed for Wrap`; and each instance a signature, `pub` +field, or constant names at any choice its bounds allow, whether or not the rest of it +maps, which may name more. Inference finishes before any type is mapped, so mapping +never adds an instance. An instance that holds an instance of a type inside it, such as +`Cell>`, is refused, as it could name ever deeper ones. A record or enum holds only values and `Copy` handles: one with a field that does not map, or that holds itself, is an object instead, and the census lists it. A borrowed return is @@ -190,8 +192,10 @@ Everything else is refused, and the census counts why. `Point`, and `api_Counter_seeded_polar_Point` at `polar::Point`. The names depend on every instance the item exports at, so one an upstream change adds can lengthen another's name. A type instance's methods are named through it, and a function instance appends - its arguments' names, a reference's by what it borrows. An `exclude` of a type or method - by the generic type's path leaves it out of every instance, and by the instance's path + its arguments' names, a reference's by what it borrows and a const's by its value: the + api fixture's `drain` is `api_drain_4` at `Batch` and `api_drain_2` at `Batch<2>`. An + `exclude` of a type or method by the generic type's path leaves it out of every + instance, and by the instance's path (`"mux::catalog::Producer::stats"`) out of that one. ## C++ diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index 7c3e5af0f..0ffb52280 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -203,6 +203,18 @@ fn main() { scaled((1 << 64) + 1, 1 << 63), scaled(1 << 127, 2) ); + let mut batch: api::Batch = api::Batch::new(); + let accepted = (1..=5).filter(|&v| batch.push(v)).count(); + let mut two = api::pair(); + let paired = (1..=3).filter(|&v| two.push(v)).count(); + let emptied = api::drain(&mut batch); + println!( + "batch {} {accepted} {emptied} {} {} {paired} {}", + batch.capacity(), + api::drain(&mut batch), + two.capacity(), + api::drain(&mut two) + ); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); println!( diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 0c864f67f..cbd923a0a 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -419,6 +419,41 @@ pub fn scaled(v: u128, by: u64) -> Option { v.checked_mul(by.into()) } +/// Up to `N` numbers, as moq's `frame::Buffer` holds up to `N` frames: a const parameter +/// with a default, which exports at it and at each other value a signature names. +pub struct Batch(Vec); + +impl Batch { + /// An empty batch. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Append `v`, unless the batch already holds `N`. + pub fn push(&mut self, v: u32) -> bool { + let room = self.0.len() < N; + if room { + self.0.push(v); + } + room + } + + /// How many numbers it holds at most. + pub fn capacity(&self) -> usize { + N + } +} + +/// A batch of two, an instance at a const argument other than the default. +pub fn pair() -> Batch<2> { + Batch::new() +} + +/// The sum of a batch's numbers, draining it: a const parameter at each instance's value. +pub fn drain(batch: &mut Batch) -> u64 { + batch.0.drain(..).map(u64::from).sum() +} + /// A range, widened by `by` at each end. pub fn widen(range: Range, by: u64) -> Range { range.start.saturating_sub(by)..range.end + by diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index bf0642ded..ae5499420 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -14,10 +14,11 @@ fixtures/reject/src/lib.rs:177:1: rust2-ffi: cannot export instance `Grow<(u32,) fixtures/reject/src/lib.rs:180:2: rust2-ffi: cannot export method `Grow::grow`: refused type `reject::Grow<(u32,)>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:191:1: rust2-ffi: cannot export instance `Shell<(u32,)>`: type argument `(u32,)`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:200:1: rust2-ffi: cannot export fn `shell`: refused type `reject::Shell<(u32,)>`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:211:1: rust2-ffi: cannot export fn `probe`: const generic; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:211:1: rust2-ffi: cannot export fn `probe`: const generic `N`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:216:1: rust2-ffi: cannot export fn `excluded_cell`: excluded type `reject::Cell`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:221:1: rust2-ffi: cannot export fn `listed`: refused type `reject::Cell>`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:229:1: rust2-ffi: cannot export fn `fixed`: const generic `reject::Fixed`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:226:1: rust2-ffi: cannot export instance `Fixed<'a'>`: const argument `'a'`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:229:1: rust2-ffi: cannot export fn `fixed`: refused type `reject::Fixed<'a'>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:234:9: rust2-ffi: cannot export use `HashSet`: re-export of the standard library's item `std`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:237:1: rust2-ffi: cannot export fn `lend`: Waker outside a parameter taken by value; exclude it in the config to leave it out fixtures/reject/src/lib.rs:23:1: rust2-ffi: cannot export fn `borrowing`: borrowing future from a function that is not async; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 4f2156def..1bcb151be 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -205,7 +205,7 @@ pub fn shell() -> Shell<(u32,)> { } /// A const generic function whose bound names an instance nothing else does: refused at -/// its const parameter, before any candidate for the other is mapped. +/// its const parameter, which nothing names, before any candidate for the other is mapped. pub struct Probe(T); pub fn probe(x: impl Into>) { @@ -222,11 +222,11 @@ pub fn listed(cell: Cell>) -> usize { cell.0.len() } -/// A const-generic type, whose const argument the IR cannot carry. -pub struct Fixed(pub u8); +/// A type whose const argument is a `char`, which only a `bool` or integer may be. +pub struct Fixed(pub u8); /// An instance of it, which a signature names. -pub fn fixed() -> Fixed<2> { +pub fn fixed() -> Fixed<'a'> { Fixed(2) } diff --git a/ffi/src/abi.rs b/ffi/src/abi.rs index d2610ca06..0f1c3dd0c 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -14,7 +14,7 @@ use std::collections::{BTreeMap, HashMap}; use rustc_span::Span; -use crate::ir::{Access, Api, Dyn, Function, Home, Int, Receiver, Type}; +use crate::ir::{Access, Api, Dyn, Function, Home, Int, Literal, Receiver, Type}; /// A C name claimed twice: the name, then what claimed it first and second. pub type Clash = (String, (String, Span), (String, Span)); @@ -372,6 +372,9 @@ impl<'a> Abi<'a> { Type::Bound(int) => format!("bound_{}", int.rust()), Type::Bounds(int) => format!("bounds_{}", int.rust()), Type::Ref(_, inner) | Type::Box(inner) => self.key(inner), + // Only an instance's name holds one, where a C identifier has no `-`. + Type::Const(Literal::Int(_, v)) if *v < 0 => format!("neg{}", v.unsigned_abs()), + Type::Const(value) => value.to_string(), other => unreachable!("no option struct holds {other:?}"), } } @@ -408,6 +411,7 @@ impl<'a> Abi<'a> { /// The C type of `ty` as a value: a parameter, field, or what a function returns. pub fn ty(&self, ty: &Type) -> Ty { match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => Ty::Void, Type::Bool => Ty::Bool, Type::Int(int) => Ty::Int(*int), diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 056162df7..a4855b08e 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -137,7 +137,7 @@ struct Site<'tcx> { key: Key<'tcx>, /// Its generic arguments as exported, each lifetime `'static`. args: ty::GenericArgsRef<'tcx>, - /// How many of its type arguments it is named by: each up to the last that is not its + /// How many of its type and const arguments it is named by: each up to the last that is not its /// default, as Rust spells it. explicit: usize, /// Its item's path, which names its members in the IR. @@ -153,8 +153,9 @@ struct Instantiation<'tcx> { /// The type parameters that stay parameters, each taking a lent closure or a foreign /// implementation, which typing maps. kept: Vec>, - /// The arguments of the type parameters that export at several types, which name it. - named: Vec>, + /// The arguments of the type parameters that export at several types, and of the const + /// parameters, which name it. + named: Vec>, } struct Collector<'tcx> { @@ -551,12 +552,12 @@ impl<'tcx> Collector<'tcx> { .skip_norm_wip() } - /// How many of `def_id`'s type arguments `args` names it by, as Rust spells it: each up - /// to the last that is not its default. + /// How many of `def_id`'s type and const arguments `args` names it by, as Rust spells + /// it: each up to the last that is not its default. fn explicit(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>) -> usize { let tcx = self.tcx; let params: Vec<&ty::GenericParamDef> = (tcx.generics_of(def_id).own_params.iter()) - .filter(|p| matches!(p.kind, ty::GenericParamDefKind::Type { .. })) + .filter(|p| !matches!(p.kind, ty::GenericParamDefKind::Lifetime)) .collect(); let mut explicit = params.len(); while let Some(param) = explicit.checked_sub(1).map(|i| params[i]) { @@ -574,6 +575,38 @@ impl<'tcx> Collector<'tcx> { explicit } + /// The first `explicit` type and const arguments of `args`, which name an instance. + fn shown_args(args: ty::GenericArgsRef<'tcx>, explicit: usize) -> Vec> { + let named = args.iter().filter(|arg| !matches!(arg.kind(), GenericArgKind::Lifetime(_))); + named.take(explicit).collect() + } + + /// A type or const argument as reports spell it. + fn spell_arg(&self, arg: ty::GenericArg<'tcx>) -> String { + match arg.kind() { + GenericArgKind::Type(ty) => self.spell(ty), + GenericArgKind::Const(ct) => self.const_arg(ct).map_or_else(|_| ct.to_string(), |c| c.to_string()), + GenericArgKind::Lifetime(region) => region.to_string(), + } + } + + /// A const argument's value, which must be a `bool` or an integer that is not 128 bits. + fn const_arg(&self, ct: ty::Const<'tcx>) -> Result { + let tcx = self.tcx; + let refused = || why("const argument", ct.to_string()); + let ty::ConstKind::Value(value) = ct.kind() else { + return Err(refused()); + }; + let scalar = value.try_to_leaf().ok_or_else(refused)?; + let bits = scalar.to_bits(scalar.size()); + match value.ty.kind() { + ty::Bool => Ok(Literal::Bool(bits != 0)), + _ => int(value.ty) + .map(|int| Literal::Int(int, signed(tcx, value.ty, bits))) + .ok_or_else(refused), + } + } + /// A type as reports spell it, each export's by its path in the IR, and an instance by /// the arguments that name it. fn spell(&self, ty: Ty<'tcx>) -> String { @@ -587,7 +620,9 @@ impl<'tcx> Collector<'tcx> { None => tcx.def_path_str(adt.did()), }; let explicit = self.explicit(adt.did(), args); - let args: Vec = args.types().take(explicit).map(|ty| self.spell(ty)).collect(); + let args: Vec = (Self::shown_args(args, explicit).into_iter()) + .map(|arg| self.spell_arg(arg)) + .collect(); match args.is_empty() { true => path, false => format!("{path}<{}>", args.join(", ")), @@ -628,7 +663,10 @@ impl<'tcx> Collector<'tcx> { .flat_map(|choice| { let options: Vec> = match param.kind { ty::GenericParamDefKind::Lifetime => vec![tcx.lifetimes.re_static.into()], - ty::GenericParamDefKind::Const { .. } => Vec::new(), + ty::GenericParamDefKind::Const { .. } => (param.default_value(tcx)) + .map(|default| default.instantiate(tcx, &choice[..]).skip_norm_wip()) + .into_iter() + .collect(), ty::GenericParamDefKind::Type { .. } => (param.default_value(tcx)) .map(|default| default.instantiate(tcx, &choice[..]).skip_norm_wip()) .into_iter() @@ -724,12 +762,19 @@ impl<'tcx> Collector<'tcx> { named } - /// The first type argument an instance is named by that cannot map as [`Self::arg`] - /// requires, which is `()`, a primitive, `String`, `Bytes`, `Duration`, a foreign object, - /// or an export's type. - fn unarguable(&self, site: &Site<'tcx>) -> Option> { + /// Why the first argument an instance is named by cannot map as [`Self::arg`] requires: + /// a type that is not `()`, a primitive, `String`, `Bytes`, `Duration`, a foreign + /// object, or an export's type, or a const that is not a `bool` or integer. + fn unarguable(&self, site: &Site<'tcx>) -> Option { let tcx = self.tcx; - site.args.types().take(site.explicit).find(|&ty| match ty.kind() { + let mut args = Self::shown_args(site.args, site.explicit).into_iter(); + args.find_map(|arg| { + let ty = match arg.kind() { + GenericArgKind::Const(ct) => return self.const_arg(ct).err(), + GenericArgKind::Type(ty) => ty, + GenericArgKind::Lifetime(_) => return None, + }; + let unarguable = match ty.kind() { ty::Tuple(list) => !list.is_empty(), ty::Bool | ty::Char | ty::Float(_) => false, ty::Int(_) | ty::Uint(_) => int(ty).is_none(), @@ -739,8 +784,10 @@ impl<'tcx> Collector<'tcx> { && !tcx.is_diagnostic_item(rustc_span::Symbol::intern("Duration"), adt.did()) && !self.exported(adt.did().krate) && !self.foreign.iter().any(|&(ty, _)| ty == adt.did()) - } - _ => true, + } + _ => true, + }; + unarguable.then(|| why("type argument", self.spell(ty))) }) } @@ -769,7 +816,12 @@ impl<'tcx> Collector<'tcx> { fn shown(&mut self, shown: &Path, instance: &Instantiation<'tcx>) -> Option { let mut shown = shown.clone(); if !instance.named.is_empty() { - let spelled: Vec = instance.named.iter().map(|&ty| self.spell(ty.peel_refs())).collect(); + let spelled: Vec = (instance.named.iter()) + .map(|&arg| match arg.kind() { + GenericArgKind::Type(ty) => self.spell(ty.peel_refs()), + _ => self.spell_arg(arg), + }) + .collect(); *shown.last_mut().expect("a path has a segment") += &format!("<{}>", spelled.join(", ")); if self.exclude(&shown) { return None; @@ -883,10 +935,6 @@ impl<'tcx> Collector<'tcx> { Err(why("instance holding an instance of a type inside it", spelled)), )); } - // The IR keeps only an instance's type arguments, which would lose a const one. - if args.consts().next().is_some() { - return Some((key, Err(why("const generic", spelled)))); - } let (infcx, env) = tcx.infer_ctxt().build_with_typing_env(TypingEnv::fully_monomorphized()); for clause in tcx.clauses_of(def_id).instantiate(tcx, args).clauses { let clause = clause.skip_norm_wip(); @@ -898,7 +946,9 @@ impl<'tcx> Collector<'tcx> { let explicit = self.explicit(def_id, args); let mut shown = path.clone(); if explicit > 0 { - let spelled: Vec = args.types().take(explicit).map(|ty| self.spell(ty)).collect(); + let spelled: Vec = (Self::shown_args(args, explicit).into_iter()) + .map(|arg| self.spell_arg(arg)) + .collect(); let last = shown.last_mut().expect("a path has a segment"); *last = format!("{last}<{}>", spelled.join(", ")); } @@ -1034,7 +1084,7 @@ impl<'tcx> Collector<'tcx> { } for site in self.sites(def_id, &path) { let decl = match self.unarguable(&site) { - Some(ty) => Decl::Refused(why("type argument", self.spell(ty))), + Some(why) => Decl::Refused(why), None => self.declare_site(&site), }; self.decls.insert(site.key, decl); @@ -1293,10 +1343,15 @@ impl<'tcx> Collector<'tcx> { } } - /// The type arguments an instance is named by, each of which must export as a value or + /// The arguments an instance is named by, each type of which must export as a value or /// a handle: Rust fills in the defaults after them, which may be any type. fn args(&self, site: &Site<'tcx>) -> Result, Why> { - site.args.types().take(site.explicit).map(|ty| self.arg(ty)).collect() + (Self::shown_args(site.args, site.explicit).into_iter()) + .map(|arg| match arg.kind() { + GenericArgKind::Const(ct) => self.const_arg(ct).map(Type::Const), + _ => self.arg(arg.expect_ty()), + }) + .collect() } /// A type argument that names an instance: a value or a handle. @@ -2010,29 +2065,42 @@ impl<'tcx> Collector<'tcx> { *self.impls.borrow_mut() = impls; // What the glue names a declared type parameter as, which a call cannot always // infer; an `impl Trait` one cannot be named, and is inferred from its argument. - let mut arg = |ty: Ty<'tcx>| { - let mapped = match *ty.kind() { - ty::Ref(_, inner, ty::Mutability::Not) => { - self.arg(inner).map(|inner| Type::Ref(Mutability::Not, Box::new(inner))) - } - _ => self.arg(ty), + let mut arg = |arg: ty::GenericArg<'tcx>| { + let mapped = match arg.kind() { + GenericArgKind::Const(ct) => self.const_arg(ct).map(Type::Const), + GenericArgKind::Type(ty) => match *ty.kind() { + ty::Ref(_, inner, ty::Mutability::Not) => { + self.arg(inner).map(|inner| Type::Ref(Mutability::Not, Box::new(inner))) + } + _ => self.arg(ty), + }, + GenericArgKind::Lifetime(_) => unreachable!("no lifetime names an instance"), }; mapped.map_err(|why| errs.push(why)).ok() }; let instance = match instance.named.is_empty() { true => None, false => { - let declared = (tcx.generics_of(def_id).own_params.iter()) - .filter(|p| matches!(p.kind, ty::GenericParamDefKind::Type { synthetic: false, .. })); + let declared = (tcx.generics_of(def_id).own_params.iter()).filter(|p| { + matches!( + p.kind, + ty::GenericParamDefKind::Type { synthetic: false, .. } | ty::GenericParamDefKind::Const { .. } + ) + }); // A kept parameter is left for Rust to infer from its argument. let args = declared - .map(|p| args.type_at(p.index as usize)) - .filter_map(|ty| match instance.kept.contains(&ty) { + .map(|p| args[p.index as usize]) + .filter_map(|a| match a.as_type().is_some_and(|ty| instance.kept.contains(&ty)) { true => Some(None), - false => arg(ty).map(Some), + false => arg(a).map(Some), + }) + .collect(); + let named = (instance.named.iter()) + .filter_map(|&a| match a.kind() { + GenericArgKind::Type(ty) => arg(ty.peel_refs().into()), + _ => arg(a), }) .collect(); - let named = instance.named.iter().filter_map(|&ty| arg(ty.peel_refs())).collect(); Some(crate::ir::Instance { args, named }) } }; @@ -2199,14 +2267,29 @@ impl<'tcx> Collector<'tcx> { } let base = args.clone(); let mut kept = Vec::new(); - // Each choice of the parameters so far, with the types that name it. - let mut choices = vec![(args, Vec::new())]; + // Each choice of the parameters so far, with the arguments that name it. + let mut choices: Vec<(Vec>, Vec>)> = vec![(args, Vec::new())]; for (i, arg) in identity.iter().enumerate() { let param = match arg.kind() { _ if i < count => continue, GenericArgKind::Lifetime(_) => continue, - // The first refusal ends it, before any later parameter's candidates are mapped. - GenericArgKind::Const(_) => return Err(vec![why("const generic", "")]), + GenericArgKind::Const(param) => { + let next: Vec<_> = (choices.into_iter()) + .flat_map(|(args, named)| { + (self.each_const(def_id, param, &args).into_iter()).map(move |value| { + let mut args = args.clone(); + args[i] = value.into(); + (args, [&named[..], &[value.into()]].concat()) + }) + }) + .collect(); + // The first refusal ends it, before any later parameter's candidates are mapped. + if next.is_empty() { + return Err(vec![why("const generic", param.to_string())]); + } + choices = next; + continue; + } GenericArgKind::Type(param) if self.lent(def_id, param) => { kept.push(param); continue; @@ -2262,7 +2345,7 @@ impl<'tcx> Collector<'tcx> { for ty in each { let mut args = args.clone(); args[i] = ty.into(); - next.push((args, [&named[..], &[ty]].concat())); + next.push((args, [&named[..], &[ty.into()]].concat())); } } // A choice no type of this parameter meets is dropped; with none left, the first @@ -2724,6 +2807,51 @@ impl<'tcx> Collector<'tcx> { found } + /// The values a const parameter exports at: as an argument of a generic type the + /// signature names, each value an instance of it has there, where each generic type the + /// signature then names is an instance. + fn each_const(&self, def_id: DefId, param: ty::Const<'tcx>, args: &[ty::GenericArg<'tcx>]) -> Vec> { + let tcx = self.tcx; + let exported = |ty: Ty<'tcx>| match ty.kind() { + ty::Adt(adt, args) if self.exported(adt.did().krate) && self.typed(adt.did()) => Some((adt.did(), args)), + _ => None, + }; + let spelled = |args| self.spelled(def_id, args); + let ty::ConstKind::Param(p) = param.kind() else { + unreachable!("a generic const argument is a parameter"); + }; + let mut found: Vec> = Vec::new(); + for (adt, at) in spelled(tcx.mk_args(args)).into_iter().filter_map(exported) { + let Some(j) = at.iter().position(|arg| arg == param.into()) else { + continue; + }; + for site in self.instances.iter().filter(|site| site.key.def_id == adt) { + let Some(value) = site.args[j].as_const() else { + continue; + }; + if found.contains(&value) { + continue; + } + let mut args = args.to_vec(); + args[p.index as usize] = value.into(); + let named = spelled(tcx.mk_args(&args)).into_iter().filter_map(exported).all(|(adt, args)| { + let key = self.key(adt, args); + match self.inferring { + true => self.instances.iter().any(|site| site.key == key), + false => matches!( + self.decls.get(&key), + Some(Decl::Record(_) | Decl::Enum(_) | Decl::Object(_)) + ), + } + }); + if named { + found.push(value); + } + } + } + found + } + /// Whether an `Fn` trait bounds a type parameter, which then takes a lent closure. fn lent(&self, def_id: DefId, param: Ty<'tcx>) -> bool { let tcx = self.tcx; @@ -3974,8 +4102,21 @@ fn matches<'tcx>(tcx: TyCtxt<'tcx>, pattern: Ty<'tcx>, ty: Ty<'tcx>, args: &mut } } (ty::Adt(a, pattern_args), ty::Adt(b, ty_args)) if a.did() == b.did() => { - (pattern_args.iter().zip(ty_args.iter())).all(|(p, t)| match (p.as_type(), t.as_type()) { - (Some(p), Some(t)) => matches(tcx, p, t, args), + (pattern_args.iter().zip(ty_args.iter())).all(|(p, t)| match (p.kind(), t.kind()) { + (GenericArgKind::Type(p), GenericArgKind::Type(t)) => matches(tcx, p, t, args), + (GenericArgKind::Const(p), GenericArgKind::Const(t)) => match p.kind() { + ty::ConstKind::Param(param) => { + let slot = &mut args[param.index as usize]; + match slot.as_const() == Some(p) { + true => { + *slot = t.into(); + true + } + false => slot.as_const() == Some(t), + } + } + _ => p == t, + }, _ => true, }) } diff --git a/ffi/src/cpp.rs b/ffi/src/cpp.rs index ccc93b862..a3472c5dd 100644 --- a/ffi/src/cpp.rs +++ b/ffi/src/cpp.rs @@ -100,6 +100,7 @@ impl<'a> Header<'a> { fn owned(&self, ty: &Type) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "void".to_string(), Type::Bool => "bool".to_string(), Type::Int(int) => cpp_int(*int).to_string(), @@ -242,6 +243,7 @@ impl<'a> Header<'a> { return format!("{d}::access::raw(::{ns}::view({e}))"); } match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => unreachable!("no value is unit"), Type::Bool | Type::Int(_) | Type::F32 | Type::F64 => e.to_string(), Type::Char => format!("static_cast({e})"), diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index 41f265963..f580ed68f 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -415,6 +415,7 @@ impl<'a> Gen<'a> { fn dt(&mut self, ty: &Type) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "void".to_string(), Type::Bool => "bool".to_string(), Type::Int(_) => "int".to_string(), diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index b757b0eed..5d3ccd484 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -10,7 +10,7 @@ use std::path::Path; use crate::abi::{self, Abi, Ty}; use crate::config::Package; -use crate::ir::{Access, Collection, Family, Function, Mutability, Receiver, Traits, Type}; +use crate::ir::{Access, Collection, Family, Function, Literal, Mutability, Receiver, Traits, Type}; const RUNTIME: &str = include_str!("../runtime/rt.rs"); @@ -372,6 +372,9 @@ impl Gen<'_> { format!("::{}", foreign(&path[0])) + &path[1..].iter().map(|s| format!("::{s}")).collect::() } Type::Object(i) => generic(&api.objects[*i].path, api.objects[*i].lifetimes, &api.objects[*i].args), + // A negative argument is an expression, which Rust takes only in braces. + Type::Const(Literal::Int(_, v)) if *v < 0 => format!("{{{v}}}"), + Type::Const(value) => value.to_string(), other => unreachable!("no callback or type argument is {other:?}"), } } @@ -379,6 +382,7 @@ impl Gen<'_> { /// The Rust value of type `ty` that the C value `e` holds, copied out of it. fn lift(&self, ty: &Type, e: &str) -> String { match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => e.to_string(), Type::Bool | Type::Int(_) | Type::F32 | Type::F64 => e.to_string(), Type::Char => format!("rt::char({e}, w)"), @@ -486,6 +490,7 @@ impl Gen<'_> { /// The C value of the Rust value `e` of type `ty`, which the caller frees. fn lower(&self, ty: &Type, e: &str) -> String { match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "()".to_string(), Type::Bool | Type::Int(_) | Type::F32 | Type::F64 => e.to_string(), Type::Char => format!("{e} as u32"), diff --git a/ffi/src/go.rs b/ffi/src/go.rs index f9be0c737..e40a46ae2 100644 --- a/ffi/src/go.rs +++ b/ffi/src/go.rs @@ -529,6 +529,7 @@ impl<'a> Gen<'a> { /// The Go type of `ty`, as a value, a parameter, or a result. fn go(&mut self, ty: &Type) -> String { match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => unreachable!("no value is unit"), Type::Bool => "bool".to_string(), Type::Int(int) => go_int(*int).to_string(), @@ -684,6 +685,7 @@ impl<'a> Gen<'a> { fn lower(&mut self, ty: &Type, e: &str) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Str | Type::String | Type::Path | Type::PathBuf => format!("a_.bufString({e})"), Type::Slice | Type::Vec | Type::Bytes => format!("a_.bufView({e})"), Type::Unit => unreachable!("no value is unit"), diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index 9c7d3851a..dfee44798 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -47,6 +47,7 @@ impl Api { Type::Record(i) => self.rust(&self.records[*i].path, &self.records[*i].args), Type::Enum(i) => self.rust(&self.enums[*i].path, &self.enums[*i].args), Type::Object(i) => self.rust(&self.objects[*i].path, &self.objects[*i].args), + Type::Const(value) => value.to_string(), other => unreachable!("no type argument is {other:?}"), } } @@ -76,7 +77,7 @@ pub struct Record { /// Its path with its instance's arguments, which the config names it by, such as /// `moq_mux::catalog::Producer`. pub shown: Path, - /// The type arguments of the instance it is, empty unless the type is generic. + /// The type and const arguments of the instance it is, empty unless the type is generic. pub args: Vec, pub span: Span, pub docs: String, @@ -207,12 +208,12 @@ pub struct Function { pub home: Home, } -/// An instance of a generic function or method, at one of the types a type parameter -/// exports at. +/// An instance of a generic function or method, at one of the arguments a type or const +/// parameter exports at. pub struct Instance { - /// The arguments of its declared type parameters, which the call names, as Rust - /// cannot infer one no argument names; its argument infers an `impl Trait` one, and - /// one left `None`, which takes a lent closure or a foreign implementation. + /// The arguments of its declared type and const parameters, which the call names, as + /// Rust cannot infer one no argument names; its argument infers an `impl Trait` one, + /// and one left `None`, which takes a lent closure or a foreign implementation. pub args: Vec>, /// The arguments that tell it from its other instances, which name it. pub named: Vec, @@ -267,11 +268,22 @@ pub struct Constant { pub value: Literal, } +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Literal { Bool(bool), Int(Int, i128), } +impl std::fmt::Display for Literal { + /// The value as Rust writes it. + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Literal::Bool(b) => write!(f, "{b}"), + Literal::Int(_, v) => write!(f, "{v}"), + } + } +} + /// A type as the API uses it, in the exact form the Rust side takes or returns. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum Type { @@ -326,6 +338,8 @@ pub enum Type { Bounds(Int), /// A tuple of one element or more. Tuple(Vec), + /// A const argument of an instance, such as the `8` of `Buffer<8>`. + Const(Literal), } /// The Rust collection a [`Type::List`] is, which Rust builds from its items in order. diff --git a/ffi/src/kotlin.rs b/ffi/src/kotlin.rs index 910056964..623ed99fb 100644 --- a/ffi/src/kotlin.rs +++ b/ffi/src/kotlin.rs @@ -359,6 +359,7 @@ impl<'a> Gen<'a> { fn kt(&mut self, ty: &Type) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "Unit".to_string(), Type::Bool => "Boolean".to_string(), Type::Int(int) => kt_int(*int).to_string(), diff --git a/ffi/src/python.rs b/ffi/src/python.rs index af4d3b167..07c48b42f 100644 --- a/ffi/src/python.rs +++ b/ffi/src/python.rs @@ -398,6 +398,7 @@ impl<'a> Gen<'a> { fn py_ty(&self, ty: &Type) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "None".to_string(), Type::Bool => "bool".to_string(), Type::Int(_) | Type::U128 => "int".to_string(), @@ -470,6 +471,7 @@ impl<'a> Gen<'a> { fn lift(&mut self, ty: &Type, o: &str, m: &str) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Bool => format!("rt::bool_in({o})"), Type::Int(int) => format!("rt::int_in::<{}>({o})", int.rust()), Type::F32 => format!("rt::float_in({o}).map(|f| f as f32)"), @@ -534,6 +536,7 @@ impl<'a> Gen<'a> { fn lower(&mut self, ty: &Type, v: &str) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "rt::none()".to_string(), Type::Bool => format!("rt::bool_out({v})"), Type::Int(int) if signed(*int) => format!("rt::int_out({v} as i64)"), diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs index 142528283..cef9e4603 100644 --- a/ffi/src/swift.rs +++ b/ffi/src/swift.rs @@ -229,6 +229,7 @@ impl<'a> Gen<'a> { fn sw(&self, ty: &Type) -> String { let abi = self.abi; match ty { + Type::Const(_) => unreachable!("a const argument is no value"), Type::Unit => "Void".to_string(), Type::Bool => "Swift.Bool".to_string(), Type::Int(int) => sw_int(*int).to_string(), diff --git a/ffi/tests/api.c b/ffi/tests/api.c index 8b44c5b33..f0d49e775 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -446,6 +446,21 @@ static int script(void) { } } printf("\n"); + api_Batch *batch = api_Batch_new(); + unsigned accepted = 0; + for (uint32_t v = 1; v <= 5; v++) { + accepted += api_Batch_push(batch, v); + } + api_Batch_2 *two = api_pair(); + unsigned paired = 0; + for (uint32_t v = 1; v <= 3; v++) { + paired += api_Batch_2_push(two, v); + } + unsigned long long emptied = api_drain_4(batch); + printf("batch %zu %u %llu %llu %zu %u %llu\n", api_Batch_capacity(batch), accepted, emptied, + (unsigned long long)api_drain_4(batch), api_Batch_2_capacity(two), paired, (unsigned long long)api_drain_2(two)); + api_Batch_2_free(two); + api_Batch_free(batch); api_range_u64 wide = api_widen((api_range_u64){.start = 2, .end = 5}, 1); printf("widen %llu %llu\n", (unsigned long long)wide.start, (unsigned long long)wide.end); api_bounds_u64 ranges[] = { diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 09deeb284..f690572a6 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -363,6 +363,21 @@ int script() { } } std::printf("\n"); + { + auto batch = api::Batch::new_(); + unsigned accepted = 0; + for (std::uint32_t v = 1; v <= 5; v++) { + accepted += batch.push(v); + } + auto two = api::pair(); + unsigned paired = 0; + for (std::uint32_t v = 1; v <= 3; v++) { + paired += two.push(v); + } + auto emptied = api::drain_4(batch); + std::printf("batch %zu %u %llu %llu %zu %u %llu\n", batch.capacity(), accepted, ull(emptied), ull(api::drain_4(batch)), + two.capacity(), paired, ull(api::drain_2(two))); + } api::range_u64 wide = api::widen({2, 5}, 1); std::printf("widen %llu %llu\n", ull(wide.start), ull(wide.end)); api::bounds_u64 ranges[] = { diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 8e0c009e3..7fa70c4a5 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -245,6 +245,13 @@ Future script() async { api.scaled(BigInt.one << 127, 2), ]; print('scaled ${scaled.map((v) => v?.toRadixString(16).padLeft(32, '0') ?? 'none').join(' ')}'); + final batch = api.Batch(), two = api.pair(); + final accepted = [for (var v = 1; v <= 5; v++) batch.push(v)].where((p) => p).length; + final paired = [for (var v = 1; v <= 3; v++) two.push(v)].where((p) => p).length; + final emptied = api.drain4(batch); + print('batch ${batch.capacity()} $accepted $emptied ${api.drain4(batch)} ${two.capacity()} $paired ${api.drain2(two)}'); + batch.close(); + two.close(); final wide = api.widen(const api.RangeU64(2, 5), 1); print('widen ${wide.start} ${wide.end}'); final counts = [ diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 0fbe6bfb5..080e2e132 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -317,6 +317,21 @@ func script() { } } fmt.Println() + batch, two := api.NewBatch(), api.Pair() + accepted, paired := 0, 0 + for v := uint32(1); v <= 5; v++ { + if must(batch.Push(v)) { + accepted++ + } + } + for v := uint32(1); v <= 3; v++ { + if must(two.Push(v)) { + paired++ + } + } + emptied := must(api.Drain4(batch)) + fmt.Printf("batch %d %d %d %d %d %d %d\n", must(batch.Capacity()), accepted, emptied, must(api.Drain4(batch)), + must(two.Capacity()), paired, must(api.Drain2(two))) wide := api.Widen(rt.RangeU64{Start: 2, End: 5}, 1) fmt.Printf("widen %d %d\n", wide.Start, wide.End) included := func(n uint64) rt.BoundU64 { return rt.BoundU64{Kind: rt.BoundIncluded, Value: n} } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 859f5c5c6..b355f5057 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -173,6 +173,14 @@ fun script() { for (v in listOf(two.pow(128), java.math.BigInteger.ONE.negate())) { check(runCatching { scaled(v, 1u) }.exceptionOrNull() is IllegalArgumentException, "$v is no u128") } + Batch.new().use { batch -> + pair().use { two -> + val accepted = (1u..5u).count { batch.push(it) } + val paired = (1u..3u).count { two.push(it) } + val emptied = drain4(batch) + println("batch ${batch.capacity()} $accepted $emptied ${drain4(batch)} ${two.capacity()} $paired ${drain2(two)}") + } + } val wide = widen(RangeU64(2u, 5u), 1u) println("widen ${wide.start} ${wide.end}") val counts = listOf( diff --git a/ffi/tests/api.py b/ffi/tests/api.py index 480487ed2..b86ec6ee5 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -234,6 +234,11 @@ def edit(counter): print(f"reach {api.reach(2, 5)}") scaled = [api.scaled(2**128 - 1, 1), api.scaled(2**64 + 1, 2**63), api.scaled(2**127, 2)] print("scaled " + " ".join("none" if v is None else f"{v:032x}" for v in scaled)) + batch, two = api.Batch.new(), api.pair() + accepted = sum(batch.push(v) for v in range(1, 6)) + paired = sum(two.push(v) for v in range(1, 4)) + emptied = api.drain_4(batch) + print(f"batch {batch.capacity()} {accepted} {emptied} {api.drain_4(batch)} {two.capacity()} {paired} {api.drain_2(two)}") wide = api.widen(range(2, 5), 1) print(f"widen {wide.start} {wide.stop}") inc, exc, unb = api.Bound.INCLUDED, api.Bound.EXCLUDED, api.Bound.UNBOUNDED diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 37bc7dbf1..96bfbb9cc 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -287,6 +287,11 @@ func script() { } print("scaled \(scaledHex.joined(separator: " "))") check(U128(high: .max, low: .max).description == "340282366920938463463374607431768211455", "a u128's decimal digits") + let batch = Batch.new(), two = pair() + let accepted = (1...5).filter { batch.push($0) }.count + let paired = (1...3).filter { two.push($0) }.count + let emptied = drain4(batch) + print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(batch)) \(two.capacity()) \(paired) \(drain2(two))") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") let counts = [ From c89f4729038a31600de7ad8182e532a6d4a4275a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 12:31:56 -0800 Subject: [PATCH 07/12] test(ffi): cover const generics in every language, and rewrite small-shapes to its array Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 8 ++++---- ffi/fixtures/reject/expected.txt | 5 +++-- ffi/tests/api.c | 8 ++++---- ffi/tests/api.cpp | 6 +++--- ffi/tests/api.dart | 8 ++++---- ffi/tests/api.go | 6 +++--- ffi/tests/api.kt | 6 +++--- ffi/tests/api.py | 6 +++--- ffi/tests/api.swift | 6 +++--- 9 files changed, 30 insertions(+), 29 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index b3e213960..728e04a61 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -215,15 +215,15 @@ fn main() { ); let mut batch: api::Batch = api::Batch::new(); let accepted = (1..=5).filter(|&v| batch.push(v)).count(); - let mut two = api::pair(); - let paired = (1..=3).filter(|&v| two.push(v)).count(); + let mut batch2 = api::pair(); + let paired = (1..=3).filter(|&v| batch2.push(v)).count(); let emptied = api::drain(&mut batch); println!( "batch {} {accepted} {emptied} {} {} {paired} {}", batch.capacity(), api::drain(&mut batch), - two.capacity(), - api::drain(&mut two) + batch2.capacity(), + api::drain(&mut batch2) ); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index c7e73464e..224de5f93 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -13,10 +13,11 @@ fixtures/reject/src/lib.rs:183:1: rust2-ffi: cannot export instance `Grow<(u32,) fixtures/reject/src/lib.rs:186:2: rust2-ffi: cannot export method `Grow::grow`: refused type `reject::Grow<(u32,)>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:197:1: rust2-ffi: cannot export instance `Shell<(u32,)>`: type argument `(u32,)`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:206:1: rust2-ffi: cannot export fn `shell`: refused type `reject::Shell<(u32,)>`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:217:1: rust2-ffi: cannot export fn `probe`: const generic; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:217:1: rust2-ffi: cannot export fn `probe`: const generic `N`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:222:1: rust2-ffi: cannot export fn `excluded_cell`: excluded type `reject::Cell`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:227:1: rust2-ffi: cannot export fn `listed`: refused type `reject::Cell>`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:235:1: rust2-ffi: cannot export fn `fixed`: const generic `reject::Fixed`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:232:1: rust2-ffi: cannot export instance `Fixed<'a'>`: const argument `'a'`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:235:1: rust2-ffi: cannot export fn `fixed`: refused type `reject::Fixed<'a'>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:23:1: rust2-ffi: cannot export fn `borrowing`: borrowing future from a function that is not async; exclude it in the config to leave it out fixtures/reject/src/lib.rs:240:9: rust2-ffi: cannot export use `HashSet`: re-export of the standard library's item `std`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:243:1: rust2-ffi: cannot export fn `lend`: Waker outside a parameter taken by value; exclude it in the config to leave it out diff --git a/ffi/tests/api.c b/ffi/tests/api.c index da3b753f2..348cc5f21 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -461,15 +461,15 @@ static int script(void) { for (uint32_t v = 1; v <= 5; v++) { accepted += api_Batch_push(batch, v); } - api_Batch_2 *two = api_pair(); + api_Batch_2 *batch2 = api_pair(); unsigned paired = 0; for (uint32_t v = 1; v <= 3; v++) { - paired += api_Batch_2_push(two, v); + paired += api_Batch_2_push(batch2, v); } unsigned long long emptied = api_drain_4(batch); printf("batch %zu %u %llu %llu %zu %u %llu\n", api_Batch_capacity(batch), accepted, emptied, - (unsigned long long)api_drain_4(batch), api_Batch_2_capacity(two), paired, (unsigned long long)api_drain_2(two)); - api_Batch_2_free(two); + (unsigned long long)api_drain_4(batch), api_Batch_2_capacity(batch2), paired, (unsigned long long)api_drain_2(batch2)); + api_Batch_2_free(batch2); api_Batch_free(batch); api_range_u64 wide = api_widen((api_range_u64){.start = 2, .end = 5}, 1); printf("widen %llu %llu\n", (unsigned long long)wide.start, (unsigned long long)wide.end); diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 8485d9720..8765f8a8c 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -373,14 +373,14 @@ int script() { for (std::uint32_t v = 1; v <= 5; v++) { accepted += batch.push(v); } - auto two = api::pair(); + auto batch2 = api::pair(); unsigned paired = 0; for (std::uint32_t v = 1; v <= 3; v++) { - paired += two.push(v); + paired += batch2.push(v); } auto emptied = api::drain_4(batch); std::printf("batch %zu %u %llu %llu %zu %u %llu\n", batch.capacity(), accepted, ull(emptied), ull(api::drain_4(batch)), - two.capacity(), paired, ull(api::drain_2(two))); + batch2.capacity(), paired, ull(api::drain_2(batch2))); } api::range_u64 wide = api::widen({2, 5}, 1); std::printf("widen %llu %llu\n", ull(wide.start), ull(wide.end)); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 5e930c079..a55bcaae5 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -247,13 +247,13 @@ Future script() async { api.scaled(BigInt.one << 127, 2), ]; print('scaled ${scaled.map((v) => v?.toRadixString(16).padLeft(32, '0') ?? 'none').join(' ')}'); - final batch = api.Batch(), two = api.pair(); + final batch = api.Batch(), batch2 = api.pair(); final accepted = [for (var v = 1; v <= 5; v++) batch.push(v)].where((p) => p).length; - final paired = [for (var v = 1; v <= 3; v++) two.push(v)].where((p) => p).length; + final paired = [for (var v = 1; v <= 3; v++) batch2.push(v)].where((p) => p).length; final emptied = api.drain4(batch); - print('batch ${batch.capacity()} $accepted $emptied ${api.drain4(batch)} ${two.capacity()} $paired ${api.drain2(two)}'); + print('batch ${batch.capacity()} $accepted $emptied ${api.drain4(batch)} ${batch2.capacity()} $paired ${api.drain2(batch2)}'); batch.close(); - two.close(); + batch2.close(); final wide = api.widen(const api.RangeU64(2, 5), 1); print('widen ${wide.start} ${wide.end}'); final counts = [ diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 88a734779..6e6ba998f 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -319,7 +319,7 @@ func script() { } } fmt.Println() - batch, two := api.NewBatch(), api.Pair() + batch, batch2 := api.NewBatch(), api.Pair() accepted, paired := 0, 0 for v := uint32(1); v <= 5; v++ { if must(batch.Push(v)) { @@ -327,13 +327,13 @@ func script() { } } for v := uint32(1); v <= 3; v++ { - if must(two.Push(v)) { + if must(batch2.Push(v)) { paired++ } } emptied := must(api.Drain4(batch)) fmt.Printf("batch %d %d %d %d %d %d %d\n", must(batch.Capacity()), accepted, emptied, must(api.Drain4(batch)), - must(two.Capacity()), paired, must(api.Drain2(two))) + must(batch2.Capacity()), paired, must(api.Drain2(batch2))) wide := api.Widen(rt.RangeU64{Start: 2, End: 5}, 1) fmt.Printf("widen %d %d\n", wide.Start, wide.End) included := func(n uint64) rt.BoundU64 { return rt.BoundU64{Kind: rt.BoundIncluded, Value: n} } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index f60090057..68f9e5d7a 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -176,11 +176,11 @@ fun script() { check(runCatching { scaled(v, 1u) }.exceptionOrNull() is IllegalArgumentException, "$v is no u128") } Batch.new().use { batch -> - pair().use { two -> + pair().use { batch2 -> val accepted = (1u..5u).count { batch.push(it) } - val paired = (1u..3u).count { two.push(it) } + val paired = (1u..3u).count { batch2.push(it) } val emptied = drain4(batch) - println("batch ${batch.capacity()} $accepted $emptied ${drain4(batch)} ${two.capacity()} $paired ${drain2(two)}") + println("batch ${batch.capacity()} $accepted $emptied ${drain4(batch)} ${batch2.capacity()} $paired ${drain2(batch2)}") } } val wide = widen(RangeU64(2u, 5u), 1u) diff --git a/ffi/tests/api.py b/ffi/tests/api.py index 1ffc755f1..de0559259 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -237,11 +237,11 @@ def edit(counter): print(f"reach {api.reach(2, 5)}") scaled = [api.scaled(2**128 - 1, 1), api.scaled(2**64 + 1, 2**63), api.scaled(2**127, 2)] print("scaled " + " ".join("none" if v is None else f"{v:032x}" for v in scaled)) - batch, two = api.Batch.new(), api.pair() + batch, batch2 = api.Batch.new(), api.pair() accepted = sum(batch.push(v) for v in range(1, 6)) - paired = sum(two.push(v) for v in range(1, 4)) + paired = sum(batch2.push(v) for v in range(1, 4)) emptied = api.drain_4(batch) - print(f"batch {batch.capacity()} {accepted} {emptied} {api.drain_4(batch)} {two.capacity()} {paired} {api.drain_2(two)}") + print(f"batch {batch.capacity()} {accepted} {emptied} {api.drain_4(batch)} {batch2.capacity()} {paired} {api.drain_2(batch2)}") wide = api.widen(range(2, 5), 1) print(f"widen {wide.start} {wide.stop}") inc, exc, unb = api.Bound.INCLUDED, api.Bound.EXCLUDED, api.Bound.UNBOUNDED diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index cfccfc5e3..23d920588 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -289,11 +289,11 @@ func script() { } print("scaled \(scaledHex.joined(separator: " "))") check(U128(high: .max, low: .max).description == "340282366920938463463374607431768211455", "a u128's decimal digits") - let batch = Batch.new(), two = pair() + let batch = Batch.new(), batch2 = pair() let accepted = (1...5).filter { batch.push($0) }.count - let paired = (1...3).filter { two.push($0) }.count + let paired = (1...3).filter { batch2.push($0) }.count let emptied = drain4(batch) - print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(batch)) \(two.capacity()) \(paired) \(drain2(two))") + print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(batch)) \(batch2.capacity()) \(paired) \(drain2(batch2))") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") let counts = [ From e8207de90cd59743f0b17b662ca7a8ace741c870 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 13:36:06 -0800 Subject: [PATCH 08/12] fix(ffi): own a future's borrowed result before it ends, and export read_frames A future's Done closure lowers its result when C polls, after the async block has dropped what the result borrows; the block now clones a borrowed result first. The api fixture's async fill covers it, as group::Consumer::read_frames needs. ffi.toml drops write_frames and read_frames, which export, and lists Buffer::drain and flac::Config::md5, which the census refuses. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 10 +++ ffi/fixtures/api/src/lib.rs | 7 ++ ffi/moq/ffi.toml | 4 +- ffi/src/collect.rs | 45 +++++++------ ffi/src/glue.rs | 69 ++++++++++++++++---- ffi/tests/api.c | 23 ++++++- ffi/tests/api.cpp | 16 +++++ ffi/tests/api.dart | 4 ++ ffi/tests/api.go | 5 ++ ffi/tests/api.kt | 3 + ffi/tests/api.py | 2 + ffi/tests/api.swift | 2 + quest/m1/ffi-export/refusals/README.md | 2 +- quest/m1/ffi-export/refusals/small-shapes.md | 9 +-- 14 files changed, 159 insertions(+), 42 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index 728e04a61..f5193b650 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -329,6 +329,16 @@ fn main() { Poll::Ready(Err(e)) => println!("nonempty err {e}"), Poll::Pending => println!("nonempty pending"), } + let mut filled: Vec = Vec::new(); + let mut fill4: api::Batch = api::Batch::new(); + if let Poll::Ready(v) = pin!(api::fill(&mut fill4, 9)).poll(&mut cx) { + filled.extend(v.iter().map(u32::to_string)); + } + let mut fill2 = api::pair(); + if let Poll::Ready(v) = pin!(api::fill(&mut fill2, 1)).poll(&mut cx) { + filled.extend(v.iter().map(u32::to_string)); + } + println!("fill {}", filled.join(" ")); // The send happens at the call: the future is dropped unpolled. drop(channel.send_later(Bytes::from_static(b"eager"))); diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 4c77d438f..e1d0ce4c6 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -525,6 +525,13 @@ pub fn drain(batch: &mut Batch) -> u64 { batch.0.drain(..).map(u64::from).sum() } +/// Fill a batch with `v` until it holds `N`, lending what it holds: a future whose result +/// borrows a parameter, as moq's `group::Consumer::read_frames` does. +pub async fn fill(batch: &mut Batch, v: u32) -> &mut [u32] { + while batch.push(v) {} + &mut batch.0 +} + /// A range, widened by `by` at each end. pub fn widen(range: Range, by: u64) -> Range { range.start.saturating_sub(by)..range.end + by diff --git a/ffi/moq/ffi.toml b/ffi/moq/ffi.toml index 05acde3c7..9dfe7c1e2 100644 --- a/ffi/moq/ffi.toml +++ b/ffi/moq/ffi.toml @@ -21,11 +21,10 @@ exclude = [ "coop::Budget::poll_yield", "datagram::Producer::poll_priority", "field@tokio::Log::level", + "frame::Buffer::drain", "frame::Producer::group", "goaway::Consumer::poll", "group::Consumer::poll_read_frames", - "group::Consumer::read_frames", - "group::Producer::write_frames", "hang::catalog::Audio::enabled", "hang::catalog::Video::enabled", "hang::container::Frame::encode", @@ -39,6 +38,7 @@ exclude = [ "mux::codec::aac::Config::parse", "mux::codec::annexb::from_length_prefixed", "mux::codec::annexb::length_prefixed_to_annexb", + "mux::codec::flac::Config::md5", "mux::codec::opus::Config::parse", "mux::container::fmp4::mp4_atom", "mux::container::fmp4::Wire::new", diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index e56280485..c3a90be0b 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -581,7 +581,9 @@ impl<'tcx> Collector<'tcx> { /// The first `explicit` type and const arguments of `args`, which name an instance. fn shown_args(args: ty::GenericArgsRef<'tcx>, explicit: usize) -> Vec> { - let named = args.iter().filter(|arg| !matches!(arg.kind(), GenericArgKind::Lifetime(_))); + let named = args + .iter() + .filter(|arg| !matches!(arg.kind(), GenericArgKind::Lifetime(_))); named.take(explicit).collect() } @@ -814,15 +816,15 @@ impl<'tcx> Collector<'tcx> { GenericArgKind::Lifetime(_) => return None, }; let unarguable = match ty.kind() { - ty::Tuple(list) => !list.is_empty(), - ty::Bool | ty::Char | ty::Float(_) => false, - ty::Int(_) | ty::Uint(_) => int(ty).is_none(), - ty::Adt(adt, _) => { - tcx.lang_items().string() != Some(adt.did()) - && self.bytes != Some(adt.did()) - && !tcx.is_diagnostic_item(rustc_span::Symbol::intern("Duration"), adt.did()) - && !self.exported(adt.did().krate) - && !self.foreign.iter().any(|&(ty, _)| ty == adt.did()) + ty::Tuple(list) => !list.is_empty(), + ty::Bool | ty::Char | ty::Float(_) => false, + ty::Int(_) | ty::Uint(_) => int(ty).is_none(), + ty::Adt(adt, _) => { + tcx.lang_items().string() != Some(adt.did()) + && self.bytes != Some(adt.did()) + && !tcx.is_diagnostic_item(rustc_span::Symbol::intern("Duration"), adt.did()) + && !self.exported(adt.did().krate) + && !self.foreign.iter().any(|&(ty, _)| ty == adt.did()) } _ => true, }; @@ -2987,16 +2989,19 @@ impl<'tcx> Collector<'tcx> { } let mut args = args.to_vec(); args[p.index as usize] = value.into(); - let named = spelled(tcx.mk_args(&args)).into_iter().filter_map(exported).all(|(adt, args)| { - let key = self.key(adt, args); - match self.inferring { - true => self.instances.iter().any(|site| site.key == key), - false => matches!( - self.decls.get(&key), - Some(Decl::Record(_) | Decl::Enum(_) | Decl::Object(_)) - ), - } - }); + let named = spelled(tcx.mk_args(&args)) + .into_iter() + .filter_map(exported) + .all(|(adt, args)| { + let key = self.key(adt, args); + match self.inferring { + true => self.instances.iter().any(|site| site.key == key), + false => matches!( + self.decls.get(&key), + Some(Decl::Record(_) | Decl::Enum(_) | Decl::Object(_)) + ), + } + }); if named { found.push(value); } diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 7273bb208..69ba26d03 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -1184,7 +1184,7 @@ impl Gen<'_> { /// The statements that hand a call's `result` to C: return it, or for a fallible /// function or a tuple, write it through its out pointers or `err`, and return whether /// it succeeded. - fn finish(&self, function: &Function, ret: (&str, &str, &str)) -> String { + fn finish(&self, function: &Function, ok: &Type, ret: (&str, &str, &str)) -> String { let (ok_value, ready, error) = ret; let write = |ty: &Type, out: &str, v: &str| { format!( @@ -1193,9 +1193,10 @@ impl Gen<'_> { self.lower(ty, v) ) }; - let write_ok = match &function.ok { - Type::Unit => String::new(), - Type::Tuple(elements) => { + // The outs C passes follow what the function returns, which `ok` may own. + let write_ok = match (&function.ok, ok) { + (_, Type::Unit) => String::new(), + (Type::Tuple(_), Type::Tuple(elements)) => { let names: Vec = (0..elements.len()).map(|n| format!("v{n}")).collect(); let writes: Vec = elements .iter() @@ -1206,15 +1207,15 @@ impl Gen<'_> { // The trailing comma makes `(v0,)` a tuple pattern, not parentheses. format!("let ({},) = v;\n\t{}", names.join(", "), writes.join("\n\t")) } - ok => write(ok, "out", "v"), + (_, ok) => write(ok, "out", "v"), }; match &function.err { - None if ok_value.is_empty() => match function.ok { - Type::Unit => "result".to_string(), - Type::Tuple(_) => format!("let v = result;\n\t{write_ok}"), - _ => self.lower(&function.ok, "result"), + None if ok_value.is_empty() => match (&function.ok, ok) { + (_, Type::Unit) => "result".to_string(), + (Type::Tuple(_), _) => format!("let v = result;\n\t{write_ok}"), + _ => self.lower(ok, "result"), }, - None => match function.ok { + None => match ok { Type::Unit => format!("let _ = result;\n\t{ready}"), _ => format!("let v = result;\n\t{write_ok}\n\t{ready}"), }, @@ -1344,15 +1345,23 @@ impl Gen<'_> { true => (format!("\n\tlet future = {call};"), "future".to_string()), false => (String::new(), call), }; + // The value C takes is lowered when it polls, outside the future, so a result + // borrowing what the future holds is cloned before the future ends. + let (ok, own) = owned(&function.ok); + let later = match (own, &function.err) { + (None, _) => format!("{later}.await"), + (Some(own), Some(_)) => format!("{later}.await.map(|v| {own})"), + (Some(own), None) => format!("{{ let v = {later}.await; {own} }}"), + }; let bind: String = Self::outs(function) .iter() .enumerate() .map(|(n, out)| format!("let {out} = outs[{n}];\n\t\t\t")) .collect(); self.line(format!( - "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}({}) -> *mut rt::Future {{\n\tlet w = \"{name}\";\n\tlet _ = w;\n\t{body}{now}\n\trt::Future::new(async move {{\n\t\tlet result = {later}.await;\n\t\t::std::boxed::Box::new(move |outs: &[*mut ::std::ffi::c_void], err: *mut ::std::ffi::c_void| -> i32 {{\n\t\t\tlet _ = (outs, err);\n\t\t\t{bind}{}\n\t\t}}) as rt::Done\n\t}})\n}}\n", + "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}({}) -> *mut rt::Future {{\n\tlet w = \"{name}\";\n\tlet _ = w;\n\t{body}{now}\n\trt::Future::new(async move {{\n\t\tlet result = {later};\n\t\t::std::boxed::Box::new(move |outs: &[*mut ::std::ffi::c_void], err: *mut ::std::ffi::c_void| -> i32 {{\n\t\t\tlet _ = (outs, err);\n\t\t\t{bind}{}\n\t\t}}) as rt::Done\n\t}})\n}}\n", params.join(", "), - self.finish(function, ("v", "rt::READY", "rt::ERROR")) + self.finish(function, &ok, ("v", "rt::READY", "rt::ERROR")) )); let poll_params: Vec = poll.params[2..] .iter() @@ -1384,8 +1393,8 @@ impl Gen<'_> { let ret = rs(&export.call.ret); self.lends = function.lends; let finish = match function.err { - Some(_) => self.finish(function, ("v", "true", "false")), - None => self.finish(function, ("", "", "")), + Some(_) => self.finish(function, &function.ok, ("v", "true", "false")), + None => self.finish(function, &function.ok, ("", "", "")), }; self.lends = false; self.line(format!( @@ -1399,3 +1408,35 @@ impl Gen<'_> { )); } } + +/// What a function returns, owned: each reference at its top or in a returned tuple as +/// its clone's type, with the expression cloning `v` into it, if any is one. +fn owned(ok: &Type) -> (Type, Option) { + let one = |ty: &Type| match ty { + Type::Ref(_, inner) => Some(match &**inner { + Type::Str => Type::String, + Type::Slice => Type::Vec, + Type::Path => Type::PathBuf, + inner => inner.clone(), + }), + _ => None, + }; + match ok { + Type::Tuple(elements) if elements.iter().any(|e| one(e).is_some()) => { + let names: Vec = (0..elements.len()).map(|n| format!("v{n}")).collect(); + let owned: Vec = (elements.iter().zip(&names)) + .map(|(e, n)| match one(e) { + Some(_) => format!("::std::borrow::ToOwned::to_owned({n})"), + None => n.clone(), + }) + .collect(); + let ty = Type::Tuple(elements.iter().map(|e| one(e).unwrap_or_else(|| e.clone())).collect()); + let own = format!("{{ let ({},) = v; ({},) }}", names.join(", "), owned.join(", ")); + (ty, Some(own)) + } + ok => match one(ok) { + Some(ty) => (ty, Some("::std::borrow::ToOwned::to_owned(v)".to_string())), + None => (ok.clone(), None), + }, + } +} diff --git a/ffi/tests/api.c b/ffi/tests/api.c index 348cc5f21..d04f2d055 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -601,6 +601,27 @@ static int script(void) { printf("nonempty pending\n"); } api_Channel_recv_nonempty_future_free(nonempty); + api_Batch *fill4 = api_Batch_new(); + api_Batch_2 *fill2 = api_pair(); + api_fill_4_future *filling4 = api_fill_4(fill4, 9); + api_fill_2_future *filling2 = api_fill_2(fill2, 1); + api_vec_u32 filled4, filled2; + if (api_fill_4_poll(filling4, waker, &filled4) == API_POLL_READY && api_fill_2_poll(filling2, waker, &filled2) == API_POLL_READY) { + printf("fill"); + for (size_t i = 0; i < filled4.len; i++) { + printf(" %u", (unsigned)filled4.ptr[i]); + } + for (size_t i = 0; i < filled2.len; i++) { + printf(" %u", (unsigned)filled2.ptr[i]); + } + printf("\n"); + api_vec_u32_free(filled4); + api_vec_u32_free(filled2); + } + api_fill_4_future_free(filling4); + api_fill_2_future_free(filling2); + api_Batch_2_free(fill2); + api_Batch_free(fill4); /* The send happens at the call: the future is dropped unpolled. */ api_Channel_send_later_future_free(api_Channel_send_later(channel, str("eager"))); @@ -1132,7 +1153,7 @@ static int script(void) { api_Token_free(twice); /* Each future dropped its waker once it finished, and Rust the one it was given. */ - return drops == 5 ? 0 : 1; + return drops == 7 ? 0 : 1; } /* Each breaks a contract, which must abort with a message naming the function. */ diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 8765f8a8c..c40c6ff7f 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -496,6 +496,22 @@ int script() { std::printf("nonempty pending\n"); } } + { + auto fill4 = api::Batch::new_(); + auto fill2 = api::pair(); + auto filled4 = api::fill_4(fill4, 9).poll(waker); + auto filled2 = api::fill_2(fill2, 1).poll(waker); + if (filled4 && filled2) { + std::printf("fill"); + for (auto v : *filled4) { + std::printf(" %u", unsigned(v)); + } + for (auto v : *filled2) { + std::printf(" %u", unsigned(v)); + } + std::printf("\n"); + } + } // The send happens at the call: the future is dropped unpolled. channel.send_later("eager"); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index a55bcaae5..b7370c65e 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -343,6 +343,10 @@ Future script() async { } on api.Error catch (e) { print('nonempty err $e'); } + final fill4 = api.Batch(), fill2 = api.pair(); + print('fill ${[...await api.fill4(fill4, 9), ...await api.fill2(fill2, 1)].join(' ')}'); + fill4.close(); + fill2.close(); // The send happens at the call: the future is dropped unawaited. channel.sendLater(utf8.encode('eager')); print('recv ${text(await channel.recv())}'); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 6e6ba998f..02a1c2f94 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -429,6 +429,11 @@ func script() { if _, err := channel.RecvNonempty(ctx); err != nil { fmt.Printf("nonempty err %s\n", err) } + fmt.Print("fill") + for _, v := range append(must(api.Fill4(ctx, api.NewBatch(), 9)), must(api.Fill2(ctx, api.Pair(), 1))...) { + fmt.Printf(" %d", v) + } + fmt.Println() // The send happens at the call: the future is cancelled before its first poll. cancelled, cancel := context.WithCancel(ctx) cancel() diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 68f9e5d7a..4bc6d0481 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -273,6 +273,9 @@ fun script() { } catch (e: ErrorException) { println("nonempty err ${e.message}") } + Batch.new().use { b4 -> + pair().use { b2 -> println("fill " + (fill4(b4, 9u) + fill2(b2, 1u)).joinToString(" ")) } + } // The send happens at the call: the future is dropped unawaited. channel.sendLater("eager".encodeToByteArray()) println("recv ${channel.recv().text()}") diff --git a/ffi/tests/api.py b/ffi/tests/api.py index de0559259..ea603cc32 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -347,6 +347,8 @@ async def block(): print(f"nonempty {payload.nbytes}") except api.ErrorException as e: print(f"nonempty err {e}") + filled = await api.fill_4(api.Batch.new(), 9) + await api.fill_2(api.pair(), 1) + print("fill " + " ".join(map(str, filled))) # The send happens at the call: the future is dropped unawaited. channel.send_later(b"eager") print(f"recv {text(await channel.recv())}") diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 23d920588..d35e42824 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -397,6 +397,8 @@ func script() { } catch { print("nonempty err \(error)") } + let filled = try await fill4(Batch.new(), 9) + fill2(Api.pair(), 1) + print("fill \(filled.map { Swift.String($0) }.joined(separator: " "))") // The send happens at the call, before the future is first polled. try await channel.sendLater(Data("eager".utf8)) print("recv \(text(try await channel.recv()))") diff --git a/quest/m1/ffi-export/refusals/README.md b/quest/m1/ffi-export/refusals/README.md index 99000210f..8589fe8a2 100644 --- a/quest/m1/ffi-export/refusals/README.md +++ b/quest/m1/ffi-export/refusals/README.md @@ -80,5 +80,5 @@ Public API: the exporter's command line and the C API of every item that exports - [Futures and kept closures as parameters export](/quest/m1/ffi-export/refusals/generics.md) - `clock::timeout`'s future, `clock::drive`'s kept closure, and `mp4::repair`'s seekable reader cross from C - [Standard and common types export by role](/quest/m1/ffi-export/refusals/std-types.md) - `Filter`, serde_json's map iterators, `Instant`, and tracing's levels, once their maintainer decisions land - [Poll and Context export](/quest/m1/ffi-export/refusals/poll.md) - a `poll_*` with no async twin takes a waker, as futures' polls do -- [Small shapes export](/quest/m1/ffi-export/refusals/small-shapes.md) - `tokio::tls::parse_fingerprint`'s returned array, once the maintainer picks its mapping +- [Small shapes export](/quest/m1/ffi-export/refusals/small-shapes.md) - `parse_fingerprint`'s and `flac::Config::md5`'s arrays, once the maintainer picks their mapping - [Bump the moq pin past its re-export and reachability fixes](/quest/m1/ffi-export/refusals/moq-pin.md) - the census has no whole-crate re-export refusal and no unreachable type of moq's own diff --git a/quest/m1/ffi-export/refusals/small-shapes.md b/quest/m1/ffi-export/refusals/small-shapes.md index 28c0197a7..9394be8ae 100644 --- a/quest/m1/ffi-export/refusals/small-shapes.md +++ b/quest/m1/ffi-export/refusals/small-shapes.md @@ -2,9 +2,10 @@ ## Goal -The census at the moq pin refuses no item of moq's own for an array. One is left: +The census at the moq pin refuses no item of moq's own for an array. Two are left: `tokio::tls::parse_fingerprint` returns `Result<[u8; 32]>`, a moq-tokio item the narrowed -stand-in still exports. +stand-in still exports, and `mux::codec::flac::Config` has a `pub md5: [u8; 16]` field, +which its getter returns and its setter takes. ## Plan @@ -22,5 +23,5 @@ return left the census with the narrowed stand-in. language's error on a mismatch; (b) a C struct `_array__` holding the array, with each language's fixed-size form above it; (c) leave arrays refused and keep this quest open until a consumer needs one. Recommended: (a), which reuses the - bytes every binding already has and clears `parse_fingerprint` with no new type; any - other element type stays refused until one is needed. + bytes every binding already has and clears both with no new type; any other element + type stays refused until one is needed. From 6ec675be798c0f623d72ade96c4a61e27849f151 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 13:57:43 -0800 Subject: [PATCH 09/12] chore(ffi): exclude flac::Config::md5 as its field, as #693 spells it Co-Authored-By: Claude Opus 5.5 --- ffi/moq/ffi.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/moq/ffi.toml b/ffi/moq/ffi.toml index 9dfe7c1e2..873db0b2d 100644 --- a/ffi/moq/ffi.toml +++ b/ffi/moq/ffi.toml @@ -20,6 +20,7 @@ exclude = [ "clock::timeout_at", "coop::Budget::poll_yield", "datagram::Producer::poll_priority", + "field@mux::codec::flac::Config::md5", "field@tokio::Log::level", "frame::Buffer::drain", "frame::Producer::group", @@ -38,7 +39,6 @@ exclude = [ "mux::codec::aac::Config::parse", "mux::codec::annexb::from_length_prefixed", "mux::codec::annexb::length_prefixed_to_annexb", - "mux::codec::flac::Config::md5", "mux::codec::opus::Config::parse", "mux::container::fmp4::mp4_atom", "mux::container::fmp4::Wire::new", From d7486dbc9ea04340497f2a10d948ea129b58f6a1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 14:04:48 -0800 Subject: [PATCH 10/12] fix(ffi): check a const choice only against chosen arguments, and reserve Swift's runtime names A function's parameter choice checked every generic type its signature names, one naming a later parameter too, which no instance matches, so two const parameters refused the function. Each type is now checked once its parameters are chosen; the api fixture's capacities covers it. Swift reserves the runtime's U128, SocketAddr, and Comparable, and a test keeps every runtime declaration reserved. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 5 ++++ ffi/fixtures/api/src/lib.rs | 5 ++++ ffi/src/collect.rs | 27 ++++++++++++--------- ffi/src/swift.rs | 32 +++++++++++++++++++++++++ ffi/tests/api.c | 1 + ffi/tests/api.cpp | 1 + ffi/tests/api.dart | 1 + ffi/tests/api.go | 1 + ffi/tests/api.kt | 1 + ffi/tests/api.py | 1 + ffi/tests/api.swift | 1 + 11 files changed, 65 insertions(+), 11 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index f5193b650..3463a619d 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -225,6 +225,11 @@ fn main() { batch2.capacity(), api::drain(&mut batch2) ); + println!( + "capacities {} {}", + api::capacities(&batch, &batch2), + api::capacities(&batch2, &batch2) + ); let wide = api::widen(2..5, 1); println!("widen {} {}", wide.start, wide.end); println!( diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index e1d0ce4c6..78c6d28c1 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -525,6 +525,11 @@ pub fn drain(batch: &mut Batch) -> u64 { batch.0.drain(..).map(u64::from).sum() } +/// Both batches' capacities: two const parameters, each at every instance's value. +pub fn capacities(a: &Batch, b: &Batch) -> usize { + a.capacity() + b.capacity() +} + /// Fill a batch with `v` until it holds `N`, lending what it holds: a future whose result /// borrows a parameter, as moq's `group::Consumer::read_frames` does. pub async fn fill(batch: &mut Batch, v: u32) -> &mut [u32] { diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index c3a90be0b..5195bf927 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2944,16 +2944,19 @@ impl<'tcx> Collector<'tcx> { let Some(args) = at(candidate) else { continue; }; - let named = spelled(args).into_iter().filter_map(exported).all(|(adt, args)| { - let key = self.key(adt, args); - match self.inferring { - true => self.instances.iter().any(|site| site.key == key), - false => matches!( - self.decls.get(&key), - Some(Decl::Record(_) | Decl::Enum(_) | Decl::Object(_)) - ), - } - }); + // A type naming a parameter not yet chosen is checked once it is. + let named = (spelled(args).into_iter().filter_map(exported)) + .filter(|(_, args)| !args.has_non_region_param()) + .all(|(adt, args)| { + let key = self.key(adt, args); + match self.inferring { + true => self.instances.iter().any(|site| site.key == key), + false => matches!( + self.decls.get(&key), + Some(Decl::Record(_) | Decl::Enum(_) | Decl::Object(_)) + ), + } + }); if named { found.push(candidate); } @@ -2964,7 +2967,7 @@ impl<'tcx> Collector<'tcx> { /// The values a const parameter exports at: as an argument of a generic type the /// signature names, each value an instance of it has there, where each generic type the - /// signature then names is an instance. + /// signature then names at chosen arguments only is an instance. fn each_const(&self, def_id: DefId, param: ty::Const<'tcx>, args: &[ty::GenericArg<'tcx>]) -> Vec> { let tcx = self.tcx; let exported = |ty: Ty<'tcx>| match ty.kind() { @@ -2989,9 +2992,11 @@ impl<'tcx> Collector<'tcx> { } let mut args = args.to_vec(); args[p.index as usize] = value.into(); + // A type naming a parameter not yet chosen is checked once it is. let named = spelled(tcx.mk_args(&args)) .into_iter() .filter_map(exported) + .filter(|(_, args)| !args.has_non_region_param()) .all(|(adt, args)| { let key = self.key(adt, args); match self.inferring { diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs index 938347d69..9201e4d35 100644 --- a/ffi/src/swift.rs +++ b/ffi/src/swift.rs @@ -118,6 +118,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result String { } out } + +#[cfg(test)] +mod tests { + use super::{RUNTIME, TAKEN}; + + /// Each type the runtime declares at its top level, and each protocol one conforms to, + /// is a name no export may take, as either would clash with or shadow it. + #[test] + fn runtime_names_taken() { + for line in RUNTIME.lines() { + let Some(decl) = [ + "public struct ", + "public final class ", + "public enum ", + "public protocol ", + ] + .iter() + .find_map(|kind| line.strip_prefix(kind)) else { + continue; + }; + let decl = decl.trim_end_matches('{').trim(); + let (name, conforms) = decl.split_once(':').unwrap_or((decl, "")); + let names = std::iter::once(name).chain(conforms.split(',')); + for name in names.map(str::trim).filter(|n| !n.is_empty() && !n.starts_with('@')) { + assert!(TAKEN.contains(&name), "the runtime's `{name}` is not in TAKEN"); + } + } + } +} diff --git a/ffi/tests/api.c b/ffi/tests/api.c index d04f2d055..bb560e902 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -469,6 +469,7 @@ static int script(void) { unsigned long long emptied = api_drain_4(batch); printf("batch %zu %u %llu %llu %zu %u %llu\n", api_Batch_capacity(batch), accepted, emptied, (unsigned long long)api_drain_4(batch), api_Batch_2_capacity(batch2), paired, (unsigned long long)api_drain_2(batch2)); + printf("capacities %zu %zu\n", api_capacities_4_2(batch, batch2), api_capacities_2_2(batch2, batch2)); api_Batch_2_free(batch2); api_Batch_free(batch); api_range_u64 wide = api_widen((api_range_u64){.start = 2, .end = 5}, 1); diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index c40c6ff7f..3c1e3c055 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -381,6 +381,7 @@ int script() { auto emptied = api::drain_4(batch); std::printf("batch %zu %u %llu %llu %zu %u %llu\n", batch.capacity(), accepted, ull(emptied), ull(api::drain_4(batch)), batch2.capacity(), paired, ull(api::drain_2(batch2))); + std::printf("capacities %zu %zu\n", api::capacities_4_2(batch, batch2), api::capacities_2_2(batch2, batch2)); } api::range_u64 wide = api::widen({2, 5}, 1); std::printf("widen %llu %llu\n", ull(wide.start), ull(wide.end)); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index b7370c65e..99bc9c089 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -252,6 +252,7 @@ Future script() async { final paired = [for (var v = 1; v <= 3; v++) batch2.push(v)].where((p) => p).length; final emptied = api.drain4(batch); print('batch ${batch.capacity()} $accepted $emptied ${api.drain4(batch)} ${batch2.capacity()} $paired ${api.drain2(batch2)}'); + print('capacities ${api.capacities42(batch, batch2)} ${api.capacities22(batch2, batch2)}'); batch.close(); batch2.close(); final wide = api.widen(const api.RangeU64(2, 5), 1); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 02a1c2f94..c093f5e74 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -334,6 +334,7 @@ func script() { emptied := must(api.Drain4(batch)) fmt.Printf("batch %d %d %d %d %d %d %d\n", must(batch.Capacity()), accepted, emptied, must(api.Drain4(batch)), must(batch2.Capacity()), paired, must(api.Drain2(batch2))) + fmt.Printf("capacities %d %d\n", must(api.Capacities42(batch, batch2)), must(api.Capacities22(batch2, batch2))) wide := api.Widen(rt.RangeU64{Start: 2, End: 5}, 1) fmt.Printf("widen %d %d\n", wide.Start, wide.End) included := func(n uint64) rt.BoundU64 { return rt.BoundU64{Kind: rt.BoundIncluded, Value: n} } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 4bc6d0481..b7232b2d9 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -181,6 +181,7 @@ fun script() { val paired = (1u..3u).count { batch2.push(it) } val emptied = drain4(batch) println("batch ${batch.capacity()} $accepted $emptied ${drain4(batch)} ${batch2.capacity()} $paired ${drain2(batch2)}") + println("capacities ${capacities42(batch, batch2)} ${capacities22(batch2, batch2)}") } } val wide = widen(RangeU64(2u, 5u), 1u) diff --git a/ffi/tests/api.py b/ffi/tests/api.py index ea603cc32..f6b2f1f2e 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -242,6 +242,7 @@ def edit(counter): paired = sum(batch2.push(v) for v in range(1, 4)) emptied = api.drain_4(batch) print(f"batch {batch.capacity()} {accepted} {emptied} {api.drain_4(batch)} {batch2.capacity()} {paired} {api.drain_2(batch2)}") + print(f"capacities {api.capacities_4_2(batch, batch2)} {api.capacities_2_2(batch2, batch2)}") wide = api.widen(range(2, 5), 1) print(f"widen {wide.start} {wide.stop}") inc, exc, unb = api.Bound.INCLUDED, api.Bound.EXCLUDED, api.Bound.UNBOUNDED diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index d35e42824..1cb799956 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -294,6 +294,7 @@ func script() { let paired = (1...3).filter { batch2.push($0) }.count let emptied = drain4(batch) print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(batch)) \(batch2.capacity()) \(paired) \(drain2(batch2))") + print("capacities \(capacities42(batch, batch2)) \(capacities22(batch2, batch2))") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") let counts = [ From e7b35b2572857a39fd30a61a2bdd497f66b923b4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 14:22:31 -0800 Subject: [PATCH 11/12] fix(ffi): place a () argument by its Rust index, beside a poll's context Co-Authored-By: Claude Opus 5.5 --- ffi/src/collect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index d83164b34..605fe8ad4 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2307,7 +2307,7 @@ impl<'tcx> Collector<'tcx> { // C passes nothing for `()`, the one value of its type. A callback's stays // refused: the glue's implementation of its trait names every parameter. if ty.is_unit() && !callback { - units.push(params.len() + units.len()); + units.push(i - usize::from(has_self)); continue; } let name = names From 99ab758e40405d6b212973577cbd8c0ce7ad97be Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 14:32:04 -0800 Subject: [PATCH 12/12] fix(ffi): match a call's context by its place among Rust's parameters A synchronous Context's task records its place among Rust's parameters, which a () before it shifts from its C index. The glue now maps each C parameter to its Rust place before matching. The api fixture's Channel::listen takes a () before its context. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 2 +- ffi/fixtures/api/src/lib.rs | 4 ++-- ffi/src/glue.rs | 10 +++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index 8a1951af9..15c894126 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -366,7 +366,7 @@ fn main() { if let Poll::Ready(Ok(len)) = channel.clone().poll_measure(&mut cx, 2) { println!("measure {len}"); } - channel.listen(&cx); + channel.listen((), &cx); channel.send(Bytes::from_static(b"heard")); println!("woken {}", count.0.load(Ordering::Relaxed)); diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 482eefc11..1a9201ec2 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -698,8 +698,8 @@ impl Channel { } /// Wakes the context's task on the next send, as `notify` wakes its waker: C passes the - /// waker the context is made of. - pub fn listen(&self, cx: &Context<'_>) { + /// waker the context is made of, and nothing for the `()` before it. + pub fn listen(&self, _: (), cx: &Context<'_>) { self.state.lock().unwrap().1 = Some(cx.waker().clone()); } diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 49b1d226c..9c526a9ac 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -1265,7 +1265,15 @@ impl Gen<'_> { } } } + // Each parameter's place among Rust's after `self`, past the `()` ones C does not + // pass and a future's context, which is the poll's. + let omitted = |at: &usize| { + function.units.contains(at) + || (function.future && function.task.as_ref().is_some_and(|task| task.at == *at)) + }; + let mut places = (0..).filter(|at| !omitted(at)); for (n, param) in function.params.iter().enumerate() { + let place = places.next().expect("a place for each parameter"); let (_, c_ty) = c_params.next().expect("a C parameter for each"); params.push(format!("p{n}: {}", rs(c_ty))); if let Type::Closure(..) = param.ty { @@ -1277,7 +1285,7 @@ impl Gen<'_> { Some(Task { at, kind: TaskKind::Context(m), - }) if *at == n && !function.future => { + }) if *at == place && !function.future => { let (binding, pass) = match m { Mutability::Mut => ("let mut", format!("&mut a{n}")), Mutability::Not => ("let", format!("&a{n}")),