Skip to content

Conversation

xymus
Copy link
Contributor

@xymus xymus commented Feb 13, 2020

Add experimental support to define System Programming Interfaces (SPI) in Swift. An SPI is a kind of API targetted at specific clients and that is hidden by default. In practice, a Swift library developer could mark decls as SPI if they are reserved for a closely-related library or client, or if they are experimental and may be modified without warning in the next version of the library.

Definition and importation of SPIs is declared with the @_spi attribute. A public decl marked with @_spi(SPIName) is usable only from within the same module and by clients that imports the module with the compatible @_spi(SPIName) attribute.

In the following example, MyLib defines a function under the SPI named Experimental.

// MyLib
@_spi(Experimental) public func newExperimentalService() {}

The SPI function newExperimentalService is hidden from clients that imports the module normally.

import MyLib

newExperimentalService() // Error: use of unresolved identifier

However, clients that imports MyLib and its SPI Experimental have access to all decls with the attribute @_spi(Experimental) declared in MyLib. This is a way for the library clients to opt-in using the SPI.

@_spi(Experimental) import MyLib

newExperimentalService() // Ok

The developers of a binary framework can restrict access to its SPIs by distributing the new private Swift textual interface file (.private.swiftinterface) only to the authorized clients. This file contains both the public API and all SPIs, it is generated next to the public textual interface (.swiftinterface). The compiler prefers to load the private textual interface and falls back on the public one if the private is not found. Generating the private file is requested by the option -emit-private-module-interface-path.


This is a first partial support for SPI in Swift. A few things still need work, including the diagnostics that is not always aware of the SPI restriction and reports SPI decls as being internal.

We might want to find a better syntax for SPI attributes that can hold a list of names. The current underlying implementation already supports a list of name per attribute but it’s not reflected in the syntax.

@xymus
Copy link
Contributor Author

xymus commented Feb 13, 2020

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 9c62848787173f41ff07aeec86ed7d4fefd1f206

@compnerd
Copy link
Member

@swift-ci please test Windows platform

@xymus
Copy link
Contributor Author

xymus commented Feb 13, 2020

@swift-ci Please smoke test

@xymus
Copy link
Contributor Author

xymus commented Feb 13, 2020

@swift-ci please test Windows platform

1 similar comment
@compnerd
Copy link
Member

@swift-ci please test Windows platform

@xymus
Copy link
Contributor Author

xymus commented Feb 14, 2020

The Windows test fails on network issues, it passed the first time. @compnerd you can trigger it again if you want.

Copy link
Contributor

@slavapestov slavapestov left a comment

Choose a reason for hiding this comment

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

It looks like SPI is not inherited from the parent context (type, or extension etc). Is this intended? Maybe it would help if the various places that directly checked for an SPI attribute on the decl were modified to evaluate a request instead, and the request could look at the parent context.

StringRef filename;

// Names of explicitly imported SPIs.
SmallVector<Identifier, 4> spis;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be an arena-allocated ArrayRef, just like the ImportedModuleDesc itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, it should now be fixed.

@@ -1378,6 +1378,8 @@ ERROR(projection_value_property_not_identifier,none,
// Access control
ERROR(attr_access_expected_set,none,
"expected 'set' as subject of '%0' modifier", (StringRef))
ERROR(attr_access_expected_spi_name,none,
"expected an SPI identifier as subject of the @_spi attribute", ())
Copy link
Contributor

Choose a reason for hiding this comment

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

We tend to quote attribute names in diagnostics


auto VD = dyn_cast<ValueDecl>(D);
if (!VD) {
diagnose(attr->getLocation(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this already handled as part of the definition in Attr.def?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, we only need the check for the access level of the decl.

void AccessControlCheckerBase::checkTypeAccessImpl(
Type type, TypeRepr *typeRepr, AccessScope contextAccessScope,
const DeclContext *useDC, bool mayBeInferred,
const DeclContext *useDC, bool mayBeInferred, bool fromSPI,
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps 'fromSPI' should be an enum?

@@ -549,6 +549,11 @@ void ModuleDecl::lookupObjCMethods(
FORWARD(lookupObjCMethods, (selector, results));
}

void ModuleDecl::lookupImportedSPIs(const ModuleDecl *importedModule,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the module variant of this called anywhere? Generally, imports in other source files don't affect behavior in a source file. So the decl checker should always be calling this on a SourceFile and not the entire module. Also, FORWARD is O(n) in the number of source files -- this can be slow if you're calling it a lot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lookupImportedSPIs is called on modules when printing the textual Swift interface and for serialization (including at merge-modules), so it's not called often. I'm not sure we could keep in only on the source file in this case.

However, the SPI decl checks use isImportedAsSPI which is only on source files and it calls only SourceFile:: lookupImportedSPIs.

@@ -2531,6 +2531,11 @@ void ModuleFile::lookupObjCMethods(
}
}

void ModuleFile::lookupImportedSPIs(const ModuleDecl *importedModule,
SmallVectorImpl<Identifier> &spis) const {
// TODO
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably this won't be needed at all, so maybe lookupImportedSPIs should be a method on SourceFile only?

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 need this one when printing the private textual interface at the merge-module phase as it needs all the SPI imported from all partial modules.

@xymus
Copy link
Contributor Author

xymus commented Feb 18, 2020

Updated to rebase on master and apply Slava's comments.

@xymus
Copy link
Contributor Author

xymus commented Feb 18, 2020

@swift-ci please smoke test

@compnerd
Copy link
Member

@swift-ci please clean test Windows platform

VD->getAttrs().hasAttribute<SPIAccessControlAttr>() &&
!useSF->isImportedAsSPI(VD))
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be a duplication. Could you refactor them to a common function?


for (auto attr : targetDecl->getAttrs().getAttributes<SPIAccessControlAttr>())
for (auto declSPI : attr->getSPINames())
for (auto importedSPI : importedSpis)
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, three nested loops.. can we use Set somewhere to avoid iterating?

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 plan on moving the logic collecting the SPI groups declared on a decl to a request in order to cover what Slava pointed out with decls inheriting SPI from their parent. With it I can probably add a function for the previous comment too.

/// Find all SPI imported from \p importedModule by this module, collecting
/// their identifiers in \p spis.
virtual void lookupImportedSPIs(const ModuleDecl *importedModule,
SmallVectorImpl<Identifier> &spis) const {};
Copy link
Contributor

Choose a reason for hiding this comment

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

The function name is kind of confusing. Can we change it to something like lookupImportedSPIKinds or lookupImportedSPINotions.

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 changed the ambiguous naming for an "SPI name" to "SPI group" instead.

StringRef filename;

// Names of explicitly imported SPIs.
ArrayRef<Identifier> spis;
Copy link
Contributor

Choose a reason for hiding this comment

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

can we rename this to spiKinds? spis sounds like these are the decl identifiers of decls marked as SPI.


// Always print SPI decls if `PrintSPIs`.
if (options.PrintSPIs &&
VD->getAttrs().hasAttribute<SPIAccessControlAttr>())
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 check necessary? It should be fine if we always print SPI attributes and they will be skipped (because decls are skipped) if we print non-spi version of the interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SPI decls would be skipped because there formal access is considered to be internal here, so we have to force skip them. It’s a bit backwards because of the recent change from the default internal to public, I’ll have to come back to clean this.

// diag::conformance_from_implementation_only_module.
enum class Reason : unsigned {
General,
ExtensionWithPublicMembers,
ExtensionWithConditionalConformances
};
enum class HiddenImportKind : unsigned {
Copy link
Contributor

Choose a reason for hiding this comment

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

uint8_t should be sufficient.

@CodaFi
Copy link
Contributor

CodaFi commented Feb 19, 2020

A request: Please diagnose @_exported+@_spi.

@xymus
Copy link
Contributor Author

xymus commented Feb 19, 2020

@swift-ci Please smoke test

@swiftlang swiftlang deleted a comment from swift-ci Feb 19, 2020
@swiftlang swiftlang deleted a comment from swift-ci Feb 19, 2020
@xymus xymus merged commit b0b927b into swiftlang:master Feb 20, 2020
@e-001
Copy link

e-001 commented Nov 18, 2021

@xymus, could you please clarify if the following is per-spec? I know IDE behavior is beyond the purview of this work, but I can't find much documentation.

Two Swift packages, one is fetched via source control and another is a local clone. Both are added to an Xcode project via the Xcode/SPM package interface. The local clone will expose and allow for @_spi(Experimental) imports, whereas the remote will not. Both sources are identical on disk. Neither includes private module emit flags. The access level from the consuming code should be the same, right?

@IlyaPuchkaTW
Copy link

@_spi does not seem to limit visibility of inherited initialisers in classes when protection is added on the initialiser in a super class and subclass does not override it

@xymus xymus deleted the spi branch October 31, 2022 21:03
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.

8 participants