Protocol existential wrappers in C++ generated header

Motivation

Swift's C++ interop can represent concrete types on both sides of the boundary. Classes get RefCountedClass wrappers with retain/release lifecycle. Structs get value-type wrappers with VWT-delegated copy and destroy. But there is no C++ representation for Swift protocols, and that gap blocks more than just protocol-typed function signatures.

The deeper problem is generics. Zoe Carver's Bridging C++ Templates with Interop pitch (February 2022) laid out the design for importing C++ function templates into Swift: specialize at the call site, constrain via protocol archetypes, and bridge class templates through protocol conformance extensions. That pitch established protocol conformance as the bridging mechanism between Swift generics and C++ templates, and identified the key limitation that class template extensions "probably can't make it across library boundaries." But it addressed only one direction. C++ libraries express generic interfaces through templates and, increasingly, C++20 concepts. Swift libraries express them through protocols and existentials. There is no representation for Swift protocols in C++, which means a C++ function template constrained by a concept cannot accept a Swift value, and a Swift function returning any P cannot be called from C++ at all. Every any P parameter or return type in a Swift API is unavilable to C++ callers.

The August 2022 workgroup meeting drew the boundary explicitly: "if a generic function has specific constraints, we should respect them in C++ as well and figure out some way of type-checking them from C++." In the same meeting, Zoe noted that C++ code would not be able to create new types conforming to Swift protocols. This framing points to exactly the representation gap that existential wrappers fill. C++ callers do not need to define new conforming types; they need to receive, hold, and operate on values that already conform. That is what existentials are for.

The community has felt this gap as well. Himeshi's Exposing Swift protocols to C++ as pure abstract classes thread (October 2025) asked how protocols should appear in generated headers. The discussion converged on the idea of a SwiftExistential container type rather than pure abstract classes, since abstract class hierarchies cannot represent Swift's value-typed existential semantics (inline buffer, VWT lifecycle) or class-bound existentials (retained pointer, isa-based metadata recovery) without object slicing hazards.

Protocol existential wrappers close this gap from the bottom up: Each Swift protocol exposed to C++ gets a wrapper class that holds the same existential container the Swift runtime uses (type metadata, witness table, inline buffer or heap-allocated value). The wrapper's public methods dispatch through the protocol witness table, so C++ callers get the same type-erased polymorphism Swift callers get. Boxing constructors on each wrapper accept concrete conforming types, which means std::convertible_to<Circle, Drawable> holds automatically. C++ template code can constrain on protocol conformance without any annotation or concept import machinery.

This is the foundation for two follow-on pieces of work. First, the compiler can auto-generate C++20 concepts from Swift protocol declarations (one concept per protocol, structural satisfaction, gated on __cpp_concepts). Those concepts give C++ developers named constraints to write generic code against Swift types. Second, the Swift frontend can resolve concept constraints at call sites and instantiate C++ templates directly, completing the reverse of Zoe's original pitch: greet(myDog) in Swift wraps into the appropriate C++ type and calls the template specialization. Both future features depend on the wrapper types this pitch introduces.

Round-trip import closes the loop in the other direction. When C++ code uses a generated existential wrapper type in a function signature, the ClangImporter recognizes the base class hierarchy and recovers the any Protocol type on re-import. Swift can call back into C++ code that operates on Swift existentials without any manual bridging.

Design

Existential container layout

The wrappers match Swift's runtime containers exactly. Opaque existentials (SwiftExistentialType) are 40 bytes for a single-protocol existential: three words of inline buffer, one word of type metadata, one word for the witness table. Class-bound existentials (SwiftClassExistentialType) are 16 bytes: one retained class pointer plus one witness table. Protocol inheritance does not add witness tables. any Hashable where Hashable: Equatable has exactly one WT; the Hashable table includes entries for all inherited Equatable requirements. Multiple witness tables only arise from ad-hoc compositions (any A & B), which are deferred.

Base classes

SwiftExistentialType lives in _SwiftCxxInteroperability.h alongside RefCountedClass. It delegates all lifecycle operations to the value witness table (copy, move, assign, destroy) and provides a _loadWitness template that indexes into a witness table at a compile-time offset with pointer authentication. SwiftClassExistentialType is the class-bound variant: it uses swift_retain/swift_release for lifecycle and recovers type metadata via swift_getObjectType (isa pointer read) rather than storing a _type field. swift::Any is a non-final subclass of SwiftExistentialType with zero witness tables. Marker protocols inherit from it, and it serves as the default template parameter for unconstrained primary associated types.

Per-protocol wrappers

Each non-marker, non-@objc protocol gets a final class inheriting from SwiftExistentialType or SwiftClassExistentialType depending on whether the protocol requires a class constraint. The wrapper has a single _witnessTable member. Protocol requirement methods dispatch through _loadWitness, passing the projected value pointer, type metadata, and witness table to the witness function. Methods inherited from base protocols use two-level dispatch: load the base protocol's witness table from a known offset in the derived table, then dispatch through that. Conversion methods (asDrawable()) copy the existential container into a smaller base-protocol wrapper.

Protocols with primary associated types emit as C++ class templates. any Container<Int> maps to Container<swift::Int>. The template parameter is a compile-time type tag only; it does not affect container layout. Nested PATs (any Container<any Container<any Drawable>>) map to Container<Container<Drawable>> via recursive type visiting.

Marker protocols emit as final subclasses of swift::Any with no members.

Boxing and function signatures

For each same-module conformance the compiler emits a boxing constructor on the wrapper. Drawable(const Circle&) packs the concrete value into the existential container using the conformance's witness table. Class-bound boxing retains the class pointer directly. These constructors are implicit conversions, so std::convertible_to<T, Drawable> provides a generic boxing constraint for free.

Functions taking or returning existential types are emitted with thunk bodies that pack and unpack the containers. Parameters extract the opaque pointer from the wrapper; returns wrap the thunk result into a new wrapper instance. Class-bound parameters use swift_interop_passDirect_ for loadable direct-pass encoding.

Stdlib conformance records

Equatable, Hashable, and Comparable get existential wrappers in the scaffolding header. Since cross-module protocol witness tables are lazily instantiated at runtime (not emitted as standalone globals), conformance records use swift_getWitnessTable backed by conformance descriptors with a static const cache. Free operator templates (operator==, operator<) and std::sort/std::find compatibility follow from this.

Existential round-trip import

The ClangImporter recognizes existential wrapper types by walking the base class hierarchy to SwiftExistentialType or SwiftClassExistentialType. It uses getSwiftSourceSymbolAttr() to look through ClassTemplateSpecializationDecl to the primary template for the SWIFT_SYMBOL attribute. buildExistentialTypeForProtocol() extracts PAT template arguments, detects default args (swift::Any), and recursively imports nested existential wrapper args. Wrapper class templates and their specializations are skipped in ImportDecl to prevent duplicate import.

Current limitations

Ad-hoc protocol compositions (any Drawable & Resizable) are deferred; the workaround is a combined protocol. Stdlib protocol existentials (any Hashable, any Equatable) are not yet usable in function signatures. Throwing, mutating, and static protocol requirements are excluded from method emission. Protocol requirements with associated type parameters (as opposed to primary associated type constraints on the existential itself) are also excluded.

Implementation

The existential wrappers work is split into 10 commits on the swift-existential-type branch and the first two commits are up for review at #90449. I'd love feedback on the design and implementation.

1 Like

It would be nice to have a plan for addressing this so that it doesn’t require a whole new mechanism to be invented later. Now that we have noncopyable types, compositions of the form any P & ~Copyable need to be supported as well. I think they are broken at the SIL level currently anyway, but that needs to be fixed, and we shouldn’t rule out supporting this in other layers.

Would it be possible to separate out the protocol part from the existential part, so that each protocol emits a type that represents the witness table, and then SwiftExistentialType/SwiftClassExistentialType could perhaps be a variadic generic template? You could still emit type aliases for the single protocol case, perhaps AnyP would be an alias for SwiftExistentialType<P> or SwiftClassExistentialType<P> as appropriate. This would also eliminate Any as a special case, since that’s an empty composition today.

2 Likes

@Slava_Pestov great suggestion! I was also frustrated by the variable length WT array for compositions but I think variadic templates are strictly better than the subclass model proposed above and a great fix.

In code, this could work out with template packs:

The pack carries three kinds of C++20 concept-constrained tags:

  • ProtocolTag -- non-marker protocols: each contributes one WT slot
  • MarkerTag -- @_marker protocols like Sendable: zero WT contribution but distinct C++ types for type-safety when calling back into Swift
  • InverseTag -- NonCopyable and NonEscapable: modify lifecycle semantics but don't contribute WTs
// Opaque existentials: [buffer: 24][metadata: 8][WTs: N * 8]
template <typename... Tags>
  requires ((ProtocolTag<Tags> || MarkerTag<Tags> || InverseTag<Tags>) && ...)
class SwiftExistentialType {
  static constexpr size_t NumWitnessTables = /* count ProtocolTags */;
  ...
  // ~Copyable enforcement
  SwiftExistentialType(const SwiftExistentialType&)
    requires(IsCopyable);

  // Implicit conversion to Any and subset compositions
  template <typename... SubTags> operator SwiftExistentialType<SubTags...>() const;
};

// Class-bound: [class pointer: 8][WTs: N * 8]
// Same tag system, swift_retain/swift_release lifecycle.
template <typename... Tags> requires (...)
class SwiftClassExistentialType { ... };

Per-protocol wrappers are thin final classes that inherit the template and add only protocol requirement methods:

class AnyDrawable final
    : public swift::_impl::SwiftExistentialType<_impl::Drawable> {
public:
  void draw() const;  // out-of-line, dispatches through WT
};

From this change we get the following:

  • Any is just SwiftExistentialType<> (empty pack).
  • Compositions -- any Drawable & Resizable = SwiftExistentialType<Drawable, Resizable>, 2 WTs. Implicit subset conversion could handle any Drawable & Resizable -> any Drawable without per-protocol asDrawable() methods.
  • Markers -- any Drawable & Sendable = SwiftExistentialType<Drawable, Sendable>, still 1 WT for ABI compatibility but a distinct C++ type from SwiftExistentialType<Drawable> that would be API incompatible with swift functions that require Sendable.
  • ~Copyable gets real C++ enforcement by removing the copy constructor. ~Escapable is type distinction only (C++ has no lifetime-dependency enforcement; best-effort [[clang::lifetimebound]] could be emitted).
  • Binary size -- template instantiations with the same NumWitnessTables share identical copy/move/destroy code and ICF merges them.
1 Like

Thanks for the proposal!

Funnily enough, there was a WG21 paper about protocols in C++: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4148r0.pdf

Based on this proposal, I do agree that this seems to be a good approach to tackle this problem.

I am a bit more concerned about this. C++ concepts and Swift protocols are very different language features and I am wondering what advantage does this bring. If we already have a wrapper class for a protocol, a C++ function can take that wrapper class as a concrete type void f(Drawable& d); without any concepts or template machinery. If we want to support multiple protocol conformances, we could have a templated existential wrapper template and each of the concrete protocol implementation could be a mixin (the Existential type could be a CRTP, deriving from each of the types representing the individual protocols), like : void f(Existential<Drawable, Printable>& e);. So I think we could support these wrappers on the C++ side without any of the complexities that come with concepts. This way the C++ code is clearly type erased, and we do not need to mix type erasure with compile-time parametric polymorphism. I find this approach a bit cleaner.

And the other side, calling these C++ functions from Swift, we could import them with type erased parameters, completely sidestepping the template instantiation problem.

So I think I would like to know more whether this type erasure only approach could work for your use cases, and if it cannot why not. Templates and concepts are complex beasts so I would try to avoid relying on them unless there is a very strong reason to do so.

I would flip the inheritanve and would make SwiftExistentialType and co use the CRTP, so we can have SwiftExistentialType<Protocol1, Protocol2, ...> . This way we can support compound protocols.

1 Like

Thanks for the link to the paper, super interesting and related!

I've incorporated the suggestions and rewritten the stack as a type-erasure-only model (i was working on basically these suggestions this week). Named protocol wrappers are concrete final classes inheriting from the variadic template (class Drawable final : public SwiftExistentialType), so C++ functions just take const Drawable&and there are no concepts in the user-facing API. Compositions fall out naturally from the variadic template:any Drawable & Resizableemits asSwiftExistentialType<DrawableTag, ResizableTag>` with implicit subset conversion operators.

The way function parameters work is that a Swift function taking any Drawable emits a C++ thunk whose parameter type is const SwiftExistentialType<DrawableTag>& rather than const Drawable&, and since Drawable inherits from that base template, passing a Drawable value is just an implicit base conversion. Composition values convert via the subset conversion operator, so the type hierarchy gibe you both:

  1. named wrappers give you ergonomic types to work with
  2. the base template types give you generic parameter acceptance across the wrapper hierarchy.

The one place where we do use concepts is internal to the scaffolding template, for tag classification (ProtocolTag, MarkerTag, InverseTag) and requires clauses on things like copy constructor deletion for ~Copyable protocols. I don't think there's a good way around this without C++20 concepts; the SFINAE equivalent would be significantly more complex, especially for the subset conversion operators where we need ContainedIn<T, Pack...> fold expressions and constexpr witness table index remapping. The tag indirection itself (lightweight structs like DrawableTag rather than the wrapper types as template args) is needed because the wrapper class can't appear as a template argument to its own base class in the generated header, but otherwise the shape is what you described.

Two areas where this could expand in the future:

  1. Swift Generics: Swift generic functions like func f(_ t: some Drawable) and func f<T: Drawable>(_ t: T) are identical at the ABI level, both taking (value, type_metadata, witness_table) as hidden parameters, and a C++ thunk could extract those from the conformance records and TypeMetadataTrait we already emit to call the generic entry point without boxing into an existential container. Since Swift generics aren't mono-morphized across module boundaries, the performance difference is really just the container packing overhead, so the existential path with boxing constructors covers the common case well enough for now.

  2. Template specialization: Additionally, the type-erasure model composes well with the template instantiation direction because the wrapper types are concrete classes with real methods that structurally satisfy C++ concepts without any explicit bridging, so a user-written concept HasDraw = requires(const T& t) { t.draw(); } would be satisfied by our Drawable wrapper as-is, and when Swift eventually needs to instantiate concept-constrained C++ templates the wrapper type can serve as the template argument directly.

1 Like