Skip to content

[cxxinterop] Inherited constructors incompatible with SWIFT_NONESCAPABLE annotation #82183

@ADKaster

Description

@ADKaster
Contributor

Description

After watching the "Safely mix C, C++ and Swift" video from WWDC 2025, I tried to apply SWIFT_NONESCAPABLE to ladybird's Span class.

https://github.com/LadybirdBrowser/ladybird/blob/00f76ccbf4f63cee30e80edae8a688dbd40874be/AK/Span.h

The use of inherited constructors by this class seems to trip up the clang importer.

Reproduction

Here's a moderately reduced Span class:

Test.h

#pragma once

#include <swift/bridging>

namespace Detail {
template<typename T>
class Span {
public:
   constexpr Span() = default;
   constexpr Span(T* p, unsigned long s) : m_ptr(p), m_size(s) {}

   template<unsigned long size>
   constexpr Span(T (&a)[size]) : m_ptr(a), m_size(size) {}
protected:
  T* m_ptr { nullptr };
  unsigned long m_size { 0 };
} SWIFT_NONESCAPABLE;

}

template <typename T>
class Span : public Detail::Span<T> {
public:
  using Detail::Span<T>::Span;

  constexpr Span() = default;

  constexpr T const* data() const { return this->m_ptr; }
  constexpr T* data() { return this->m_ptr; }

  constexpr unsigned long size() const { return this->m_size; }

} SWIFT_NONESCAPABLE;


template<typename T>
using ReadonlySpan = Span<T const>;

using ReadonlyBytes = ReadonlySpan<unsigned char>;
using Bytes = Span<unsigned char>;

module.modulemap

module MyCxx {
   header "Test.h"
   requires cplusplus
   export *
}

main.swift

import MyCxx

func doSomethingWith(_ s: Bytes) {}

Compile with:

swiftc -I. -cxx-interoperability-mode=default -I$(swiftc -print-target-info | jq -r '.paths.runtimeResourcePath + "/../../include"') main.swift

or swiftc -I. -cxx-interoperability-mode=default main.swift on macOS.

Errors:

/home/andrew/ladybird-org/swift-test-apps/nonescapable-span/./Test.h:24:26: error: an initializer cannot return a ~Escapable result
22 | class Span : public Detail::Span<T> {
23 | public:
24 |   using Detail::Span<T>::Span;
   |                          `- error: an initializer cannot return a ~Escapable result
25 | 
26 |   constexpr Span() = default;

Expected behavior

Compiles with no errors

Environment

Swift version 6.2-dev (LLVM b5d039be1fbae13, Swift 4fb4945)
Target: x86_64-unknown-linux-gnu
Build config: +assertions

swiftly main-snapshot-2025-06-03

Additional information

It would be extremely preferable if my span type would be treated the same as std::span :)

#81634 seems to have added special treatment for std::span here in 262a53f.

cc @hnrklssn @j-hui

Activity

added
bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.
triage neededThis issue needs more specific labels
on Jun 11, 2025
ADKaster

ADKaster commented on Jun 11, 2025

@ADKaster
ContributorAuthor

Actually wait, there's a lot of special logic for std::span, in this compiler helper class... https://github.com/swiftlang/swift/blob/1869832929c5076b837b744022e10197a7e04522/stdlib/public/Cxx/CxxSpan.swift

... how do I tell swift's ClangImporter and Swiftify components that my classes are just as good as ::std's?

hnrklssn

hnrklssn commented on Jun 11, 2025

@hnrklssn
Contributor

So there are some separate issues here.

Firstly, it does indeed look like error: an initializer cannot return a ~Escapable result does not point to an initializer, which is at best the wrong source location, or at worst an incorrect error. Thanks for filing an issue!

Secondly, ClangImporter is aware of the std::span type and automatically adds the conformance to the CxxSpan protocol. If your Span type has the same layout you may be able to manually add a conformance using SWIFT_CONFORMS_TO_PROTOCOL(CxxSpan). @Xazax-hun will be able to provide more context on whether this is a bad idea or not.

Thirdly, there is currently no way to make ClangImporter create safe overloads for types other than pointers and std::span. If you could expand on why you prefer to use your own class rather than std::span, that would be very useful context for us :)

ADKaster

ADKaster commented on Jun 11, 2025

@ADKaster
ContributorAuthor

look like error: an initializer cannot return a ~Escapable result does not point to an initializer,

In theory it does point to an initializer. Or at least, it should. This is the inherited constructor case from https://en.cppreference.com/w/cpp/language/using_declaration.html#Inheriting_constructors. So we're importing the constructors from Detail::Span<T> into Span<T>. Whether swift's frontend synthesizes initializers that make sense given this pattern is indeed a question.

I would rather not rely on forcing the compiler to conform our class to a special protocol like CxxSpan. Presumably there's a lot of extra magic happening behind the scenes than just protocol conformance to make std::span behave.

For the "why use your own special class", there's a few reasons.

Pragmatically, we have been using this class for many years and its usage is widespread throughout our codebase. It has a slightly different API surface from std::span. Switching consumers of this class to use std::span instead would be a large refactor for little gain.

Switching the internals of this class to use std::span instead of a pointer + size might help convince swift's frontend that it's indeed a span, but that would lead to the other reasons we don't want to use std::span: It pulls in a bunch of std:: classes, headers, and idioms. Given that this is a fundamental datatype to our entire codebase, we'd be pulling in std:: in a lot of places that we didn't previously. For example, our codebase doesn't use <type_traits>. We have our own implementaitons. Pulling in std::span would likely end up pulling in duplicated definitions for a lot of things that we've already implemented ourselves, increasing compile times for little to no gain in the common case of compiling C++ code with a C++ compiler.

In the WWDC25 video, the presenter mentioned that std::span's operator[] and other related accessors are not bounds-safe. This is not the case with our class. We heavily use release assertions. Our operator[] and at() methods are bounds-safe.

So mostly it comes down to:

  • We've been using our own class and it works fine
  • We don't want to drag in STL headers when we don't need to, we already have replacements for the majority of them
  • It's easier to control the inlining and compile time behavior of our own class than it is to rely on std.
  • Our class has release-assertions for bounds access always on, without relying on a 'hardening' flag to the entire STL. Even our operator[](int) and at() methods do release-assert bounds checking.

I suspect that our codebase is not the only place with these concerns. folly, abseil, boost, and other STL-like libraries all have their own span classes. I believe WebKit (and possibly even LLVM) are in the process of replacing their custom classes with std:: as the language version increases, but I don't think that approach is going to work for all projects that have a large history of using custom span-like classes.

Edit: Seems folly's span is std::span in c++20+, but abseil keeps their span around and distinct from std::span for various reasons https://github.com/abseil/abseil-cpp/blob/9ac131cf7da4de8b19b4c956a0c13edba74d92d9/absl/types/span.h#L33

Xazax-hun

Xazax-hun commented on Jun 11, 2025

@Xazax-hun
Contributor

Ah, I agree that the error messages here are not great at the moment :/

For ~Escapable types we always need to annotate the lifetimes. The constructors are creating a new object and the compiler needs to know where the lifetimes are coming from.

For example, one of the constructors should be something like:

constexpr Span(T* p [[clang::lifetimebound]], unsigned long s) : m_ptr(p), m_size(s) {}

I haven't looked at the full example yet, will try to do so tomorrow. But adding lifetimebound annotations should get rid of the error, if it does not, we have a compiler bug that we need to address (other than the bugs we already mentioned in this thread like the subpar diagnostics.)

A side note: we import std::span as a regular, escapable type. We just generate an overload with Swift's own Span that is not escapable. Marking a type non-escapable is a big undertaking because all types that have this type as a member would also need to be non-escapable, and all functions returning non-escapable types need to have lifetime annotations.

Also, some swift protocols like the Sequence protocol is not compatible with non-escapable element types, so we cannot conform containers of non-escapable types to most protocols.

There are some new protocols under development that will solve some of these problems but they are not yet ready. Because of this, it might be too early to mark a frequently used vocabulary type non-escapable, but it is functional enough for some targeted use cases where it is mostly used in the interoperating layers for additional safety. E.g., you could introduce a second span type that your own span type can be converted to and use it tactically for some APIs used for interoperability.

We plan to improve the ergonomics around non-escapable types but it will take some time :/

ADKaster

ADKaster commented on Jun 11, 2025

@ADKaster
ContributorAuthor
constexpr Span(T* p [[clang::lifetimebound]], unsigned long s) : m_ptr(p), m_size(s) {}

Changing the constructor to this doesn't make the error go away. In fact, it makes the compiler repeat the same error twice 😅

Applying [[clang::lifetimebound]] to the array constructor makes it go back to only posting the error once.


Re: virality of nonescapable, that makes sense. I'll hold off on applying that to our span type at the moment then. The ... aggressive compiler warnings that "aaaaaa you are using a nonescapable type here but didn't annotate lifetime" were already a decent deterrent, barring the error here.

https://gist.github.com/ADKaster/9b7085014ac77c7da56d3d94ba43c929

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.c++ interopFeature: Interoperability with C++triage neededThis issue needs more specific labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @Xazax-hun@ADKaster@hnrklssn@fahadnayyar

        Issue actions

          [cxxinterop] Inherited constructors incompatible with SWIFT_NONESCAPABLE annotation · Issue #82183 · swiftlang/swift