Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6e3fb93
chore(quest): claim quest/m1/ffi-export/refusals/small-shapes.md
kixelated Nov 9, 2026
b42d794
feat(ffi): pass no C argument for a () parameter, and export an empty…
kixelated Nov 9, 2026
a5b647c
Merge remote-tracking branch 'origin/main' into pr675
kixelated Nov 10, 2026
c26e26d
feat(ffi): export u128 as two u64 halves, and each language's big int…
kixelated Nov 10, 2026
9299d34
fix(ffi): u128's Python overflow, and move the empty enum to the api …
kixelated Nov 10, 2026
51ff40d
fix(ffi): Go's Option<u128> is a nil-able *big.Int, and format
kixelated Nov 10, 2026
da70370
Merge remote-tracking branch 'origin/main' into quest/m1/ffi-export/r…
kixelated Nov 10, 2026
bde3fee
feat(ffi): export const generics at their inferred values
kixelated Nov 10, 2026
6d5bd9f
Merge remote-tracking branch 'origin/main' into quest/m1/ffi-export/r…
kixelated Nov 10, 2026
c89f472
test(ffi): cover const generics in every language, and rewrite small-…
kixelated Nov 10, 2026
e8207de
fix(ffi): own a future's borrowed result before it ends, and export r…
kixelated Nov 10, 2026
6ec675b
chore(ffi): exclude flac::Config::md5 as its field, as #693 spells it
kixelated Nov 10, 2026
d7486db
fix(ffi): check a const choice only against chosen arguments, and res…
kixelated Nov 10, 2026
9a39789
Merge remote-tracking branch 'origin/main' into quest/m1/ffi-export/r…
kixelated Nov 10, 2026
e7b35b2
fix(ffi): place a () argument by its Rust index, beside a poll's context
kixelated Nov 10, 2026
99ab758
fix(ffi): match a call's context by its place among Rust's parameters
kixelated Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,23 @@ re-exports its items instead, which export. 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 |
| a `pub` field of an object | a getter returning a copy, and a setter assigning in place, as `x.field = value` does |
| a `pub` trait | a callback interface: a table of C functions and their data; a trait C cannot implement, such as one with a blanket impl, an associated type, or a supertrait but a marker, is left out, since C never passes one, and a signature taking one from C is refused |
| 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<T, E>` | `bool f(..., T *out, E *err)`; an `E` of another crate that implements `Display`, such as `anyhow::Error` or `io::Error`, is `String`, its message |
| 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 | `<prefix>_tuple_<A>_<B>`: a struct of fields `_0`, `_1`, and so on; of references only returned |
| `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 |
| a function returning `Poll<T>` that polls through a `&Context`, a `&mut Context`, or a reference to an object exactly one associated function makes from a `Waker` alone, of a type with no generic parameters, such as kio's `Waiter` | a future of `T`, as an `async fn`'s: each poll calls the function again with the poll's context, or with an object made from its waker and kept until the next poll, so what it registers stays registered; each other parameter taken by value must be `Copy` |
| `poll_x`, when `x` exports as a future | left out: the future is its poll |
| `bool`, integers but 128-bit, floats, `char` | `bool`, `<stdint.h>` types, `float`, `double`, a `uint32_t` code point |
| `u128` | `<prefix>_u128`: its low and high 64 bits |
| `&str`, `String`, `&[u8]`, `Vec<u8>`, `bytes::Bytes` | `<prefix>_buf`: a view passed in, an owned buffer returned |
| `&Path`, `PathBuf` | `<prefix>_buf` of the path's bytes as the OS holds them on Unix, which need not be UTF-8, else UTF-8 |
| `Duration` | `<prefix>_duration` |
Expand All @@ -100,17 +102,19 @@ re-exports its items instead, which export. A type no path reaches is refused.
| `impl IntoIterator<Item = X>`, where `X` is no type parameter | `Vec<X>`, 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<u8>` 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<u32>`; 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<u64> for Wrap<u32>`; 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<Cell<u8>>`, 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<u32>`; 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<u64> for Wrap<u32>`; 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<Cell<u8>>`, 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
Expand Down Expand Up @@ -212,8 +216,10 @@ 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<moq::mux::catalog::hang::Extra>::stats"`) out of that one.

## C++
Expand Down Expand Up @@ -268,8 +274,8 @@ exceptions and RTTI on or off: it throws nothing and reads no type information.
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, which Clang's `-Wdangling`
refuses for a view or `str()` of a temporary `buf`. A `Vec<T>` is a `std::vector`, of
`view`s for bytes passed in, and a socket address `socket_addr`, the C struct. `Option<T>` is `std::optional<T>`, a
tuple `std::tuple`, and `Result<T, E>` a `result<T, E>`, whose `value()` aborts on an
`view`s for bytes passed in, a socket address `socket_addr`, the C struct, and a `u128`
`u128`, the C struct. `Option<T>` is `std::optional<T>`, a tuple `std::tuple`, and `Result<T, E>` a `result<T, E>`, whose `value()` aborts on an
error.
- **Traits** are abstract classes, passed as a `std::unique_ptr` that Rust deletes.
- **Closures** lent for a call are any callable, taken by a template parameter, which
Expand Down Expand Up @@ -329,7 +335,7 @@ in as a `ByteArray`, bytes returned as their owner, and anything else encoded in
record holding bytes does too; Rust's lent ones are copied across in one call to compare.
Strings are `String`, `usize` `ULong` and `isize` `Long`, which abort past a 32-bit
target's range, `Option<T>` `T?`, a tuple `Pair`,
`Triple`, or else a generated `Tuple<n>`, though a tuple of one returned whole is its element, `Duration` `kotlin.time.Duration`, and an `Err` a thrown `<E>Exception`
`Triple`, or else a generated `Tuple<n>`, 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 `<E>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
Expand Down Expand Up @@ -386,7 +392,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>` `T | None`, a tuple a `tuple`, `Duration`
`bytes`. Strings are `str`, `u128` an `int`, `Option<T>` `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 `<E>Exception`
holding it in `error`, whose `str` is its `Display`.
Expand Down Expand Up @@ -443,7 +449,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>` `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 `<T>Error` holding it.
- A **path** is a `String`: one Rust returns that is not UTF-8 traps. A `SocketAddr`,
Expand Down Expand Up @@ -534,9 +541,10 @@ 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<T>` 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
of the OS's bytes, which need not be UTF-8. `Option<T>` 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,
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
Expand Down Expand Up @@ -604,7 +612,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<T>` is `T?`, a tuple a Dart record, and a `Vec<T>` or `&[T]` a
`List<T>`. `Duration` is `Duration`, truncated to the microsecond: a negative one passed
`List<T>`. 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
Expand Down
37 changes: 36 additions & 1 deletion ffi/fixtures/api/examples/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,31 @@ 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 mut batch: api::Batch = api::Batch::new();
let accepted = (1..=5).filter(|&v| batch.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),
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!(
Expand Down Expand Up @@ -309,6 +334,16 @@ fn main() {
Poll::Ready(Err(e)) => println!("nonempty err {e}"),
Poll::Pending => println!("nonempty pending"),
}
let mut filled: Vec<String> = 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")));
Expand All @@ -331,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));

Expand Down
65 changes: 63 additions & 2 deletions ffi/fixtures/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,63 @@ pub fn total(range: Range<u64>) -> 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
}

/// `v` times `by`, unless past `u128::MAX`.
pub fn scaled(v: u128, by: u64) -> Option<u128> {
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<const N: usize = 4>(Vec<u32>);

impl<const N: usize> Batch<N> {
/// 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<const N: usize>(batch: &mut Batch<N>) -> u64 {
batch.0.drain(..).map(u64::from).sum()
}

/// Both batches' capacities: two const parameters, each at every instance's value.
pub fn capacities<const N: usize, const M: usize>(a: &Batch<N>, b: &Batch<M>) -> 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<const N: usize>(batch: &mut Batch<N>, v: u32) -> &mut [u32] {
while batch.push(v) {}
&mut batch.0
}

/// A range, widened by `by` at each end.
pub fn widen(range: Range<u64>, by: u64) -> Range<u64> {
range.start.saturating_sub(by)..range.end + by
Expand Down Expand Up @@ -641,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());
}

Expand Down Expand Up @@ -1110,6 +1167,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);
Expand Down
5 changes: 3 additions & 2 deletions ffi/fixtures/reject/expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>::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<u16>`; 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<std::vec::Vec<String>>`; 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
Expand Down
Loading