Skip to content

Strip a type prefix, and always use the bundled tl::expected - #4

Merged
kixelated merged 2 commits into
kixelated/uniffi-0.32from
kixelated/cpp-shape
Oct 17, 2026
Merged

kixelated merged 2 commits into
kixelated/uniffi-0.32from
kixelated/cpp-shape

Conversation

@kixelated

Copy link
Copy Markdown
Owner

Two generator changes for moq's C++ package, which ship in one fork release (v0.11.0-kixelated.5+v0.32.2) and one pin bump in moq-dev/moq.

strip_type_prefix

[bindings.cpp] strip_type_prefix = "Foo" names the records, objects, enums, and callback interfaces FooClient/FooError as Client/Error. A name continues only after an uppercase letter (Foobar stays), custom types keep their names (custom_types is keyed by them), and a rename onto an existing type fails generation. moq-ffi prefixes every type with Moq for languages without namespaces; with this, cpp/moq drops its ~100-line alias list and each type has one name.

It renames through uniffi_bindgen::interface::rename (the same machinery as Kotlin's rename table), but uniffi 0.32's visitor has two gaps:

  • It skips a constructor's error type, so the type oracle (as_codetype) also renames every type it renders.
  • It leaves ComponentInterface::get_type keyed by the old names, so sorted_types looks types up by name instead.

The C backend reads the same [bindings.cpp] config but ignores this option: its names are snake case under the namespace, so the prefix is not redundant there.

tl::expected at every standard (BREAKING)

error_style = "expected" used std::expected when the standard library had it and the bundled tl::expected otherwise, so bindings built at C++17 and code built at C++23 disagreed on the type. It now always uses the bundled tl::expected, which has the same members. With exceptions disabled, value() on an error printed nothing and hit __builtin_unreachable(); the vendored copy now prints uniffi: expected::value() called on an error and aborts, like a misused future.

Tests

  • expected-test generates uniffi_c_api with strip_type_prefix = "Capi" and checks capi::Store::open and its capi::Error, including a throwing constructor (the uniffi gap).
  • expected-value-on-error-test checks the abort message.
  • strip-type-prefix-collision-test checks the collision refusal.

Locally: the full ctest suite (without memcheck; no valgrind here) passes with GCC 16, cargo fmt --check is clean, and the expected test syntax-checks with Clang 21 at C++17 with -fno-exceptions. moq's just cpp check passes against this generator (C++17, C++23, C++23 over C++17 bindings, pkg-config, doc samples).

Open decisions

  • The expected change is unconditional rather than an option. A switch would keep the C++17/C++23 mismatch as a footgun for any user who turns it off, and this fork's only consumer wants it on. Recommend unconditional.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

`strip_type_prefix = "Foo"` names `FooClient` as `Client`, so a crate that
prefixes its types for languages without namespaces needs no alias list in
C++. It renames through uniffi's `rename`, which skips a constructor's error
type and leaves `get_type` keyed by the old names, so the type oracle renames
each type it renders as well and `sorted_types` looks types up by name. A
rename onto an existing type fails generation.

`error_style = "expected"` now always uses the bundled `tl::expected`, never
`std::expected`, so bindings built at C++17 link with code built at C++23
instead of silently disagreeing on the type. With exceptions disabled,
`value()` on an error aborts with a message instead of reaching
`__builtin_unreachable()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit to moq-dev/moq that referenced this pull request Oct 16, 2026
uniffi.toml sets the generator fork's new `strip_type_prefix = "Moq"`, so
moq-ffi's `MoqClient` is generated as `moq::Client` and moq.hpp drops its
alias list and the `just cpp check` audit of it. The fork also makes
`uniffi::expected` the bundled `tl::expected` at every C++ standard, so
the MOQ_ABI link guard and the "C++ standard" docs go away, and `value()`
on an error aborts with a message instead of being undefined. The check
now builds the probe at C++23 over bindings at C++17 to prove they link.

The generator pin is the head of kixelated/uniffi-bindgen-cpp#4 until
that PR is released as v0.11.0-kixelated.5+v0.32.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 17, 2026 02:11
@kixelated

Copy link
Copy Markdown
Owner Author

Reviewed the full diff ahead of the v0.11.0-kixelated.5+v0.32.2 release, which moq-dev/moq#5727 needs. No blocking defects.

  • strip_type_prefix renames through uniffi's own rename (the Kotlin backend's mechanism), keyed by crate, and also covers the one type uniffi misses (a constructor's error type). Exact-prefix (Moq) and lowercase-continuation (Moqx) names are skipped, and collisions with local custom types are refused. Tests cover the constructor-error gap, the abort message, and the collision refusal.
  • Always using the bundled tl::expected removes the std::expected branch; the vendored header now aborts with a message instead of __builtin_unreachable(), reachable only from value() with exceptions off.

Non-blocking edge cases, none of which affect moq (one crate, C backend unused):

  1. The rename table merges across crates and applies to every component, so a second crate with its own local type of the same old name would print under the new name.
  2. The collision check only covers local types, so a rename onto an imported type name isn't refused.
  3. --lang c ignores strip_type_prefix and writes its own unrenamed C++ files, so generating C and C++ into one directory with the option set overwrites the renamed files (a pre-existing risk).

Added the release commit (f42a415), following the earlier release PRs.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 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 ✅ Completed 2026-10-17T02:14:33.530893Z f42a415 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-17T02:14:22.190295Z f42a415 Draft marked ready
ℹ️ 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: f42a41584e

ℹ️ 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 on lines +83 to +87
let flat: HashMap<String, String> = renames
.values()
.flatten()
.filter_map(|(old, new)| Some((old.clone(), new.as_str()?.to_string())))
.collect();

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 Keep fallback renames scoped to their defining crate

When library mode generates multiple components, flattening the crate-keyed rename tables and copying the result to every component loses the type's module identity. If one generated crate strips FooError while another component imports an unrenamed FooError from a crate not present in renames—or two crates map the same spelling differently—renamed() rewrites that reference using this global entry even though the defining header retains the original name, producing uncompilable bindings. Preserve the crate/module key in the fallback mapping.

Useful? React with 👍 / 👎.

Comment on lines +284 to +288
let found = self
.ci
.iter_local_types()
.chain(self.ci.iter_external_types())
.find(|t| type_name(t) == Some(name));

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 Distinguish external types in dependency lookup

When stripping a prefix makes a local record share a simple name with an imported record—for example, local FooCoordinates becomes Coordinates while one of its fields is ext::Coordinates—this name-only, local-first lookup conflates the two. The dependency graph can consequently contain a false Coordinates -> Coordinates self-edge and report a cyclic dependency even though the C++ types are valid in separate namespaces. Use the module path as part of topology keys and lookup.

Useful? React with 👍 / 👎.

@kixelated
kixelated merged commit af17091 into kixelated/uniffi-0.32 Oct 17, 2026
5 checks passed
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