-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
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.
Activity
ADKaster commentedon Jun 11, 2025
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 commentedon Jun 11, 2025
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 theCxxSpan
protocol. If your Span type has the same layout you may be able to manually add a conformance usingSWIFT_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 thanstd::span
, that would be very useful context for us :)ADKaster commentedon Jun 11, 2025
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 fromDetail::Span<T>
intoSpan<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:
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 commentedon Jun 11, 2025
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:
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 commentedon Jun 11, 2025
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