Skip to content

feat(ffi): export generic types at the instances the config names - #103

Merged
kixelated merged 6 commits into
mainfrom
quest/m1/ffi-generic
Oct 27, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m1/ffi-generic

Conversation

@kixelated

@kixelated kixelated commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/ffi-generic.md, and deletes it.

Problem

The exporter refused every generic type. moq-ffi's media API uses several of them, each at one type, for example catalog::Producer<hang::Extra>. #94 found that refused generic types blocked about 67 of moq-ffi's partly covered items.

Approach

Config

A new instances table, beside exclude and rename, at the top level or under [crates.<pkg>]. Each key is an instance and each value is its C name without the prefix:

[crates.moq-mux.instances]
"catalog::Producer<moq_mux::catalog::hang::Extra>" = "catalog_producer"
  • The head path is from the crate root, as for every other key.
  • An argument is one of:
    • (), a primitive, or String;
    • an exported type, by its path from its package's name in snake case;
    • an instance, if that type is generic.
  • Trailing arguments left out take their defaults, so Pair<u32> is Pair<u32, u32>. A default must also be a type a key could spell, or the instance is refused at its span.
  • The key is parsed at load. Each of these is an error at the type's span, or with no span when nothing is found:
    • a path that names nothing;
    • a type that is not generic;
    • too many arguments;
    • a const parameter;
    • an unmet bound;
    • a duplicate.

Collector

  • A declared type is keyed by its DefId plus its erased arguments when it is generic. A generic type exports only at the named instances.
  • A signature that reaches any other instance is refused with "instance the config does not name X<..>" at its span. A note at the type's definition says the type exports only at the instances the config names.
  • An instance gets the methods of each inherent impl it meets: the impl's self type unifies with the instance, and the impl's where-clauses hold there. So impl Pair<u32> reaches only Pair<u32>. A method whose own where-clause on the impl's parameters fails at the instance is likewise left out (Pair::even, where L: PartialEq<R>).
  • An exclude can leave a method out of every instance, by the generic path (catalog::Producer::stats), or out of one instance, by its spelling (Pair<u8, dep_fixture::Size>::swap). The census lists a refused member by its instance spelling.
  • New type-parameter rule, applied only after the existing ones fail. It covers a method's own type parameter that is an argument of an exported generic type in the signature, such as Config<()>::with_catalog<E>(self, Catalog<E>) -> Config<E>.
    • The parameter takes the one argument of a named instance that meets its bounds and makes every exported generic type in the signature a named instance, a returned future's output included.
    • If several arguments qualify, it is refused as "generic parameter of several instances".

C

  • An instance's type is named by its instances value, and its methods through that name. For example, moq_catalog_producer_new(moq_broadcast_producer *, moq_catalog_config_extra *, ...).
  • The glue spells each instance with turbofish (::dep::Pair::<u32, u32>). Docs and messages spell it as Rust does.
  • An instance's associated consts export as getter functions.

moq

  • Named in ffi/moq/ffi.toml:
    • hang: Catalog<Extra> as hang_catalog.
    • moq-mux, all at Extra or at catalog::hang::Container:
      • catalog::{Config<()>, Config<Extra>, Producer, Reserved, Timebase}
      • catalog::hang::Consumer
      • container::{Consumer, GroupConsumer}
      • import::Container
  • Dropped excludes:
    • the generic types now named;
    • excludes left stale now that Producer/Reserved return the types, so the census no longer refuses them: import::{Track, TrackStream}, codec::*::Import, container::ts::Programs, and select::Broadcast::retain.
  • Added excludes for what the census still refuses:
    • methods of the new instances, such as Producer::json_snapshot and Producer::modify;
    • decode and finish of the codec imports and Programs.

Census

Each export at the pin. A cell reads before → after.

Export Types exported Types refused Methods and fields exported Refused Refused items
moq-net 119 33 515 36 91
moq-pattern 6 0 26 4 4
moq-clock 7 5 32 5 13
moq-tokio 61 36 217 88 173
hang 58 → 59 13 → 12 169 → 186 23 → 26 40 → 42
moq-mux 87 → 108 57 → 37 239 → 383 43 → 63 111 → 111
  • New method refusals. Refused methods went up because a refused type's methods are not counted, and the new instances bring methods that do not all map:
    • Producer::{json_*, binary_*} take a type parameter no rule picks.
    • Producer::modify returns a borrowing Guard.
    • Catalog<Extra>::media returns Catalog<()>.
  • moq-mux's refused items stay at 111. The 20 generic types that now export were replaced in the list by their refused methods.

Coverage of moq-ffi

356 items: moq-ffi's UniFFI functions, methods, and types, without tests. I re-scored the before header by the same method. My count differs from #94's 146 / 114 / 96 by a few judgment calls.

Covered Partial Not covered
Before 152 113 91
After 193 72 91

41 items moved from partial to covered:

  • 27 from the instances:
    • MoqBroadcastProducer (new, close) and MoqOriginProducer::create_broadcast
    • MoqMediaTimebase, MoqContainerInit, and MoqMediaCatalogProducer
    • MoqMediaContainerProducer: new, write, cut, seek, and finish
    • the catalog, track, and group consumers
  • 14 from dropping the stale excludes: all of MoqMediaTrackProducer and MoqMediaTrackStreamProducer.

What still blocks the 72 partial items, ranked:

  1. Generic methods still refused, 21:
    • the flate producers, 8;
    • the JSON producers, 10;
    • Producer::modify's borrowing Guard, 3.
  2. Config fields with getters but no setters, 16.
  3. Foreign std or serde types and iterators, 11.
  4. hang::catalog::Container exports as an opaque object, since Unknown holds a serde_json::Map, 7.
  5. No FromStr for Epoch or Pattern, 4.
  6. Option<&T> parameters of Source::resolve, 3.
  7. Other small groups, 10. One of them is DEFAULT_NAME on hang::Catalog<()>, which is not named (see Decisions).

Tests

  • api fixture. C compiled against the generated header runs the same script as the Rust transcript, and the two transcripts must match line for line. New in the fixture:
    • Pair<L, R = L>, a record, at three instances: Pair<u32> (a defaulted argument), Pair<Size, u8>, and Pair<u8, Size>.
      • swap returns one instance as the other.
      • impl Pair<u32> adds sum.
      • Pair<u32>::beside<T: Copy> resolves T to Size by the new rule. It checks u32 as u8 truncation from C.
      • One instance excludes swap by its spelling.
    • Stack<Counter>, an object whose handles move in and out.
  • reject fixture, each refusal at its span:
    • an unnamed instance, with its note;
    • a type parameter two instances could fill ("several instances");
    • an instance with an unmet bound;
    • an instance of a type that is not generic;
    • an instance of a path that names nothing (no span).
    • The test now stops a spanless error's block at the next diagnostic.
  • fn generic<T: Copy>(T) -> T stays refused. The new rule applies only to an argument of a generic type in the signature.

Impact

  • rust2-ffi config: new instances table, top-level and per crate. Nothing existing changes meaning.
  • rust2-ffi IR: Record, Enum, and Object gain args, the instance's type arguments. An instance keeps its generic type's path.
  • rust2-ffi census: a named instance that is itself refused (as a type would be) is listed under "and names none of these in instances", not in the exclude list.
  • moq's C API: gains moq_hang_catalog_* and moq_catalog_{config,config_extra,producer,reserved,timebase,hang_consumer}_*. It also gains moq_container_{consumer,group_consumer}_*, moq_import_container_*, moq_import_track_*, moq_import_track_stream_*, and the codec imports. No existing C name changes.

No MIGRATE entry, since nothing is released.

Alternatives

  • Instances under rename ("Producer<Extra>" = "catalog_producer"): one table fewer. But a rename renames something that exports anyway, while an instance creates an export. Keeping them separate keeps "names nothing exported" a clear error for each.
  • Auto-instancing every instance a signature reaches: no config needed. But C names would come from argument spellings, and a public signature could silently add types. The quest asked for an explicit list that refuses the rest.
  • Spelling arguments by Rust's use path (hang::Extra) instead of the IR root (moq_mux::catalog::hang::Extra): shorter, but an item has several paths, while the IR root is the one every other key and message uses.

Decisions

These are choices I made without asking. Each can be revisited.

  • Config shape: a separate instances table with explicit C names, as above. Recommend keeping it.
  • Argument spelling: by the package's snake-case root, which is the census's spelling, so a census line can be pasted. Recommend keeping it.
  • The type-parameter rule from instances. with_catalog needs it, since Producer::new takes Config<Extra> and only with_catalog builds one. Recommend keeping it. The other option is to name Config<Extra> and require moq to add a non-generic constructor.
  • A rename of a method by its generic path names it at every instance. Two named instances then clash, which is a loud error. I did not add per-instance renames, since no consumer needs one yet.
  • C names:
    • hang_catalog: the rule's moq_catalog would collide its accessors with the catalog module's types.
    • catalog_config for Config<()> and catalog_config_extra for Config<Extra>.
  • Instance consts are getter functions, not #defines, since a const's value may depend on the arguments. Recommend keeping this.
  • hang::Catalog<()> is not named. Naming it would export a second full catalog type for DEFAULT_NAME and default_subscription (1 partial item). Recommend leaving it out until a consumer needs more of it.

Follow-ups

  • Recorded in quest/m1/ffi-export.md under "Several crates":
    • the json_*/binary_* producers' type parameter;
    • modify's borrowing Guard;
    • Catalog<()>.
  • No moq change is needed for this PR.

Checks

  • just check and just test pass at 7dbf18f.
  • Codex on d0fa0b6 found that a defaulted argument such as Option<u32> bypassed the key's restrictions and ICEd. d687109 refuses it (reject fixture), reproduced against d0fa0b6.
  • Codex on d687109 found that a method's own bounds on the impl's parameters went unchecked, so the glue failed to build. 7dbf18f leaves such a method out (api fixture, Pair::even), reproduced against d687109.
  • Codex on 7dbf18f had no findings (thumbs up).
  • just ffi moq passes and prints ok.
  • just ffi census runs at the pin.
  • Not run: just ffi bench, which needs the bench shell.

(written by Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn

kixelated and others added 3 commits October 26, 2026 20:46
Quest-Claim: fcd028eb3bc03965
A config's `instances` names each instance a generic type exports at, with its C
name. A signature that reaches an instance the config does not name is refused
at its span. moq's config names the catalog producer, consumers, and import
container at moq-mux's untyped extension, and drops the excludes they left stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
@kixelated kixelated changed the title Export instances of generic types feat(ffi): export generic types at the instances the config names Oct 27, 2026
@kixelated
kixelated marked this pull request as ready for review October 27, 2026 05:48
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-27T16:54:56.267383Z
We couldn’t complete this review. Comment @codex review to try again.
1a0f670 New commits
🔒 Security Review ✅ Completed 2026-10-27T06:13:16.001388Z 7dbf18f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0fa0b6dab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ffi/src/collect.rs Outdated
Comment on lines +708 to +710
.map(|ty| match ty.kind() {
ty::Tuple(list) if list.is_empty() => Ok(Type::Unit),
_ => self.ty(ty, Pos::Behind),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate defaulted arguments against the supported instance types

When a generic argument is defaulted, instance_ty bypasses the explicit argument restrictions, and this fallback accepts types the generators cannot spell. For example, pub struct Wrapper<T = Option<u32>> { pub value: T } with instance "Wrapper" = "wrapper" reaches the IR with Type::Option as its argument, then panics in Api::arg during ABI generation instead of producing a source-spanned refusal. Reject unsupported defaulted arguments here, or support them consistently in both Rust spelling functions. This violates the root AGENTS.md requirement to report unsupported constructs as source-spanned errors.

AGENTS.md reference: AGENTS.md:L14-L14

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d687109, reproduced first: Holder<T = Option<u32>> named as "Holder" ICEd with "no type argument is Option". Every instance argument, a default included, must now export as one a key can spell ((), a primitive, String, a record, enum, or object). Otherwise the instance is refused at its span with "type argument the config cannot spell". The reject fixture covers it.

(written by Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)
Reviewed commit: d0fa0b6

One actionable issue, already independently reported in this finding: defaulted arguments bypass the supported instance-type restrictions (ffi/src/collect.rs:440-442, 705-712). A public-field Holder<T = Option<u32>> configured as "Holder" = "holder" reaches Api::arg with Type::Option and panics during ABI naming. Validate resolved defaults before IR construction, or support their spelling consistently; cover this with a regression test. Referencing the existing thread rather than duplicating it inline.

Direction: explicit instances and C names keep the export surface predictable; reusing normal collection and rejecting ambiguous inference is preferable to automatic instancing. No additional actionable bugs found in the changed collector, ABI/glue, configuration, and fixtures.

Verification: static GitHub inspection, including an independent collector review. I did not run the compiler, fixtures, or benchmarks.

(Written by OpenAI)

A defaulted argument skipped the key's restrictions, so `Holder<T = Option<u32>>`
reached the IR with an `Option` argument and panicked while naming it. Each
instance argument must now export as one a key can spell, or the instance is
refused at its span.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d68710966a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ffi/src/collect.rs
Comment on lines +1056 to +1058
match self.function(item.def_id, method, Some(self_ty), Some(parent)) {
Ok(function) => methods.push(function),
Err(why) => refused.push((method, span, why)),
Err(why) => refused.push((shown, span, why)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check method-level bounds after substituting instance arguments

When a method constrains an impl parameter in its own where clause, its bounds are never checked if it has no type parameters of its own: impl_args checks only the impl's clauses, and instantiate skips the inherited parameters without calling choose. For example, exporting Wrapper<String> for pub struct Wrapper<T> { pub value: T } with pub fn copied(&self) -> T where T: Copy incorrectly exports copied, even though String is not Copy. The exporter succeeds but generates a call that fails when the generated crate is built. Check the method's instantiated predicates before accepting it, and report an unavailable method at its source span rather than emitting unusable glue.

AGENTS.md reference: AGENTS.md:L14-L14

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7dbf18f, reproduced first: Pair<L, R>::even (where L: PartialEq<R>) exported at Pair<Size, u8>, and the glue failed with E0277. I did not refuse the method. It is now left out of an instance whose bounds it fails, the same way an impl whose where-clauses fail is left out, since Rust has no such method at that instance. Bounds on the method's own type parameters still go to the type-parameter rules. The api fixture calls even from C at Pair<u32>, where it holds.

(written by Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)
Reviewed commit: d687109

Reviewed the one-commit delta from d0fa0b6. The previous default-argument panic finding is fixed: Collector::args now rejects IR argument kinds the formatters cannot spell, including defaulted Option<u32>, before ABI generation. The reject fixture checks the source-spanned refusal.

No new actionable bugs found. Central validation is the simpler, appropriate fix and keeps the explicit instance grammar unchanged.

Verification: static GitHub comparison of the collector and three fixture changes; I did not run tests or reproduce the compiler execution locally.

(Written by OpenAI)

A method's `where` clause on its impl's parameters went unchecked, so
`Pair<Size, u8>` exported `even` (`where L: PartialEq<R>`) and the glue failed
to build. Such a method is not one of that instance's, as an impl whose bounds
fail is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)
Reviewed commit: 7dbf18f

Reviewed the one-commit delta from d687109. The independent method-bound finding is fixed: applies() substitutes impl arguments and filters methods with failed concrete predicates before emitting glue, while leaving method-generic predicates to type selection. The Pair::even fixture covers an available instance and makes invalid emission for the other configured instances fail the generated-crate build. The earlier default-argument validation remains intact.

No new actionable bugs found. Filtering methods that cannot be called at an instance is consistent with impl filtering and avoids adding a separate configuration workaround.

Verification: static GitHub inspection of the collector, docs and differential fixture changes, with an independent second pass. Tests and generated glue were not executed by this review.

(Written by OpenAI)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@kixelated
kixelated merged commit 8a90a0f into main Oct 27, 2026
1 check passed
@kixelated
kixelated deleted the quest/m1/ffi-generic branch October 27, 2026 20:32
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