From ae8e95d5d9263e2eae5a450cc0b0f831b4ff4139 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Tue, 19 Aug 2025 16:03:18 -0700 Subject: [PATCH] [Embedded] Introduce a test for @_implementationOnly + @_neverEmitIntoClient In Desktop Swift, @_implementationOnly imports allow one to hide the implementation so long as you're careful to only reference entities from the imported modules in code that gets compiled into the object file and *not* referenced by the corresponding Swift module file. Until very recently, there was no such affordance for Embedded Swift, because all functions would have their SIL serialized to the Swift module file. Using them from a client module would then attempt to deserialize the SIL, loading the @_implementationOnly-imported module and causing the compiler to abort. With the introduction of @_neverEmitIntoClient, we now have a way to say "only in the object file, never in the module file" for the definition of functions. Introduce a test that makes sure @_implementationOnly + @_neverEmitIntoClient has the desired effect of hiding the imported modules from clients. It's still brittle and hard to use, just like the existing @_implementationOnly, but this shows that it's at least possible to do this implementation hiding in Embedded Swift. --- test/embedded/linkage/Inputs/CHeader.h | 5 ++ .../linkage/Inputs/SwiftDependency.swift | 7 ++ test/embedded/linkage/Inputs/module.modulemap | 3 + .../linkage/implementation_only_hiding.swift | 73 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 test/embedded/linkage/Inputs/CHeader.h create mode 100644 test/embedded/linkage/Inputs/SwiftDependency.swift create mode 100644 test/embedded/linkage/Inputs/module.modulemap create mode 100644 test/embedded/linkage/implementation_only_hiding.swift diff --git a/test/embedded/linkage/Inputs/CHeader.h b/test/embedded/linkage/Inputs/CHeader.h new file mode 100644 index 0000000000000..d7ef0f7c265cc --- /dev/null +++ b/test/embedded/linkage/Inputs/CHeader.h @@ -0,0 +1,5 @@ +typedef struct { + double x, y; +} Point; + +Point getPoint(double x, double y); diff --git a/test/embedded/linkage/Inputs/SwiftDependency.swift b/test/embedded/linkage/Inputs/SwiftDependency.swift new file mode 100644 index 0000000000000..2fc3b1624c11b --- /dev/null +++ b/test/embedded/linkage/Inputs/SwiftDependency.swift @@ -0,0 +1,7 @@ +public struct A { + private var string: String + + public init() { self.string = "Hello" } + + public func doSomething() { } +} diff --git a/test/embedded/linkage/Inputs/module.modulemap b/test/embedded/linkage/Inputs/module.modulemap new file mode 100644 index 0000000000000..5445080fa9516 --- /dev/null +++ b/test/embedded/linkage/Inputs/module.modulemap @@ -0,0 +1,3 @@ +module CDependency { + header "CHeader.h" +} diff --git a/test/embedded/linkage/implementation_only_hiding.swift b/test/embedded/linkage/implementation_only_hiding.swift new file mode 100644 index 0000000000000..079c6f02cc7ca --- /dev/null +++ b/test/embedded/linkage/implementation_only_hiding.swift @@ -0,0 +1,73 @@ +// This elaborate test ensures that @_implementationOnly with Embedded Swift +// can hide some dependencies if you are very, very careful. + +// RUN: %empty-directory(%t) +// RUN: mkdir -p %t/Dependencies +// RUN: mkdir -p %t/Files +// RUN: mkdir -p %t/Modules + +// Copy Swift file + C header + module map into a separate directory we'll use +// when building the module whose implementation we want to hide. +// RUN: cp %S/Inputs/SwiftDependency.swift %t/Dependencies/ +// RUN: cp %S/Inputs/CHeader.h %t/Dependencies/ +// RUN: cp %S/Inputs/module.modulemap %t/Dependencies/ + +// RUN: split-file %s %t/Files + +// Compile the Swift dependencies into that same location. +// RUN: %target-swift-frontend -parse-as-library -emit-module %t/Dependencies/SwiftDependency.swift -enable-experimental-feature Embedded -o %t/Dependencies/SwiftDependency.swiftmodule + +// Build the library (which is supposed to encapsulate those dependencies) +// against the dependencies. +// RUN: %target-swift-frontend -parse-as-library -emit-module %t/Files/Library.swift -enable-experimental-feature Embedded -I %t/Dependencies/ -o %t/Modules/Library.swiftmodule + +// Remove the dependencies so there is no way we can find them later. +// RUN: rm -rf %t/Dependencies + +// Build the application against the library. This is expected to work because +// @_neverEmitIntoClient hides the body of test(). +// RUN: %target-swift-frontend -emit-ir -parse-as-library %t/Files/Application.swift -enable-experimental-feature Embedded -I %t/Modules -o %t/Application.ir + +// Build the application against the library, but intentionally trigger +// deserialization of some serialized SIL that refers to an implementation-only +// dependency. Right now, these fail spectacularly. Over time, we want them to +// become compile-time errors or start working. +// RUN: not --crash %target-swift-frontend -emit-ir -parse-as-library %t/Files/Application.swift -enable-experimental-feature Embedded -I %t/Modules -o %t/Application.ir -DBAD_C_USAGE +// RUN: not --crash %target-swift-frontend -emit-ir -parse-as-library %t/Files/Application.swift -enable-experimental-feature Embedded -I %t/Modules -o %t/Application.ir -DBAD_SWIFT_USAGE + +// REQUIRES: swift_in_compiler +// REQUIRES: swift_feature_Embedded + +//--- Library.swift +@_implementationOnly import CDependency +@_implementationOnly import SwiftDependency + +@_neverEmitIntoClient +public func test() { + _ = getPoint(3.14159, 2.71828) + A().doSomething() +} + +public func badCLibraryUsage() { + _ = getPoint(3.14159, 2.71828) +} + +public func badSwiftLibraryUsage() { + A().doSomething() +} + +//--- Application.swift + +import Library + +public func useTest() { + test() + +#if BAD_C_USAGE + badCLibraryUsage() +#endif + +#if BAD_SWIFT_USAGE + badSwiftLibraryUsage() +#endif +}