How to link to ObjC framework from the dyld_shared_cache in Package.swift

I am able to do it via XCode and leveraging a bridging header and .tdb files, however, I'd like to do the same via the more streamlined swift package init project, but don't understand how to link in a bridging header and .tdb files?

I've also tried to create a swift package init --type library and then add that local lib as a dependancy to my Swift cli (--type tool) project, however, it complains about not knowing what the ObjC symbols are when I try and call them from Swift.

The name of the module that goes with the headers <objc/whatever.h> is ObjectiveC, you should be able to import it directly without using any custom headers or linker options.

I've been looking at a lot of blogs etc, but it seems that SPM has changed a lot so most articles are no longer valid.

It seems that the most modern way would be to create an xframework to contain an 'umbrella' header for some private framework with some headers from class-dump and then I can link those in as deps to my Package.swift and then call it from my Swift like how I can via an XCode swift project with tbd and a bridging header?

Does that go for private frameworks too?

Oh, no, not so much. I didn’t realize that was the question, sorry. What ObjC functions are in a private framework, though?

EDIT: I misunderstood and thought you meant β€œObjC runtime functions”, not β€œAPIs written in ObjC”. I think if you’re going to the length of writing your own TBD file for a private framework, you can also have that TBD file include the β€œinstall name” where that framework’s binary is located? And then you have to pass the custom linker search path for that TBD file and make sure you are trying to link against that library. (I haven’t done this in a while.)

Apple, of course, does not want you to do this, so I wouldn’t expect the tools to make it straightforward. The more you can make your library look like a third-party system-wide library the better, I guess?

Ah I'm probably using incorrect jargon. I meant ObjC classes and methods in the frameworks in the dyld_shared_cache. Not actual objc runtime functions etc.

One ex-Apple bit of information I’ll give here is that either the dyld shared cache isn’t special, in that you link to libraries as if they were still present on disk, or it is, in that none of this will work. The fact that you got it to work in Xcode suggests that it’s the former, and it’s merely a matter of linking against a third-party library that happens to have a TBD instead of a dylib (like the SDKs) and then that TBD happens to have an install name that refers to a library/framework binary path in the shared cache.

That sounds right to me, so I guess my question boils down to, what is the Package.swift syntax (and Swift package folder structure) that I need to link against a .tbd file (with supporting bridge header)? I assume it's probably just setting some cSettings (compiler flags etc)?

Here's an example of how to do it for any others that are curious - GitHub - NSAntoine/PrivateKits: Swift wrappers of various Private System Frameworks for Darwin platforms

FYI you can do this without unsafeFlags if you generate a framework-bundle-based xcframework with the tbd file instead of an actual binary, and declare it in your SwiftPM manifest with binaryTarget.

Can you point me to example of how to generate an xcframework bundle?

Assuming you have a framework that looks something like

Foo.framework/
  Foo.tbd
  Headers/
    Foo.h
    ...
  Modules/
    module.modulemap (optional)
  Info.plist

you can turn it into an xcframework by following Apple's docs:

If you just have a tbd and some header files, you might be able to use the -library flag instead of -framework, but there's a chance it'll complain about the tbd not being a real MachO file. If that happens you can create the framework structure yourself; since the tbd specifies an install_name that already exists on the system you don't need to worry about embedding/codesigning/etc.

Trying this with the previously linked src

tree XCFrameworks/CoreUI.framework/
XCFrameworks/CoreUI.framework/
β”œβ”€β”€ CoreUI.tbd
β”œβ”€β”€ Headers
β”‚   β”œβ”€β”€ CSIBitmapWrapper.h
β”‚   β”œβ”€β”€ CSIGenerator.h
β”‚   β”œβ”€β”€ CUICatalog.h
β”‚   β”œβ”€β”€ CUICommonAssetStorage.h
β”‚   β”œβ”€β”€ CUINamedLookup.h
β”‚   β”œβ”€β”€ CUIRenditionKey.h
β”‚   β”œβ”€β”€ CUIStructuredThemeStore.h
β”‚   β”œβ”€β”€ CUIThemeRendition.h
β”‚   β”œβ”€β”€ CoreUI.h
β”‚   └── structs.h
β”œβ”€β”€ Info.plist
└── Modules
    └── module.modulemap

When I try this it complains

xcodebuild -create-xcframework -framework XCFrameworks/CoreUI.framework -output CoreUI.xcframework
error: unable to read the file at 'XCFrameworks/CoreUI.framework/CoreUI'

Also recent macOS removed the on-disk DYLIBS in the same way they do on iOS, so they only exist in the dyld_shared_cache

This is a limitation with xcodebuild -create-xframework rather than the xcframework format itself. You can stick any binary into the framework, name it CoreUI, run -create-xcframework, and replace it with your CoreUI.tbd inside the created xcframework.

Related tidbit: if you ever need a stub xcframework without a tbd or a Mach-O (helpful for using .swiftmodules with SwiftPM), you can echo "!<arch>" > Foo.framework/Foo as that's a (dubiously) valid static library!

I'm trying to build a project following the XCFramework + TBD suggestion, but the linker refuses to work.

% swift build
Building for debugging...
error: link command failed with exit code 1 (use -v to see invocation)
ld: unknown file type in '/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug/StoreFoundation.framework/StoreFoundation.tbd'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: fatalError
[6/12] Linking libMASDownloadMetadata.dylib
% "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -dynamic -dylib -arch x86_64 -platform_version macos 10.13.0 10.13.0 -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk -o /Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug/libMASDownloadMetadata.dylib -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk/usr/lib/swift -L/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug -L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib /Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug/MASDownloadMetadataLib.build/MASDownloadMetadata.m.o -rpath /usr/lib/swift -framework StoreFoundation -install_name @rpath/libMASDownloadMetadata.dylib -rpath @loader_path -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/lib/darwin/libclang_rt.osx.a -F/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug -F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks -v 
@(#)PROGRAM:ld PROJECT:ld-1053.12
BUILD 15:44:24 Feb  3 2024
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
will use ld-classic for: armv6 armv7 armv7s arm64_32 i386 armv6m armv7k armv7m armv7em
LTO support using: LLVM version 15.0.0 (static support for 29, runtime is 29)
TAPI support using: Apple TAPI version 15.0.0 (tapi-1500.3.2.2)
Library search paths:
	/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk/usr/lib/swift
	/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk/usr/lib
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk/usr/lib/swift
Framework search paths:
	/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk/System/Library/Frameworks
ld: unknown file type in '/Users/user/Documents/MyTool/.build/x86_64-apple-macosx/debug/StoreFoundation.framework/StoreFoundation.tbd'

The structure of the XCFramework seems to be okay and so does the TBD file.

% tree StoreFoundation.xcframework
StoreFoundation.xcframework
β”œβ”€β”€ Info.plist
└── macos-arm64_x86_64
    └── StoreFoundation.framework
        β”œβ”€β”€ Headers
        β”‚   └── StoreFoundation
        β”‚       β”œβ”€β”€ ASDNotificationCenterDialogObserver-Protocol.h
        β”‚       β”œβ”€β”€ ...
        β”‚       └── StoreFoundation.h
        β”œβ”€β”€ Info.plist
        β”œβ”€β”€ Modules
        β”‚   └── module.modulemap
        └── StoreFoundation.tbd

6 directories, 99 files
--- !tapi-tbd
tbd-version:          4
targets:              [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
install-name:         '/System/Library/PrivateFrameworks/StoreFoundation.framework/Versions/A/StoreFoundation'
exports:
  - targets:          [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
    symbols:          [ OBJC_IVAR_$_CKDockMessaging._cachedIconPaths,
                       ...,
                       _markMainQueue ]
    objc-classes:    [ CKBook,
                       ...,
                       SSRequest ]
    objc-ivars:      [ CKBook._author,
                       ...,
                       SSRequest._state ]

This is the Package.swift for the project:

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MASDownloadMetadata",
    products: [
        .executable(name: "masdownloadmetadata", targets: ["MASDownloadMetadataBin"]),
        .library(name: "MASDownloadMetadata", type: .dynamic, targets: ["MASDownloadMetadataLib"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
    ],
    targets: [
        .executableTarget(
            name: "MASDownloadMetadataBin",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .target(name: "MASDownloadMetadataLib"),
            ]
        ),
        .target(
            name: "MASDownloadMetadataLib",
            dependencies: [
                .target(name: "StoreFoundation"),
            ]
        ),
        .binaryTarget(
            name: "StoreFoundation",
            path: "StoreFoundation.xcframework"
        )
    ]
)

Do you have any idea of what could be wrong with this configuration?

It looks like my TBD file was missing ... at the end of the file, causing ld to fail with unknown file type.

Fails to link:

--- !tapi-tbd
tbd-version:          4
targets:              [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
install-name:         '/System/Library/PrivateFrameworks/StoreFoundation.framework/Versions/A/StoreFoundation'
exports:
  - targets:          [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
    symbols:          [ OBJC_IVAR_$_CKDockMessaging._cachedIconPaths,
                       _markMainQueue ]
    objc-classes:    [ CKBook,
                       SSRequest ]
    objc-ivars:      [ CKBook._author,
                       SSRequest._state ]

Succeeds:

--- !tapi-tbd
tbd-version:          4
targets:              [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
install-name:         '/System/Library/PrivateFrameworks/StoreFoundation.framework/Versions/A/StoreFoundation'
exports:
  - targets:          [ x86_64-macos, x86_64-maccatalyst, arm64-macos, arm64-maccatalyst, arm64e-macos, arm64e-maccatalyst, armv7-ios, armv7s-ios, arm64-ios, arm64e-ios ]
    symbols:          [ OBJC_IVAR_$_CKDockMessaging._cachedIconPaths,
                       _markMainQueue ]
    objc-classes:    [ CKBook,
                       SSRequest ]
    objc-ivars:      [ CKBook._author,
                       SSRequest._state ]
...
1 Like

@kabiroberai I'd be curious what you think about this?

Where I'm trying to auto-gen XCFrameworks for ANY dylib in the DSC. Any feedback would be great! :)

1 Like

It just clicked that you created the ipsw tool β€” I've found it immensely useful for RE so thank you! XCFramework generation seems like it could be quite handy, and a repo with private frameworks would be neat. Once Objective-C is out of the way I wonder if this could be extended to regenerating private swiftinterface files by extracting metadata from the binary; might be worth exploring as a future direction.