Skip to content
Open
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
5 changes: 5 additions & 0 deletions include/swift/AST/DiagnosticsFrontend.def
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,11 @@ ERROR(package_cmo_requires_library_evolution, none,
WARNING(internal_bridging_header_without_library_evolution,none,
"using internal bridging headers without library evolution can cause instability", ())

ERROR(error_invalid_clang_header_access_level, none,
"invalid minimum clang header access level '%0'; chose from "
"'public'|'package'|'internal'",
(StringRef))

ERROR(experimental_not_supported_in_production,none,
"experimental feature '%0' cannot be enabled in production compiler",
(StringRef))
Expand Down
5 changes: 5 additions & 0 deletions include/swift/Frontend/FrontendOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#ifndef SWIFT_FRONTEND_FRONTENDOPTIONS_H
#define SWIFT_FRONTEND_FRONTENDOPTIONS_H

#include "swift/AST/AttrKind.h"
#include "swift/Basic/FileTypes.h"
#include "swift/Basic/PathRemapper.h"
#include "swift/Basic/Version.h"
Expand Down Expand Up @@ -501,6 +502,10 @@ class FrontendOptions {
/// header.
std::optional<ClangHeaderExposeBehavior> ClangHeaderExposedDecls;

// Include declarations that are at least as visible as the acces specified
// by -emit-clang-header-min-access
std::optional<AccessLevel> ClangHeaderMinAccess;

struct ClangHeaderExposedImportedModule {
std::string moduleName;
std::string headerName;
Expand Down
5 changes: 5 additions & 0 deletions include/swift/Option/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,11 @@ def emit_clang_header_path : Separate<["-"], "emit-clang-header-path">,
HelpText<"Emit an Objective-C and C++ header file to <path>">,
Alias<emit_objc_header_path>;

def emit_clang_header_min_access : Separate<["-"], "emit-clang-header-min-access">,
Flags<[FrontendOption, NoInteractiveOption, ArgumentIsPath, CacheInvariant]>,
MetaVarName<"<access-level>">,
HelpText<"The minimum access level of declarations to include in the emitted header.>">;

def static : Flag<["-"], "static">,
Flags<[FrontendOption, ModuleInterfaceOption, NoInteractiveOption]>,
HelpText<"Make this module statically linkable and make the output of -emit-library a static library.">;
Expand Down
20 changes: 16 additions & 4 deletions lib/Frontend/ArgsToFrontendOptionsConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#include "ArgsToFrontendInputsConverter.h"
#include "ArgsToFrontendOutputsConverter.h"
#include "clang/Driver/Driver.h"
#include "swift/AST/AttrKind.h"
#include "swift/AST/DiagnosticsFrontend.h"
#include "swift/Basic/Assertions.h"
#include "swift/Basic/Platform.h"
Expand All @@ -24,18 +24,19 @@
#include "swift/Parse/Lexer.h"
#include "swift/Parse/ParseVersion.h"
#include "swift/Strings.h"
#include "clang/Driver/Driver.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/TargetParser/Triple.h"
#include "llvm/CAS/ObjectStore.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/Option.h"
#include "llvm/Support/Compression.h"
#include "llvm/Support/PrefixMapper.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/LineIterator.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/PrefixMapper.h"
#include "llvm/Support/Process.h"
#include "llvm/TargetParser/Triple.h"

using namespace swift;
using namespace llvm::opt;
Expand Down Expand Up @@ -399,6 +400,17 @@ bool ArgsToFrontendOptionsConverter::convert(
HasExposeAttrOrImplicitDeps)
.Default(std::nullopt);
}
if (const Arg *A = Args.getLastArg(OPT_emit_clang_header_min_access)) {
Opts.ClangHeaderMinAccess =
llvm::StringSwitch<std::optional<AccessLevel>>(A->getValue())
.Case("public", AccessLevel::Public)
.Case("package", AccessLevel::Package)
.Case("internal", AccessLevel::Internal)
.Default(std::nullopt);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be an explicit spelling for the default behavior? Something like auto or default?

if (!Opts.ClangHeaderMinAccess)
Diags.diagnose(SourceLoc(), diag::error_invalid_clang_header_access_level,
A->getValue());
}
for (const auto &arg :
Args.getAllArgValues(options::OPT_clang_header_expose_module)) {
auto splitArg = StringRef(arg).split('=');
Expand Down
25 changes: 16 additions & 9 deletions lib/PrintAsClang/ModuleContentsWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "PrintSwiftToClangCoreScaffold.h"
#include "SwiftToClangInteropContext.h"

#include "swift/AST/AttrKind.h"
#include "swift/AST/Decl.h"
#include "swift/AST/DiagnosticsSema.h"
#include "swift/AST/ExistentialLayout.h"
Expand Down Expand Up @@ -1151,35 +1152,41 @@ class ModuleWriter {
};
} // end anonymous namespace

static AccessLevel getRequiredAccess(const ModuleDecl &M) {
return M.isExternallyConsumed() ? AccessLevel::Public : AccessLevel::Internal;
static AccessLevel getRequiredAccess(const ModuleDecl &M,
AccessLevel minAccess) {
return M.isExternallyConsumed() ? minAccess : AccessLevel::Internal;
Comment on lines +1155 to +1157
Copy link
Contributor

Choose a reason for hiding this comment

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

I don’t think I like this behavior. If you explicitly specify an access level on the command line, that ought to override the implicit computation completely, even when isExternallyConsumed() is false. In other words, I’d like to see:

Suggested change
static AccessLevel getRequiredAccess(const ModuleDecl &M,
AccessLevel minAccess) {
return M.isExternallyConsumed() ? minAccess : AccessLevel::Internal;
static AccessLevel getRequiredAccess(const ModuleDecl &M,
std::optional<AccessLevel> minAccess) {
if (minAccess)
return *minAccess;
return M.isExternallyConsumed() ? AccessLevel::Public : AccessLevel::Internal;

With matching changes upstream of this function to preserve the optionality of ClangHeaderMinAccess up to this point.

}

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

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

EmittedClangHeaderDependencyInfo swift::printModuleContentsAsCxx(
raw_ostream &os, ModuleDecl &M, SwiftToClangInteropContext &interopContext,
bool requiresExposedAttribute, llvm::StringSet<> &exposedModules) {
AccessLevel minAccess, bool requiresExposedAttribute,
llvm::StringSet<> &exposedModules) {
std::string moduleContentsBuf;
llvm::raw_string_ostream moduleOS{moduleContentsBuf};
std::string modulePrologueBuf;
Expand All @@ -1197,8 +1204,8 @@ EmittedClangHeaderDependencyInfo swift::printModuleContentsAsCxx(

// FIXME: Use getRequiredAccess once @expose is supported.
ModuleWriter writer(moduleOS, prologueOS, info.imports, M, interopContext,
AccessLevel::Public, requiresExposedAttribute,
exposedModules, OutputLanguageMode::Cxx);
minAccess, requiresExposedAttribute, exposedModules,
OutputLanguageMode::Cxx);
writer.write();
info.dependsOnStandardLibrary = writer.isStdlibRequired();
if (M.isStdlibModule()) {
Expand Down
14 changes: 9 additions & 5 deletions lib/PrintAsClang/ModuleContentsWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ using ImportModuleTy = PointerUnion<ModuleDecl*, const clang::Module*>;
void printModuleContentsAsObjC(raw_ostream &os,
llvm::SmallPtrSetImpl<ImportModuleTy> &imports,
ModuleDecl &M,
SwiftToClangInteropContext &interopContext);
SwiftToClangInteropContext &interopContext,
AccessLevel minAccess);

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

struct EmittedClangHeaderDependencyInfo {
/// The set of imported modules used by this module.
Expand All @@ -52,9 +54,11 @@ struct EmittedClangHeaderDependencyInfo {
/// Prints the declarations of \p M to \p os in C++ language mode.
///
/// \returns Dependencies required by this module.
EmittedClangHeaderDependencyInfo printModuleContentsAsCxx(
raw_ostream &os, ModuleDecl &M, SwiftToClangInteropContext &interopContext,
bool requiresExposedAttribute, llvm::StringSet<> &exposedModules);
EmittedClangHeaderDependencyInfo
printModuleContentsAsCxx(raw_ostream &os, ModuleDecl &M,
SwiftToClangInteropContext &interopContext,
AccessLevel minAccess, bool requiresExposedAttribute,
llvm::StringSet<> &exposedModules);

} // end namespace swift

Expand Down
17 changes: 12 additions & 5 deletions lib/PrintAsClang/PrintAsClang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "SwiftToClangInteropContext.h"

#include "swift/AST/ASTContext.h"
#include "swift/AST/AttrKind.h"
#include "swift/AST/Module.h"
#include "swift/AST/PrettyStackTrace.h"
#include "swift/Basic/Assertions.h"
Expand Down Expand Up @@ -620,7 +621,9 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
if (M->getASTContext().LangOpts.hasFeature(Feature::CDecl)) {
SmallPtrSet<ImportModuleTy, 8> imports;
llvm::raw_string_ostream cModuleContents{moduleContentsScratch};
printModuleContentsAsC(cModuleContents, imports, *M, interopContext);
printModuleContentsAsC(
cModuleContents, imports, *M, interopContext,
frontendOpts.ClangHeaderMinAccess.value_or(AccessLevel::Public));
Copy link
Contributor

Choose a reason for hiding this comment

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

The value_or here would need to be removed for my suggestion above.


llvm::StringMap<StringRef> exposedModuleHeaderNames;
writeImports(os, imports, *M, bridgingHeader, frontendOpts,
Expand All @@ -634,7 +637,9 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
// Objective-C content
SmallPtrSet<ImportModuleTy, 8> imports;
llvm::raw_string_ostream objcModuleContents{moduleContentsScratch};
printModuleContentsAsObjC(objcModuleContents, imports, *M, interopContext);
printModuleContentsAsObjC(
objcModuleContents, imports, *M, interopContext,
frontendOpts.ClangHeaderMinAccess.value_or(AccessLevel::Public));
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

emitObjCConditional(os, [&] {
llvm::StringMap<StringRef> exposedModuleHeaderNames;
writeImports(os, imports, *M, bridgingHeader, frontendOpts,
Expand Down Expand Up @@ -685,6 +690,7 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
llvm::raw_string_ostream moduleContents{moduleContentsBuf};
auto deps = printModuleContentsAsCxx(
moduleContents, *M, interopContext,
frontendOpts.ClangHeaderMinAccess.value_or(AccessLevel::Public),
Copy link
Contributor

@beccadax beccadax Oct 10, 2025

Choose a reason for hiding this comment

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

Possibly here too.

/*requiresExposedAttribute=*/requiresExplicitExpose, exposedModules);
// FIXME: In ObjC++ mode, we do not need to reimport duplicate modules.
llvm::StringMap<StringRef> exposedModuleHeaderNames;
Expand All @@ -701,9 +707,10 @@ bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
auto macroGuard = computeMacroGuard(M->getASTContext().getStdlibModule());
os << "#ifndef " << macroGuard << "\n";
os << "#define " << macroGuard << "\n";
printModuleContentsAsCxx(
os, *M->getASTContext().getStdlibModule(), interopContext,
/*requiresExposedAttribute=*/true, exposedModules);
printModuleContentsAsCxx(os, *M->getASTContext().getStdlibModule(),
interopContext, AccessLevel::Public,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this fixed as AccessLevel::Public? (Quite possible there’s a legitimate reason; I’m just wondering what it is.)

/*requiresExposedAttribute=*/true,
exposedModules);
os << "#endif // " << macroGuard << "\n";
}

Expand Down
2 changes: 1 addition & 1 deletion lib/PrintAsClang/SwiftToClangInteropContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ SwiftToClangInteropContext::SwiftToClangInteropContext(
ModuleDecl &mod, const IRGenOptions &irGenOpts)
: mod(mod), irGenOpts(irGenOpts) {}

SwiftToClangInteropContext::~SwiftToClangInteropContext() {}
SwiftToClangInteropContext::~SwiftToClangInteropContext() = default;

IRABIDetailsProvider &SwiftToClangInteropContext::getIrABIDetails() {
if (!irABIDetails)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=has-expose-attr -emit-clang-header-min-access public -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-PUBLIC < %t/core.h

// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=has-expose-attr -emit-clang-header-min-access package -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-PACKAGE < %t/core.h

// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=has-expose-attr -emit-clang-header-min-access internal -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-INTERNAL < %t/core.h

@_expose(Cxx)
public func publicFunc(_ x: Int) -> Int {
return x
}

@_expose(Cxx)
package func packageFunc(_ x: Int) -> Int {
return x
}

@_expose(Cxx)
internal func internalFunc(_ x: Int) -> Int {
return x
}

@_expose(Cxx)
private func privateFunc(_ x: Int) -> Int {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is never emitted, should we warn about sub-internal decls being annotated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. This is also used for wasm and that is a bit special. I will need to double check with someone to see it is OK to rule that out.

return x
}

// CHECK-PUBLIC-NOT: internalFunc
// CHECK-PUBLIC-NOT: packageFunc
// CHECK-PUBLIC-NOT: privateFunc
// CHECK-PUBLIC: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {

// CHECK-PACKAGE-NOT: internalFunc
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int packageFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core11packageFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-PACKAGE-NOT: privateFunc
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {

// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int internalFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core12internalFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int packageFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core11packageFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-INTERNAL-NOT: privateFunc
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
79 changes: 79 additions & 0 deletions test/Interop/SwiftToCxx/core/set-min-access-level.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=all-public -emit-clang-header-min-access public -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-PUBLIC < %t/core.h

// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=all-public -emit-clang-header-min-access package -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-PACKAGE < %t/core.h

// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -module-name Core -typecheck -verify -emit-clang-header-path %t/core.h -clang-header-expose-decls=all-public -emit-clang-header-min-access internal -package-name Core
// RUN: %FileCheck %s --check-prefix CHECK-INTERNAL < %t/core.h

// RUN: %empty-directory(%t)
// RUN: not %target-swift-frontend %s -module-name Core -typecheck -emit-clang-header-path %t/core.h -clang-header-expose-decls=all-public -emit-clang-header-min-access inernal -package-name Core 2>&1 | %FileCheck %s --check-prefix CHECK-DIAGNOSTIC

public func publicFunc(_ x: Int) -> Int {
return x
}

package func packageFunc(_ x: Int) -> Int {
return x
}

internal func internalFunc(_ x: Int) -> Int {
return x
}

private func privateFunc(_ x: Int) -> Int {
return x
}

public struct S {
public func publicMethod(_ x: Int) -> Int {
return x
}

package func packageMethod(_ x: Int) -> Int {
return x
}

internal func internalMethod(_ x: Int) -> Int {
return x
}

private func privateMethod(_ x: Int) -> Int {
return x
}

private var x: Int
}

// CHECK-PUBLIC-NOT: internalFunc
// CHECK-PUBLIC-NOT: packageFunc
// CHECK-PUBLIC-NOT: privateFunc
// CHECK-PUBLIC: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-PUBLIC: SWIFT_INLINE_THUNK swift::Int S::publicMethod(swift::Int x) const {
// CHECK-PUBLIC-NOT: packageMethod
// CHECK-PUBLIC-NOT: internalMethod
// CHECK-PUBLIC-NOT: privateMethod

// CHECK-PACKAGE-NOT: internalFunc
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int packageFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core11packageFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-PACKAGE-NOT: privateFunc
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int S::publicMethod(swift::Int x) const {
// CHECK-PACKAGE: SWIFT_INLINE_THUNK swift::Int S::packageMethod(swift::Int x) const {
// CHECK-PACKAGE-NOT: internalMethod
// CHECK-PACKAGE-NOT: privateMethod

// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int internalFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core12internalFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int packageFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core11packageFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-INTERNAL-NOT: privateFunc
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int publicFunc(swift::Int x) noexcept SWIFT_SYMBOL("s:4Core10publicFuncyS2iF") SWIFT_WARN_UNUSED_RESULT {
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int S::publicMethod(swift::Int x) const {
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int S::packageMethod(swift::Int x) const {
// CHECK-INTERNAL: SWIFT_INLINE_THUNK swift::Int S::internalMethod(swift::Int x) const {
// CHECK-INTERNAL-NOT: privateMethod

// CHECK-DIAGNOSTIC: error: invalid minimum clang header access level 'inernal'; chose from 'public'|'package'|'internal'