Context: I am trying to create a distributable swift library that depends on a C-library.
In my C library, I have enums and structures:
typedef enum { ONE, TWO } my_numbers_t;
typedef struct {
int left;
int right;
} my_pair_t;
I created a Swift system library MyCLibrary that package my C library and imported it into my Swift target MySwiftSDK as @_implementationOnly import MyCLibrary.
I understand these C-types cannot be exposed in my public API of my swift module MySwiftSDK. Otherwise, I would get errors such as cannot use struct 'my_pair_t' here; 'MyCLibrary' has been imported as implementation-only
Basically, you have to decide between "is exposed to client code" and "is hidden from client code" and stick to that decision all the way. In the MySuperSDK class, you have decided to hide the MyCLibrary from the client code by importing it as @_implementationOnly, but then you've gone back on your decision by using types from that library in the signature of myMethod(number:pair:) method.
You either have to:
Expose the C library in order to use those types directly (maybe splitting it into a bunch of smaller libraries to limit the API surface that will be exposed).
Make independent wrappers for every declaration you're importing from the C library.
For instance:
@_implementationOnly import MyCLibrary
public struct MyNumbers: Sendable, Hashable {
// MARK: MyNumbers - UnderlyingValue
internal typealias UnderlyingValue = MyCLibrary.my_numbers_t
internal init(underlyingValue: UnderlyingValue) {
self. underlyingValue = underlyingValue
}
internal let underlyingValue: UnderlyingValue
}
extension MyNumbers: {
// MARK: MyNumbers - Known
public static let one = Self(underlyingValue: MyCLibrary.ONE)
public static let two = Self(underlyingValue: MyCLibrary.TWO)
}
If I remember correctly, even using an @_implementationOnly imported type as an internal/private stored property of a public struct will fail, because the compiler needs to load MyCLibrary for downstream dependents to know the layout of the property so that it can determine the layout of the struct. In other words, ABI is also a factor, not just public API. @_implementationOnly doesn't abstract this out today.
This only holds for structs (and enums); if MyNumbers were a class, the ABI isn't affected because the storage is always indirect. But making it a class isn't a good solution here since you presumably want the usual value semantics.
The only options you have available in this case, to my knowledge, are to wrap the stored property in an Any (and thus incur some minor performance penalty when boxing/unboxing the value), or if the C type is small enough, just reproduce it in Swift and write initializers to convert between them.
Unless there's some undocumented behavior, according to the documentation for @_implementationOnly, it should merely introduce compile-time checks to prevent any declaration from the imported module from being part of the API or the ABI.
As long as the struct isn't @frozen and the stored property is not @usableFromInline (if I'm not missing anything), an internal stored property should not be part of the ABI, due to accessor thunks being generated by the compiler (at least in library evolution mode).
Yes, but unless the struct is @frozen (if I'm not missing anything), that layout is not part of the ABI (at least in library evolution mode), no?
I thought the whole deal with Swift structs (especially compared to C structs) is that they don't expose their layout, they wrap all properties into computed property thunks and merely expose the runtime allocation size of the struct (e.g. for use by dynamic-sized stack allocation) specifically for the purpose of allowing structs to be ABI-resilient against non-API changes.
Good point—the "at least in library evolution mode" is the key bit there. If you're building with library evolution disabled (the default behavior), then non-public stored properties are still part of the layout/ABI; they're generated more or less the same as if they were @frozen.
I don't do much work in library evolution mode, so I haven't tested to see if @_implementationOnly import with -enable-library-evolution allows the use you described. (@_implementationOnly import is still useful without library evolution, to trim the size of the build graph, which is the use case I'm most familiar with.)
Given that @_implementationOnly is an unsupported and not-fully-implemented feature, I wouldn't necessarily rely on it always working as these characteristics might lead us to expect, though.
@technogen I am curious about this statement "expose the C library". From my experimentation, I did not manage to expose my static library. I encapsulate my C static library into a SPM systemLibrary() but when I import into my MySwiftSDK with library evolution (BUILD_LIBRARY_FOR_DISTRIBUTION=Yes) the compiler says:
error: no such module 'MyCLibrary'
import MyCLibrary
The only way to fix this issue is to replace import MyCLibrary by @_implementationOnly import MyCLibrary.
So I cannot see a way to expose my C library.
SwiftPM supports [Objective-]C[++] targets. Each target has a publicHeadersPath (that defaults to include). As long as you don't mix Swift code with [Objective-]C[++] code in a single target and make sure to put all public headers and the module map in the public headers folder, SwiftPM will build the [Objective-]C[++] code in that target into a proper module, which you can import from Swift code by adding it as a dependency, just like any other dependency.
As the documentation says:
Use system library targets to adapt a library installed on the system to work with Swift packages. Such libraries are generally installed by system package managers (such as Homebrew and apt-get) and exposed to Swift packages by providing a modulemap file along with other metadata such as the library’s pkgConfig name.
So, unless the library comes with something like a homebrew formula, what you really want is an [Objective-]C[++] target in SwiftPM, instead of a system library.
Thanks @technogen , I am learning a lot of new things with my thread. As far as I can see SPM target only knows how to handle [Objective-]C[++] source files - but not archive.
I tried to drop my static library into the folder and it did not seem to handle it:
My Package.swift:
.target(
name: "MyCLibrary"
),
My Sources/MyCLibrary directory contains:
Sources/MyCLibrary/include/MyCLibrary-Header.h
Sources/MyCLibrary/libmyclibrary.a
Sources/MyCLibrary/module.modulemap
... with Sources/MyCLibrary/module.modulemap
module MyStaticLib {
header "MyStaticLib-Header.h"
link "mystaticlib"
export *
}
But reading a bit more about SPM target with [Objective-]C[++], it does not really works to incorporate large existing out-of-tree C SDK - example headerSearchPath expect a relative include directory.
So I guess my initial approach of abusing of systemLibrary and pkg-config to expose my out-of-tree C SDK is currenly the best approach.
And after reading all the great contributions from my initial thread, there is no straightforward conversion from C struct/enum type to an opaque swift type.
If you're okay with only targeting Apple platforms, the best way of using pre-built libraries with SwiftPM is to wrap them into XCFramework bundles and use them via a binary target.
You can learn how to do that by running the xcodebuild -create-xcframework -help command.