chinedufn / swift-bridge

swift-bridge facilitates Rust and Swift interop.
https://chinedufn.github.io/swift-bridge
Apache License 2.0
822 stars 60 forks source link

iOS app can't link to `extern "Swift"` code in Release mode #166

Open bes opened 1 year ago

bes commented 1 year ago

When I run my iOS application in Debug mode, everything is working correctly and the code is linking properly.

But when I build my iOS app in Release mode, for App Store release, I am greeted with the following errors:

Undefined symbols for architecture arm64:
  "___swift_bridge__$OfflineReceiver$_free", referenced from:
      core::ptr::drop_in_place$LT$myproj_ios..myproj_offline..ffi..OfflineReceiver$GT$::hb5a4651c8752da54 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$OfflineReceiver$on_manifest", referenced from:
      offline::progress::run_actor::_$u7b$$u7b$closure$u7d$$u7d$::hd5afb0205e5abf69 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$OfflineReceiver$on_offline_done", referenced from:
      myproj_ios::myproj_offline::download_variant::_$u7b$$u7b$closure$u7d$$u7d$::h17a6945056206be1 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$OfflineReceiver$on_progress", referenced from:
      myproj_ios::myproj_offline::ffi::OfflineReceiver::on_progress::h80e7a91a8a0acb74 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$get_api_url", referenced from:
      myproj_ios::myproj_offline::ffi::get_api_url::h8098cba097ff8c42 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$get_authentication_token", referenced from:
      myproj_ios::api::authorization_header::h3cb62deba6add7c5 in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$get_web_url", referenced from:
      myproj_ios::myproj_offline::ffi::get_web_url::h428b19a7e304204c in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
  "___swift_bridge__$log_to_swift", referenced from:
      myproj_ios::logging::ffi_log_to_swift::h7bb085a9a47d98bc in libmyproj_ios.a(myproj_ios-d575b7966d7d1c3d.myproj_ios.45588441-cgu.0.rcgu.o)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Basically it can't link against anything that is declared extern "Swift". I've tried a few things before reaching out, but I can't figure out what is going wrong.

Please help 🙏

chinedufn commented 1 year ago

How are you building your iOS application?

If you're using the Xcode + Cargo approach, if you look for LIBRARY_SEARCH_PATHS in your project.pbxproj do you see the path to your release binary?

If you're using the Swift package approach, did you include your release binary in the universal library?


But yeah, please go into more detail about your build setup.

bes commented 1 year ago

I am using the Swift package approach.

The binary is built like this:

    cargo build --release --target x86_64-apple-darwin
    cargo build --release --target aarch64-apple-darwin
    lipo \
        ./target/aarch64-apple-darwin/release/libmyproj_ios.a \
        ./target/x86_64-apple-darwin/release/libmyproj_ios.a -create -output \
        ./target/universal-macos/release/libmyproj_ios.a

    cargo build --release --target aarch64-apple-ios
    cargo build --release --target x86_64-apple-ios
    cargo build --release --target aarch64-apple-ios-sim
    lipo \
        ./target/aarch64-apple-ios-sim/release/libmyproj_ios.a \
        ./target/x86_64-apple-ios/release/libmyproj_ios.a -create -output \
        ./target/universal-ios/release/libmyproj_ios.a
    swift-bridge-cli create-package \
        --bridges-dir ./generated \
        --out-dir ../../video-ios/lib-myproj-ios \
        --ios target/aarch64-apple-ios/release/libmyproj_ios.a \
        --simulator target/universal-ios/release/libmyproj_ios.a \
        --macos target/universal-macos/release/libmyproj_ios.a \
        --name myprojIos

I didn't originally include the macos target, but added that as a way to see if I could move forward.

I'm happy to share more about my setup, but I'm not sure what to look for. Any help would be appreciated.

chinedufn commented 1 year ago

Gotcha.

Those symbols should be in the Swift code that swift-bridge is generating for you.

So, it sounds like your generated Swift code is being included in your Swift package in Debug, but not in Release.

Can you compare the Swift and C code in that ./generated directory between Debug and Release to see if the symbols are present in debug builds but absent in release builds?


If you'd like to explore yourself you can do clone the swift-bridge repo and use cargo run -p swift-bridge-cli create-package ... then poke around in here https://github.com/chinedufn/swift-bridge/blob/master/crates/swift-bridge-build/src/package.rs#L97-L112


If you can make a minimal reproduction of the issue such that we can run something like:

git clone https://github.com/bes/swift-bridge-issue-166
xcodebuild # whatever arguments are necessary ...

Then we can look into this.

bes commented 1 year ago

The thing is - I'm using the SAME swift-bridge package for XCiode Debug and XCode Release builds! What I mean is that my XCode project Debug build, the one that I use for debugging locally, is working, but when I switch over to Release to package it for App Store, then it fails - with no changes in the SPM pacakage.

I am also not using a spm-over-git setup, but rather I have embedded the package as a "local" spm package in XCode.

But I compared the output from the xcode compiler, and the code in the generated .swift file:

//       ___swift_bridge__$OfflineReceiver$_free
@_cdecl("__swift_bridge__$OfflineReceiver$_free")

I build my xcode project using Fastlane, which looks like this:

  lane :build_debug do
    gym(
      scheme: 'X-debug',
      configuration: 'Debug',
      include_symbols: true,
      export_options: {
        compileBitcode: false,
        uploadBitcode: false,
        uploadSymbols: true,
        method: "development",
        teamID: "xxx",
        provisioningProfiles: {
          // ...
        },
      },
    )
  end

  lane :build_release do
    gym(
      scheme: "X-release",
      configuration: "Release",
      export_method: "app-store",
      export_options: {
        compileBitcode: false,
        uploadBitcode: false,
        uploadSymbols: true,
        method: "app-store",
        teamID: "xxx",
        provisioningProfiles: {
            // ...
        },
      },
    )
  end

But the build failure also happens when I switch over to the release profile in xcode and build from there.

I will try to make a proof-of-concept, but that will probably take some time to get working.

Summary:

bes commented 1 year ago

OK so I made some progress -

@_cdecl("__swift_bridge__$OfflineReceiver$_free")
public func __swift_bridge__OfflineReceiver__free (ptr: UnsafeMutableRawPointer) {

If I slap public on the header functions generated by extern "Swift" then it compiles!

Seems like XCode is aggressively pruning those functions in the Release build, for some reason.

bes commented 1 year ago

For now I'm making do with

sed -i '' 's/^func __swift_bridge__/public func __swift_bridge__/g' mylib.swift

But I feel like either I should modify my XCode build somehow or this is a bug in swift-bridge.

chinedufn commented 1 year ago

Yeah we should solve this in swift-bridge.

We just need to make this a public func instead of func https://github.com/chinedufn/swift-bridge/blob/3c0d00da33fdf19e201eb121c7e6e370897f8070/crates/swift-bridge-ir/src/codegen/generate_swift.rs#L317-L319


We can add a mod visibility_codegen_tests to the codegen tests https://github.com/chinedufn/swift-bridge/blob/3c0d00da33fdf19e201eb121c7e6e370897f8070/crates/swift-bridge-ir/src/codegen/codegen_tests.rs#L30-L49

And then in that module we can add a extern_swift_functions_public test where we confirm that we generate a public func @_cdecl function.


We can use ExpectedRustTokens::SkipTest and ExpectedCHeader::SkipTest, like this https://github.com/chinedufn/swift-bridge/blob/4f2a9d70ceabfae1708628bb637c7ba0ac9225d7/crates/swift-bridge-ir/src/codegen/codegen_tests/extern_rust_method_swift_class_placement_codegen_tests.rs#L32-L34 https://github.com/chinedufn/swift-bridge/blob/4f2a9d70ceabfae1708628bb637c7ba0ac9225d7/crates/swift-bridge-ir/src/codegen/codegen_tests/extern_rust_method_swift_class_placement_codegen_tests.rs#L95-L97

jmp-0x7C0 commented 7 months ago

@chinedufn I've put up a PR (#262) based on these instructions.

Yeah we should solve this in swift-bridge.

We just need to make this a public func instead of func

https://github.com/chinedufn/swift-bridge/blob/3c0d00da33fdf19e201eb121c7e6e370897f8070/crates/swift-bridge-ir/src/codegen/generate_swift.rs#L317-L319

We can add a mod visibility_codegen_tests to the codegen tests

https://github.com/chinedufn/swift-bridge/blob/3c0d00da33fdf19e201eb121c7e6e370897f8070/crates/swift-bridge-ir/src/codegen/codegen_tests.rs#L30-L49

And then in that module we can add a extern_swift_functions_public test where we confirm that we generate a public func @_cdecl function.

We can use ExpectedRustTokens::SkipTest and ExpectedCHeader::SkipTest, like this

https://github.com/chinedufn/swift-bridge/blob/4f2a9d70ceabfae1708628bb637c7ba0ac9225d7/crates/swift-bridge-ir/src/codegen/codegen_tests/extern_rust_method_swift_class_placement_codegen_tests.rs#L32-L34

https://github.com/chinedufn/swift-bridge/blob/4f2a9d70ceabfae1708628bb637c7ba0ac9225d7/crates/swift-bridge-ir/src/codegen/codegen_tests/extern_rust_method_swift_class_placement_codegen_tests.rs#L95-L97

extrawurst commented 5 months ago

lol i ran into the same issue: https://github.com/rustunit/bevy_ios_iap/commit/ba61693731d11a81d15aea11d75519b00f60e284 seems xcode linker optimized funcs away that are not public and not used from inside their own swift package

llbartekll commented 3 weeks ago

hey, Im running into the same issue @chinedufn do you think we can make it work without running the script that makes functions public?

chinedufn commented 3 weeks ago

If someone writes a PR that solves this issue, I can merge it.

A contributor wrote a pull-request that fixed it, but they are no longer planning to work on addressing the PR's feedback.

https://github.com/chinedufn/swift-bridge/pull/262

Anyone can feel free to fork that branch, or create their own new brach, and then get the fix working.