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

Conversation

xymus
Copy link
Contributor

@xymus xymus commented Apr 18, 2025

Add a block for C clients in the compatibility header. This block contains only the C representation of the @cdecl functions that are printed using only C types.

This C block is printed above the Objective-C and C++ blocks as if we add support for @cdecl types other languages should be able to reference them in function signatures. Other languages block don't duplicate printing the @cdecl functions either as they are already accessible to them.

Nullability attributes may still be printed so we simply ignore them for C clients.

@xymus
Copy link
Contributor Author

xymus commented Apr 18, 2025

@swift-ci Please smoke test

@xymus xymus requested a review from beccadax April 18, 2025 19:01
@beccadax
Copy link
Contributor

Before I dig into the details: What's with the nullability stuff here? I mean, obviously you want to ensure that compilers which don't support nullability annotations can still understand the header, but it seems like you're disabling (at least some parts of) nullability checking even in compilers that support it. Why?

@xymus
Copy link
Contributor Author

xymus commented Apr 18, 2025

In C mode I don't think clang supports nullability checking. It recognizes _Nonnull and raises warning: type nullability specifier '_Nonnull' is a Clang extension [-Werror,-Wnullability-extension]. I don't see a way to enable this extension for C as it's gated on Objective-C (without modifying clang at least). So instead here we disable the warning in compilers that support the attribute so it's ignored for C clients.

I don't like it so much either tbh. Alternatively we could change the printing logic to simply not print those attributes in the C block, but I'm thinking these attributes may still be useful for Objective-C clients calling a @cdecl function. In a more practical way to avoid side effects I'm also considering restricting the current changes under Ignore nullability attributes for C clients only to the C block.

@xymus
Copy link
Contributor Author

xymus commented Apr 18, 2025

@swift-ci Please smoke test

Copy link
Contributor

@beccadax beccadax left a comment

Choose a reason for hiding this comment

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

Good start, but I have a few concerns.

Comment on lines 222 to 229
// Ignore nullability attributes for C clients.
out << "#if !defined(__OBJC__) && !defined(__cplusplus)\n"
"# if __has_feature(nullability)\n"
"# pragma clang diagnostic ignored \"-Wnullability-extension\"\n"
"# else\n"
"# define _Nonnull\n"
"# endif\n"
"#endif\n";
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few issues I’d address here:

  1. The comment doesn’t quite reflect what you’re actually doing here (which is why I asked about the -W flag; it didn’t match the comment).
  2. It looks to me like only ObjC mode prevents the -Wnullability-extension warning from being emitted—C++ mode doesn't.
  3. I believe there are circumstances where we’ll emit _Nullable, _Null_unspecified, and possibly _Nullable_result in addition to _Nonnull.

The upshot is, consider doing this instead:

Suggested change
// Ignore nullability attributes for C clients.
out << "#if !defined(__OBJC__) && !defined(__cplusplus)\n"
"# if __has_feature(nullability)\n"
"# pragma clang diagnostic ignored \"-Wnullability-extension\"\n"
"# else\n"
"# define _Nonnull\n"
"# endif\n"
"#endif\n";
// 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";

However, I do have a lingering concern about even this version: the C standard reserves underscore-uppercase identifiers for the compiler and standard library to use, so I believe #define-ing _Nonnull and friends is technically not valid. I might run this by some clang engineers to see if they recommend you do something different.

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 applied this change and gated it behind the feature flag for now. I'll look deeper into types representable in C next and should be able to clean up the story around nullability then.


@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.


// 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.


//--- Lib.swift

// 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.

xymus added 4 commits April 18, 2025 15:23
Add a block for C clients in the compatibility header. This block
contains only the `@cdecl` functions that are printed using only C
types.

This C block is printed above the Objective-C and C++ blocks as if we
add support for `@cdecl` types other languages should be able to
reference them in function signatures. Other languages block don't
duplicate printing the `@cdecl` functions either as they are already
accessible to them.
In C mode we still print nullability attributes. Don't let clang warn on
them and ignore the attribute if the C compiler doesn't know them.
@xymus
Copy link
Contributor Author

xymus commented Apr 18, 2025

@swift-ci Please smoke test

@@ -2999,6 +3006,14 @@ bool DeclAndTypePrinter::shouldInclude(const ValueDecl *VD) {
return false;
}

// In C output mode print only @cdecls and skip them in other modes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you elaborate on this? Where did we emit cdecls before this change?

What if someone what to consume these cdecls from ObjC or C++ code? Is that still possible?

Copy link
Contributor Author

@xymus xymus May 5, 2025

Choose a reason for hiding this comment

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

We print the new C specific @cdecl only in C mode and keep printing the classic @_cdecl in other modes. The other modes print @_cdecl in they own block of the compatibility header gated behind the corresponding language check. I've updated the comment clarify this.

Objective-C and C++ clients see and can call the newly printed @cdecl functions. They are printed in a block at the beginning of the compatibility header without a language check except for an extern "C" block.

@xymus
Copy link
Contributor Author

xymus commented Apr 30, 2025

@swift-ci Please smoke test Linux

@xymus
Copy link
Contributor Author

xymus commented Apr 30, 2025

@swift-ci Please smoke test Windows

@xymus
Copy link
Contributor Author

xymus commented May 5, 2025

@swift-ci Please smoke test

@xymus
Copy link
Contributor Author

xymus commented May 6, 2025

@swift-ci Please smoke test

Added tests for pointer types and changed the shouldInclude check to be less compact and prepare to accept @cdecl enums.

@xymus
Copy link
Contributor Author

xymus commented May 7, 2025

@swift-ci Please smoke test

@xymus
Copy link
Contributor Author

xymus commented May 8, 2025

@swift-ci Please smoke test

@xymus
Copy link
Contributor Author

xymus commented May 9, 2025

@swift-ci Please smoke test Windows

@xymus
Copy link
Contributor Author

xymus commented May 13, 2025

Merging to unblock the follow up PR adding support for #include and types imported from C.

@xymus xymus merged commit 9c7f0c7 into swiftlang:main May 13, 2025
3 checks passed
hamishknight added a commit to hamishknight/swift that referenced this pull request May 22, 2025
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.

3 participants