Skip to content

PrintAsClang: Print a C block in the compatibility header for @cdecl functions #80917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions lib/PrintAsClang/DeclAndTypePrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,14 @@ class DeclAndTypePrinter::Implementation
return false;
}

std::optional<PrimitiveTypeMapping::ClangTypeInfo>
getKnownType(const TypeDecl *typeDecl) {
if (outputLang == OutputLanguageMode::C)
return owningPrinter.typeMapping.getKnownCTypeInfo(typeDecl);

return owningPrinter.typeMapping.getKnownObjCTypeInfo(typeDecl);
}

/// If \p typeDecl is one of the standard library types used to map in Clang
/// primitives and basic types, print out the appropriate spelling and
/// return true.
Expand All @@ -2297,8 +2305,7 @@ class DeclAndTypePrinter::Implementation
/// for interfacing with C and Objective-C.
bool printIfKnownSimpleType(const TypeDecl *typeDecl,
std::optional<OptionalTypeKind> optionalKind) {
auto knownTypeInfo =
owningPrinter.typeMapping.getKnownObjCTypeInfo(typeDecl);
auto knownTypeInfo = getKnownType(typeDecl);
if (!knownTypeInfo)
return false;
os << knownTypeInfo->name;
Expand Down Expand Up @@ -2999,6 +3006,22 @@ bool DeclAndTypePrinter::shouldInclude(const ValueDecl *VD) {
return false;
}

// In C output mode print only the C variant `@cdecl` (no `@_cdecl`),
// while in other modes print only `@_cdecl`.
std::optional<ForeignLanguage> cdeclKind = std::nullopt;
if (auto *FD = dyn_cast<AbstractFunctionDecl>(VD))
cdeclKind = FD->getCDeclKind();
if (cdeclKind &&
(*cdeclKind == ForeignLanguage::C) !=
(outputLang == OutputLanguageMode::C))
return false;

// C output mode only accepts @cdecl functions.
if (outputLang == OutputLanguageMode::C &&
!cdeclKind) {
return false;
}

if (VD->getAttrs().hasAttribute<ImplementationOnlyAttr>())
return false;

Expand Down
11 changes: 11 additions & 0 deletions lib/PrintAsClang/ModuleContentsWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,17 @@ void swift::printModuleContentsAsObjC(
.write();
}

void swift::printModuleContentsAsC(
raw_ostream &os, llvm::SmallPtrSetImpl<ImportModuleTy> &imports,
ModuleDecl &M, SwiftToClangInteropContext &interopContext) {
llvm::raw_null_ostream prologueOS;
llvm::StringSet<> exposedModules;
ModuleWriter(os, prologueOS, imports, M, interopContext, getRequiredAccess(M),
/*requiresExposedAttribute=*/false, exposedModules,
OutputLanguageMode::C)
.write();
}

EmittedClangHeaderDependencyInfo swift::printModuleContentsAsCxx(
raw_ostream &os, ModuleDecl &M, SwiftToClangInteropContext &interopContext,
bool requiresExposedAttribute, llvm::StringSet<> &exposedModules) {
Expand Down
5 changes: 5 additions & 0 deletions lib/PrintAsClang/ModuleContentsWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ void printModuleContentsAsObjC(raw_ostream &os,
ModuleDecl &M,
SwiftToClangInteropContext &interopContext);

void printModuleContentsAsC(raw_ostream &os,
llvm::SmallPtrSetImpl<ImportModuleTy> &imports,
ModuleDecl &M,
SwiftToClangInteropContext &interopContext);

struct EmittedClangHeaderDependencyInfo {
/// The set of imported modules used by this module.
SmallPtrSet<ImportModuleTy, 8> imports;
Expand Down
2 changes: 1 addition & 1 deletion lib/PrintAsClang/OutputLanguageMode.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace swift {

enum class OutputLanguageMode { ObjC, Cxx };
enum class OutputLanguageMode { ObjC, Cxx, C };

} // end namespace swift

Expand Down
39 changes: 38 additions & 1 deletion lib/PrintAsClang/PrintAsClang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ static void emitObjCConditional(raw_ostream &out,
out << "#endif\n";
}

static void emitExternC(raw_ostream &out,
llvm::function_ref<void()> cCase) {
emitCxxConditional(out, [&] {
out << "extern \"C\" {\n";
});
cCase();
emitCxxConditional(out, [&] {
out << "} // extern \"C\"\n";
});
}

static void writePtrauthPrologue(raw_ostream &os, ASTContext &ctx) {
emitCxxConditional(os, [&]() {
ClangSyntaxPrinter(ctx, os).printIgnoredDiagnosticBlock(
Expand Down Expand Up @@ -207,6 +218,21 @@ static void writePrologue(raw_ostream &out, ASTContext &ctx,

static_assert(SWIFT_MAX_IMPORTED_SIMD_ELEMENTS == 4,
"need to add SIMD typedefs here if max elements is increased");

if (ctx.LangOpts.hasFeature(Feature::CDecl)) {
// For C compilers which don’t support nullability attributes, ignore them;
// for ones which do, suppress warnings about them being an extension.
out << "#if !__has_feature(nullability)\n"
"# define _Nonnull\n"
"# define _Nullable\n"
"# define _Null_unspecified\n"
"#elif !defined(__OBJC__)\n"
"# pragma clang diagnostic ignored \"-Wnullability-extension\"\n"
"#endif\n"
"#if !__has_feature(nullability_nullable_result)\n"
"# define _Nullable_result _Nullable\n"
"#endif\n";
}
}

static int compareImportModulesByName(const ImportModuleTy *left,
Expand Down Expand Up @@ -577,12 +603,21 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
llvm::PrettyStackTraceString trace("While generating Clang header");

SwiftToClangInteropContext interopContext(*M, irGenOpts);
writePrologue(os, M->getASTContext(), computeMacroGuard(M));

// C content (@cdecl)
if (M->getASTContext().LangOpts.hasFeature(Feature::CDecl)) {
SmallPtrSet<ImportModuleTy, 8> imports;
Copy link
Contributor

Choose a reason for hiding this comment

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

When you’re in C++ or ObjC mode, are there going to be two sets of imports? Do both of them actually get printed? What if there’s overlap between them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept the logic collecting imports as part of the boilerplate when I set up the C block but not the imports printing part. I don't expect it to collect anything at this point and it's unclear to me if we'll need to print imports for C in the future. Maybe for imported @cdecl enums or eventually some form of @cdecl struct, but even then we may have to print a local copy as we wouldn't know what header to import. Do you see other scenarios where we'd need imports for C?

Copy link
Contributor

@beccadax beccadax Apr 18, 2025

Choose a reason for hiding this comment

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

Imports come up when a declaration uses a type from another (C) library. For instance, suppose you have this header:

// CoreToaster/CTTimeSetting.h

typedef enum __attribute__((enum_extensibility(open)) CTTimeSetting CTTimeSetting;
enum CTTimeSetting {
    CTTimeSettingLight,
    CTTimeSettingNormal,
    CTTimeSettingDark
};

And you write this Swift file:

// ToasterKit.swift

import CoreToaster

@cdecl(TKGetDefaultTimeSetting)
public func defaultTimeSetting() -> CTTimeSetting { .normal }

The ToasterKit generated header will need to use CTTimeSetting, which means it has to import the header it came from:

// ToasterKit+Swift.h

#include <CoreToaster/CTTimeSetting.h>
// ...other stuff...
CTTimeSetting TKGetDefaultTimeSetting();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes perfect sense. I've added imported types to the pitch already and plan on adding support for C includes in a future PR.

emitExternC(os, [&] {
printModuleContentsAsC(os, imports, *M, interopContext);
});
}

// Objective-C content
SmallPtrSet<ImportModuleTy, 8> imports;
std::string objcModuleContentsBuf;
llvm::raw_string_ostream objcModuleContents{objcModuleContentsBuf};
printModuleContentsAsObjC(objcModuleContents, imports, *M, interopContext);
writePrologue(os, M->getASTContext(), computeMacroGuard(M));
emitObjCConditional(os, [&] {
llvm::StringMap<StringRef> exposedModuleHeaderNames;
writeImports(os, imports, *M, bridgingHeader, frontendOpts,
Expand All @@ -591,6 +626,8 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
writePostImportPrologue(os, *M);
emitObjCConditional(os, [&] { os << "\n" << objcModuleContents.str(); });
writeObjCEpilogue(os);

// C++ content
emitCxxConditional(os, [&] {
// FIXME: Expose Swift with @expose by default.
bool enableCxx = frontendOpts.ClangHeaderExposedDecls.has_value() ||
Expand Down
1 change: 1 addition & 0 deletions test/PrintAsObjC/cdecl-imports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) %s -parse-as-library -typecheck -verify -emit-objc-header-path %t/swift.h
// RUN: %FileCheck %s < %t/swift.h
// RUN: %check-in-clang %t/swift.h
// RUN: %check-in-clang-c %t/swift.h
// RUN: %check-in-clang-cxx %t/swift.h

// REQUIRES: objc_interop
Expand Down
23 changes: 23 additions & 0 deletions test/PrintAsObjC/cdecl-official-for-objc-clients.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// Similar test to cdecl-official but gated to objc-interop compatibility

// RUN: %empty-directory(%t)
// RUN: split-file %S/cdecl-official.swift %t --leading-lines

/// Generate cdecl.h
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) \
// RUN: %t/Lib.swift -emit-module -verify -o %t -emit-module-doc \
// RUN: -emit-clang-header-path %t/cdecl.h \
// RUN: -enable-experimental-feature CDecl

/// Check cdecl.h directly
// RUN: %check-in-clang %t/cdecl.h
// RUN: %check-in-clang-cxx %t/cdecl.h

/// Build an Objective-C client against cdecl.h
// RUN: %clang -c %t/Client.c -fmodules -I %t \
// RUN: -F %S/../Inputs/clang-importer-sdk-path/frameworks \
// RUN: -I %clang-include-dir -Werror \
// RUN: -isysroot %S/../Inputs/clang-importer-sdk

// REQUIRES: swift_feature_CDecl
// REQUIRES: objc_interop
78 changes: 78 additions & 0 deletions test/PrintAsObjC/cdecl-official.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// RUN: %empty-directory(%t)
// RUN: split-file %s %t --leading-lines

/// Generate cdecl.h
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) \
// RUN: %t/Lib.swift -emit-module -verify -o %t -emit-module-doc \
// RUN: -emit-clang-header-path %t/cdecl.h \
// RUN: -enable-experimental-feature CDecl

/// Check cdecl.h directly
// RUN: %FileCheck %s --input-file %t/cdecl.h
// RUN: %check-in-clang-c %t/cdecl.h -Wnullable-to-nonnull-conversion

/// Build a client against cdecl.h
// RUN: %clang-no-modules -c %t/Client.c -I %t \
// RUN: -F %S/../Inputs/clang-importer-sdk-path/frameworks \
// RUN: -I %clang-include-dir -Werror \
// RUN: -isysroot %S/../Inputs/clang-importer-sdk

// REQUIRES: swift_feature_CDecl

//--- Lib.swift

// CHECK-NOT: assume_nonnull

// CHECK: #if defined(__cplusplus)
Copy link
Contributor

Choose a reason for hiding this comment

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

Strongly recommend adding test cases that require an import and then testing the imports you get.

Remember, to be compatible with non-clang compilers, official cdecl needs to not use @import; it needs to use #include instead (or at least fall back to it). It also needs to not use the .h-less #includes that are common in C++.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, C includes with require some new logic to the printing component.

// CHECK: extern "C" {
// CHECK: #endif

/// My documentation
@cdecl("simple")
func a_simple(x: Int, bar y: Int) -> Int { return x }
// CHECK-LABEL: // My documentation
// CHECK-LABEL: SWIFT_EXTERN ptrdiff_t simple(ptrdiff_t x, ptrdiff_t y) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;

@cdecl("primitiveTypes")
public func b_primitiveTypes(i: Int, ci: CInt, l: CLong, c: CChar, f: Float, d: Double, b: Bool) {}
// CHECK-LABEL: SWIFT_EXTERN void primitiveTypes(ptrdiff_t i, int ci, long l, char c, float f, double d, bool b) SWIFT_NOEXCEPT;

@cdecl("has_keyword_arg_names")
func c_keywordArgNames(auto: Int, union: Int) {}
// CHECK-LABEL: SWIFT_EXTERN void has_keyword_arg_names(ptrdiff_t auto_, ptrdiff_t union_) SWIFT_NOEXCEPT;
Copy link
Contributor

@beccadax beccadax Apr 18, 2025

Choose a reason for hiding this comment

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

I would add test cases for various pointer types: UnsafeMutableRawPointer should become void * _Nonnull, UnsafeMutablePointer<Int> should become ptrdiff_t * _Nonnull, the non-Mutable variants should gain a const, and adding ? or ! at the end should get you _Nullable and _Null_unspecified respectively. (Unless we’re in an assume_nonnull region? If you think so, there should be a CHECK line for that.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I'm taking note of these. I plan on adding more test and type-checking around the types accepted by @cdecl in a future PR, a few more are not covered either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We already get the desired behavior so I've added tests for pointer types and nullability to this PR.


@cdecl("return_never")
func d_returnNever() -> Never { fatalError() }
// CHECK-LABEL: SWIFT_EXTERN void return_never(void) SWIFT_NOEXCEPT SWIFT_NORETURN;

/// Pointer types
// CHECK: /// Pointer types

@cdecl("pointers")
func f_pointers(_ x: UnsafeMutablePointer<Int>,
y: UnsafePointer<Int>,
z: UnsafeMutableRawPointer,
w: UnsafeRawPointer,
u: OpaquePointer) {}
// CHECK: SWIFT_EXTERN void pointers(ptrdiff_t * _Nonnull x, ptrdiff_t const * _Nonnull y, void * _Nonnull z, void const * _Nonnull w, void * _Nonnull u) SWIFT_NOEXCEPT;

@cdecl("nullable_pointers")
func g_nullablePointers(_ x: UnsafeMutableRawPointer,
y: UnsafeMutableRawPointer?,
z: UnsafeMutableRawPointer!) {}
// CHECK: SWIFT_EXTERN void nullable_pointers(void * _Nonnull x, void * _Nullable y, void * _Null_unspecified z) SWIFT_NOEXCEPT;

// CHECK: #if defined(__cplusplus)
// CHECK-NEXT: }
// CHECK-NEXT: #endif

//--- Client.c

#include "cdecl.h"

int main() {
ptrdiff_t x = simple(42, 43);
primitiveTypes(1, 2, 3, 'a', 1.0f, 2.0, true);
has_keyword_arg_names(1, 2);
return_never();
}
33 changes: 33 additions & 0 deletions test/PrintAsObjC/cdecl-with-objc.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// Ensure we print @cdecl and @_cdecl only once.

// RUN: %empty-directory(%t)

/// Generate cdecl.h
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) \
// RUN: %s -emit-module -verify -o %t -emit-module-doc \
// RUN: -emit-objc-header-path %t/cdecl.h \
// RUN: -disable-objc-attr-requires-foundation-module \
// RUN: -enable-experimental-feature CDecl

/// Check cdecl.h directly
// RUN: %FileCheck %s --input-file %t/cdecl.h
// RUN: %check-in-clang %t/cdecl.h
// RUN: %check-in-clang-c %t/cdecl.h -Wnullable-to-nonnull-conversion
// RUN: %check-in-clang-cxx %t/cdecl.h

// REQUIRES: swift_feature_CDecl
// REQUIRES: objc_interop

@cdecl("cFunc")
func cFunc() { }
// CHECK: cFunc
// CHECK-NOT: cFunc

/// The class would break C parsing if printed in wrong block
@objc
class ObjCClass {}

@_cdecl("objcFunc")
func objcFunc() -> ObjCClass! { return ObjCClass() }
// CHECK: objcFunc
// CHECK-NOT: objcFunc
1 change: 1 addition & 0 deletions test/PrintAsObjC/cdecl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// RUN: %FileCheck %s < %t/cdecl.h
// RUN: %check-in-clang %t/cdecl.h
// RUN: %check-in-clang -fno-modules -Qunused-arguments %t/cdecl.h -include ctypes.h -include CoreFoundation.h
// RUN: %check-in-clang-c -fno-modules -Qunused-arguments %t/cdecl.h -include ctypes.h -include CoreFoundation.h
// RUN: %check-in-clang-cxx -fno-modules -Qunused-arguments %t/cdecl.h -include ctypes.h -include CoreFoundation.h

// REQUIRES: objc_interop
Expand Down
8 changes: 8 additions & 0 deletions test/PrintAsObjC/lit.local.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ config.substitutions.insert(0, ('%check-in-clang',
'-I %%clang-include-dir '
'-isysroot %r/Inputs/clang-importer-sdk' % config.test_source_root) )

config.substitutions.insert(0, ('%check-in-clang-c',
'%%clang-no-modules -fsyntax-only -x c-header -std=c99 '
'-Weverything -Werror -Wno-unused-macros -Wno-incomplete-module '
'-Wno-auto-import -Wno-poison-system-directories '
'-F %%clang-importer-sdk-path/frameworks '
'-I %%clang-include-dir '
'-isysroot %r/Inputs/clang-importer-sdk' % config.test_source_root) )

config.substitutions.insert(0, ('%check-in-clang-cxx',
'%%clang -fsyntax-only -x objective-c++-header -std=c++17 '
'-fobjc-arc -fmodules -fmodules-validate-system-headers '
Expand Down
16 changes: 16 additions & 0 deletions test/attr/attr_cdecl_official.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,19 @@ class Foo {

@cdecl("throwing") // expected-error{{raising errors from @cdecl functions is not supported}}
func throwing() throws { }

@cdecl("acceptedPointers")
func acceptedPointers(_ x: UnsafeMutablePointer<Int>,
y: UnsafePointer<Int>,
z: UnsafeMutableRawPointer,
w: UnsafeRawPointer,
u: OpaquePointer) {}

@cdecl("rejectedPointers")
func rejectedPointers( // expected-error 6 {{global function cannot be marked '@cdecl' because the type of the parameter}}
x: UnsafePointer<String>, // expected-note {{Swift structs cannot be represented in Objective-C}} // FIXME: Should reference C.
y: CVaListPointer, // expected-note {{Swift structs cannot be represented in Objective-C}}
z: UnsafeBufferPointer<Int>, // expected-note {{Swift structs cannot be represented in Objective-C}}
u: UnsafeMutableBufferPointer<Int>, // expected-note {{Swift structs cannot be represented in Objective-C}}
v: UnsafeRawBufferPointer, // expected-note {{Swift structs cannot be represented in Objective-C}}
t: UnsafeMutableRawBufferPointer) {} // expected-note {{Swift structs cannot be represented in Objective-C}}
5 changes: 5 additions & 0 deletions test/lit.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,11 @@ config.substitutions.append( ('%clangxx',
"%r %s" %
(config.clangxx, clang_mcp_opt)) )

# Alternative to %clang that doesn't require -fmodules.
config.substitutions.append( ('%clang-no-modules',
"%r" %
(config.clang)) )

# This must come after all substitutions containing "%clang".
# Note: %clang is the locally-built clang.
# To get Xcode's clang, use %target-clang.
Expand Down