Skip to content

Enable Bundle bridging#228

Merged
marcprux merged 1 commit intomainfrom
bridge-bundle
Apr 9, 2026
Merged

Enable Bundle bridging#228
marcprux merged 1 commit intomainfrom
bridge-bundle

Conversation

@marcprux
Copy link
Copy Markdown
Member

@marcprux marcprux commented Apr 9, 2026

Currently, trying to pass a Bundle from a native Skip Fuse module to a transpiled Skip Lite module as a parameter will fail to compile with the error:

[2/6] Emitting module SkipAndroidBridgeSamples
/Users/marc/Library/Developer/Xcode/DerivedData/Skip-Everything-aqywrhrzhkbvfseiqgxuufbdwdft/Build/Intermediates.noindex/BuildToolPluginIntermediates/skip-android-bridge.output/SkipAndroidBridgeSamples/skipstone/SkipAndroidBridgeSamples/src/main/swift/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift:21:13: error: function cannot be declared public because its parameter uses an internal type
19 | }
20 | 
21 | public func getAssetURL(named name: String, in bundle: Bundle? = nil) -> URL? {
   |             `- error: function cannot be declared public because its parameter uses an internal type
22 |     (bundle ?? Bundle.module).url(forResource: name, withExtension: nil)
23 | }

/Users/marc/Library/Developer/Xcode/DerivedData/Skip-Everything-aqywrhrzhkbvfseiqgxuufbdwdft/Build/Intermediates.noindex/BuildToolPluginIntermediates/skip-android-bridge.output/SkipAndroidBridgeSamplesTests/skipstone/SkipAndroidBridgeSamples/.build/SkipAndroidBridgeSamples/swift/plugins/outputs/swift/SkipAndroidBridgeSamples/destination/skipstone/SkipBridgeGenerated/Bundle_Support.swift:6:11: note: type declared here
 4 | import SkipAndroidBridge
 5 | 
 6 | typealias Bundle = AndroidModuleBundle
   |           `- note: type declared here
 7 | class AndroidModuleBundle : AndroidBundle, @unchecked Sendable {
 8 |     required init(_ bundle: SkipAndroidBridge.BundleAccess) {

/Users/marc/Library/Developer/Xcode/DerivedData/Skip-Everything-aqywrhrzhkbvfseiqgxuufbdwdft/Build/Intermediates.noindex/BuildToolPluginIntermediates/skip-android-bridge.output/SkipAndroidBridgeSamples/skipstone/SkipAndroidBridgeSamples/src/main/swift/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift:25:13: error: function cannot be declared public because its parameter uses an internal type
23 | }
24 | 

This is because in order to handle how Android stores resources (as assets and accessible through the AssetManager), we need to subclass Foundation.Bundle as an AndroidBundle. When SwiftPM generates its Bundle.module property, it spits out a resource_bundle_accessor.swift file that will be compiled along with the module like:

import Foundation

extension Foundation.Bundle {
    static let module: Bundle = {
        let mainPath = \(mainPathSubstitution)
        let buildPath = "\(bundlePath.pathString.asSwiftStringLiteralConstant)"

        let preferredBundle = Bundle(path: mainPath)

        guard let bundle = preferredBundle ?? Bundle(path: buildPath) else {
            // Users can write a function called fatalError themselves, we should be resilient against that.
            Swift.fatalError("could not load resource bundle: from \\(mainPath) or \\(buildPath)")
        }

        return bundle
    }()
}

So in order to intercept this and return our subclass, the SwiftPM plugin spits out a typealias from Bundle to a local class AndroidModuleBundle : AndroidBundle that defines its own init?(path: String) so we can return the AndroidBundle subclass. It does this in a swift/plugins/outputs/swift/SkipAndroidBridgeSamples/destination/skipstone/SkipBridgeGenerated/Bundle_Support.swift file created by the KotlinBundleTransformer.swift.

All told, the relevant source and generate file tree looks like this for the SkipAndroidBridgeSamples module's resources:

.
├── .build
│   ├── aarch64-unknown-linux-android28
│   │   └── debug
│   │       └── SkipAndroidBridgeSamples.build
│   │           └── DerivedSources
│   │               └── resource_bundle_accessor.swift
│   └── plugins
│       └── outputs
│           └── skip-android-bridge
│               └── SkipAndroidBridgeSamples
│                   └── destination
│                       └── skipstone
│                           ├── SkipAndroidBridgeSamples
│                           │   └── src
│                           │       └── main
│                           │           └── assets
│                           │               └── skip
│                           │                   └── android
│                           │                       └── bridge
│                           │                           └── samples
│                           │                               └── Resources
│                           │                                   └── SkipAndroidBridgeSamples.json -> /opt/src/github/skiptools/skip-android-bridge/Sources/SkipAndroidBridgeSamples/Resources/SkipAndroidBridgeSamples.json
│                           └── SkipBridgeGenerated
│                               └── Bundle_Support.swift
└── Sources
    └── SkipAndroidBridgeSamples
        └── Resources
            └── SkipAndroidBridgeSamples.json

This has been working fine: Bundle.module.url(forResource: "foo") will return asset:/my/kotlin/package/Resources/foo as expected (which is subsequently handled by a custom URL scheme handler, both on the Kotlin and Swift sides). However, because it uses a package-internal subclass of AndroidBundle, it isn't bridgeable. And even it if was, each module would have their own separate AndroidModuleBundle class definition that would be incompatible with each other.

This PR changes the way we generate the local Bundle.init(path: String) override by instead making a public typealias Bundle = AndroidBundle to the base AndroidBundle, and intercepting the initialization from resource_bundle_accessor.swift with convenience init?(path: String, unusedp_0: Void? = nil) .

This makes Bundle bridgeable and simplifies the internal handling of AndroidBundle by eliminating the custom per-module subclass and just using the (bridgeable) base class the destination for the Bundle typealias.

@cla-bot cla-bot bot added the cla-signed label Apr 9, 2026
@marcprux marcprux merged commit 779cdf0 into main Apr 9, 2026
4 of 5 checks passed
@marcprux marcprux deleted the bridge-bundle branch April 9, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant