Skip to content

feat(ffi): export generic parameters through blanket impls, and leave out traits C cannot implement - #670

Merged
moq-bot[bot] merged 10 commits into
mainfrom
quest/m1/ffi-export/refusals/generics
Nov 10, 2026
Merged

moq-bot[bot] merged 10 commits into
mainfrom
quest/m1/ffi-export/refusals/generics

Conversation

@kixelated

@kixelated kixelated commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Works quest/m1/ffi-export/refusals/generics.md and rewrites it to what remains.

Problem

The census at moq ffcb7cd9 refused 51 of moq's own items for a generic parameter, type argument, trait object, supertrait, associated type, unmet bound, callback or closure signature, or blanket impl. moq-ffi's session.rs and server.rs wrap Client::connect and Server::accept by hand because Boxable refused them.

Approach

  • Blanket impls: a parameter bounded by a trait with a blanket impl, as impl Boxable is, exports at each type implementing what the impl requires (Server::accept<transport::memory::Session>, and moq-tokio's web Session until narrowing drops it).
  • Per-choice bounds: instantiate chooses each type parameter at each choice of the earlier ones, so track<C, R>(.., config: impl Into<Option<R>>) takes Option<R> for each R, rather than String chosen once with R unbound, which failed as an unmet bound (6 items).
  • Readers and borrowed items: impl io::Read takes &[u8] (Catalog::from_reader, 3 items); impl IntoIterator<Item = &X> takes &[X].
  • Type arguments: Bytes and Duration name instances (health::Limits<Duration>).
  • Traits C cannot implement are left out: a trait refused as a callback (blanket impl, associated type, supertrait, unmappable method) no longer fails the export; the census lists it under "Traits left out, since C cannot implement them" with its reason, and a signature that takes one from C, as Box<dyn Source> does, is refused with a note naming why (16 of moq's items; 101 traits in the census, mostly rustls's and moq-tokio's). The api fixture's Seed, Kept, and Payload excludes go.

Census of moq-ffi at the pin: these constructs refuse 25 items of moq's own, down from 58, and 13 of the 25 are moq-tokio items outside its session side, which narrowing drops; Client::connect and Server::accept_request now pass their generic parameter but stay refused for a future borrowed from a function that is not async, which poll.md owns. build_prefix exports once moq stops re-exporting bytes whole, which makes Bytes a handle. The rest, futures and kept closures as parameters, is the rewritten quest, waiting on how they cross from C.

Choices made without asking:

  • A trait C cannot implement is left out and listed rather than refused, by analogy with the epic's decision for what C can never hold: C never passes one, its methods still export on each implementor, and any signature that would need one from C is still refused at its span. Rejected: refusing them, which would make moq seal or hide transport::poll::Session, Container, IntoBytes, and the like.
  • &[u8] is taken only for io::Read, not as a general fallback, which picked &[u8] for any T: Copy and broke Pair::beside.
  • ffi/moq/ffi.toml keeps its excludes: deleting it is the epic's work, and narrowing edits it in parallel.

Impact

  • rust2-ffi: a trait C cannot implement no longer fails the export; the census gains a section listing them and its Traits row no longer counts them refused.
  • C API of exports that now map: functions bounded by blanket-impl traits, impl io::Read and impl IntoIterator<Item = &X> parameters, instances at Bytes and Duration, and parameters bounded by an earlier one.
  • No breaking change to an existing export's C names: just ffi moq passes unchanged.

Alternatives

  • Exporting each trait C cannot implement as a callback (associated types as foreign implementations of their bounds, supertraits as merged tables): no binding passes a transport session or catalog stream from C, and the census showed none is taken from C.

Follow-ups

  • The rewritten quest waits on the maintainer's choice of C shape for futures and kept closures; recommended: a foreign future (poll, data, free) and an owned closure in the exporter.
  • Recount poll.md: Client::connect and Server::accept_request now reach its "borrowing future" construct. Recommend: fold into poll.md when it measures again, no new quest.

Decisions

  • Design checkpoint after review round 3, settled by the maintainer on 2026-11-09: ✅ (a) keep 'a in the impl IntoIterator<Item = &'a X> candidate, as &'a [X] instead of an erased borrow, so the existing borrows analysis of the instantiated signature decides, and delete the hand-written lent guard (rejected: (b) drop the rule and leave build_prefix to the rewritten quest, (c) keep adding guards). Landed (a): the candidate takes the item's own region, which instantiate already keeps as it is, so no region plumbing was needed. A 'static item, a handle's &mut Text<'a>, a 'b: 'a bound, and a handle chosen for an earlier T: Into<Text<'a>> all make 'a 'static, which borrows refuses as 'static borrow at its span, as for any &'a parameter. This closes the review finding at ffi/src/collect.rs:2331. Reject fixtures: summed_forever and retext_last (rounds 1-2), and new outlast and retext_into (round 3).
  • first_of, which returns Option<&'a String> borrowed from the items, now exports, and soundly: the borrow is copied out before C's slice ends, as any borrowed return of a Clone type is. It moved from the reject fixture to the api fixture, tested in every language.
  • Review round 1 of the new session found that a parameter kept for a foreign implementation let unmet skip a bound naming it, such as B: From<U>, keeping a choice whose glue does not compile; the per-choice loop this PR rewrites carried that skip over. Fixed here: a bound naming only kept parameters, other than one of them as its own subject (met by what the call passes), must hold in the item's own environment. The api fixture's tally exports only at LogCount, and fails to compile at Point with the change reverted; the reject fixture's tallied refuses as an unmet bound.

Checks

Models

Size: L
Implementer: Claude Code, Claude Opus 5.5, high effort
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, L)

(Written by Claude Opus 5.5 with high)

🤖 Generated with Claude Code

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1c557-5ae9-7921-ae6c-8fdfd59f4c86, round 2
  • Head: 3c278e8
  • Base: origin/main at 784d5e0
  • Score: 10/10, The fix validates dependent bounds before retaining callback specializations, with regression fixtures covering valid exports and source-span refusals; no remaining issues were found by inspection.
  • Findings: none
  • The last round's findings, at c5a5c8f:
    • Fixed: medium, ffi/src/collect.rs:2226: Validate dependent bounds before keeping a callback parameter; ffi/src/collect.rs:2313 now checks dependent bounds involving kept parameters, filtering invalid choices, while ffi/fixtures/api/src/lib.rs:1493 and ffi/fixtures/reject/expected.txt cover successful specialization and refusal.

@pullfrog

pullfrog Bot commented Nov 9, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

kixelated and others added 2 commits November 9, 2026 08:29
…oice bounds, and leave out traits C cannot implement

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Generic parameters and traits export feat(ffi): export generic parameters through blanket impls, and leave out traits C cannot implement Nov 9, 2026
kixelated and others added 3 commits November 9, 2026 10:18
… for the call

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rameter names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits November 9, 2026 17:37
…efusals/generics

# Conflicts:
#	ffi/README.md
#	ffi/fixtures/reject/expected.txt
#	ffi/src/collect.rs
`impl IntoIterator<Item = &'a X>` takes `&'a [X]` rather than a slice whose
borrow is erased, so the borrows analysis of the instantiated signature
decides whether C's slice lives long enough, as it does for any `&'a`
parameter. The hand-written `lent` guard goes, along with the cases it
missed: a lifetime that a later-chosen handle parameter pins to `'static`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits November 10, 2026 03:58
With the slice borrowed for the items' lifetime, `first_of`'s returned
borrow is copied out before the call ends, so it exports soundly: move it to
the api fixture and test it in every language. Regenerate the reject
fixture's expected refusals after merging main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parameter kept for a foreign implementation stayed a parameter in every
bound, so `unmet` skipped `B: From<U>` and kept a choice of `B` whose glue
does not compile. A bound that names only kept parameters, other than one of
them as its own subject, which what the call passes meets, now holds only as
the item's own environment proves it for every type passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 10, 2026 15:22
@coderabbitai

coderabbitai Bot commented Nov 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 45 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4da75cf8-862a-499a-8007-eb8fb231ee8e
Commits

Reviewing files that changed from the base of the PR and between ddf231a and 3c278e8.

Files selected for processing: 18

  • ffi/README.md
  • ffi/fixtures/api/examples/transcript.rs
  • ffi/fixtures/api/ffi.toml
  • ffi/fixtures/api/src/lib.rs
  • ffi/fixtures/reject/expected.txt
  • ffi/fixtures/reject/src/lib.rs
  • ffi/src/census.rs
  • ffi/src/collect.rs
  • ffi/src/ir.rs
  • ffi/tests/api.c
  • ffi/tests/api.cpp
  • ffi/tests/api.dart
  • ffi/tests/api.go
  • ffi/tests/api.kt
  • ffi/tests/api.py
  • ffi/tests/api.swift
  • quest/m1/ffi-export/refusals/README.md
  • quest/m1/ffi-export/refusals/generics.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated
kixelated enabled auto-merge November 10, 2026 15:23
@moq-bot
moq-bot Bot added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit e993054 Nov 10, 2026
10 checks passed
@moq-bot
moq-bot Bot deleted the quest/m1/ffi-export/refusals/generics branch November 10, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant