Skip to content

Android NDK version needs to be visible in Swift #81402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
marcprux opened this issue May 9, 2025 · 1 comment
Open

Android NDK version needs to be visible in Swift #81402

marcprux opened this issue May 9, 2025 · 1 comment
Labels
Android Platform: Android cross-compilation Area → utils: Cross-compilation of project sources

Comments

@marcprux
Copy link
Contributor

marcprux commented May 9, 2025

The proposed Android SDK bundle (#80788) will depend on an external NDK being configured. The NDK changes various headers in between their releases, especially around nullability annotations being aded or removed, which affects Swift's view of the C functions and structures.

This means that in practice, some packages that rely on the definitions in the NDK headers will fail to build with earlier or later versions. For example, swift-nio builds successfully for Android with NDK 27, but fails for NDK 26 and 28:

swift-nio failure for NDK 26
[1618/1660] Compiling NIOPosix Linux.swift
/Users/runner/work/swift-package-builds/swift-package-builds/vapor/.build/checkouts/swift-nio/Sources/NIOPosix/System.swift:130:96: error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<msghdr>?, Int32) -> Int' is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<msghdr>, CInt) -> ssize_t' (aka '@convention(c) (Int32, UnsafeMutablePointer<msghdr>, Int32) -> Int')
 128 | #endif
 129 | #if canImport(Android)
 130 | private let sysRecvMsg: @convention(c) (CInt, UnsafeMutablePointer<msghdr>, CInt) -> ssize_t = recvmsg
     |                                                                                                `- error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<msghdr>?, Int32) -> Int' is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<msghdr>, CInt) -> ssize_t' (aka '@convention(c) (Int32, UnsafeMutablePointer<msghdr>, Int32) -> Int')
 131 | private let sysSendMsg: @convention(c) (CInt, UnsafePointer<msghdr>, CInt) -> ssize_t = sendmsg
 132 | #elseif !os(Windows)

/Users/runner/work/swift-package-builds/swift-package-builds/vapor/.build/checkouts/swift-nio/Sources/NIOPosix/System.swift:131:89: error: C function pointer signature '@Sendable (Int32, UnsafePointer<msghdr>?, Int32) -> Int' is not compatible with expected type '@convention(c) (CInt, UnsafePointer<msghdr>, CInt) -> ssize_t' (aka '@convention(c) (Int32, UnsafePointer<msghdr>, Int32) -> Int')
 129 | #if canImport(Android)
 130 | private let sysRecvMsg: @convention(c) (CInt, UnsafeMutablePointer<msghdr>, CInt) -> ssize_t = recvmsg
 131 | private let sysSendMsg: @convention(c) (CInt, UnsafePointer<msghdr>, CInt) -> ssize_t = sendmsg
     |                                                                                         `- error: C function pointer signature '@Sendable (Int32, UnsafePointer<msghdr>?, Int32) -> Int' is not compatible with expected type '@convention(c) (CInt, UnsafePointer<msghdr>, CInt) -> ssize_t' (aka '@convention(c) (Int32, UnsafePointer<msghdr>, Int32) -> Int')
 132 | #elseif !os(Windows)
 133 | private let sysRecvMsg: @convention(c) (CInt, UnsafeMutablePointer<msghdr>?, CInt) -> ssize_t = recvmsg

/Users/runner/work/swift-package-builds/swift-package-builds/vapor/.build/checkouts/swift-nio/Sources/NIOPosix/System.swift:139:102: error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<sockaddr>?, UnsafeMutablePointer<socklen_t>?) -> Int32' (aka '@Sendable (Int32, Optional<UnsafeMutablePointer<sockaddr>>, Optional<UnsafeMutablePointer<UInt32>>) -> Int32') is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt' (aka '@convention(c) (Int32, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<UInt32>) -> Int32')
 137 | #if canImport(Android)
 138 | private let sysGetpeername:
 139 |     @convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt = getpeername
     |                                                                                                      `- error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<sockaddr>?, UnsafeMutablePointer<socklen_t>?) -> Int32' (aka '@Sendable (Int32, Optional<UnsafeMutablePointer<sockaddr>>, Optional<UnsafeMutablePointer<UInt32>>) -> Int32') is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt' (aka '@convention(c) (Int32, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<UInt32>) -> Int32')
 140 | private let sysGetsockname:
 141 |     @convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt = getsockname

/Users/runner/work/swift-package-builds/swift-package-builds/vapor/.build/checkouts/swift-nio/Sources/NIOPosix/System.swift:141:102: error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<sockaddr>?, UnsafeMutablePointer<socklen_t>?) -> Int32' (aka '@Sendable (Int32, Optional<UnsafeMutablePointer<sockaddr>>, Optional<UnsafeMutablePointer<UInt32>>) -> Int32') is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt' (aka '@convention(c) (Int32, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<UInt32>) -> Int32')
 139 |     @convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt = getpeername
 140 | private let sysGetsockname:
 141 |     @convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt = getsockname
     |                                                                                                      `- error: C function pointer signature '@Sendable (Int32, UnsafeMutablePointer<sockaddr>?, UnsafeMutablePointer<socklen_t>?) -> Int32' (aka '@Sendable (Int32, Optional<UnsafeMutablePointer<sockaddr>>, Optional<UnsafeMutablePointer<UInt32>>) -> Int32') is not compatible with expected type '@convention(c) (CInt, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) -> CInt' (aka '@convention(c) (Int32, UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<UInt32>) -> Int32')
 142 | #elseif !os(Windows)
 143 | private let sysGetpeername:
swift-nio failure for NDK 28
[333/417] Compiling NIOPosix VsockAddress.swift
/Users/runner/work/swift-package-builds/swift-package-builds/swift-nio/Sources/NIOPosix/ThreadPosix.swift:66:9: error: cannot convert value of type 'ThreadDestructor' (aka '@convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer') to expected argument type '@convention(c) (UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer?'
 64 |         &handleLinux,
 65 |         nil,
 66 |         destructor,
    |         `- error: cannot convert value of type 'ThreadDestructor' (aka '@convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer') to expected argument type '@convention(c) (UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer?'
 67 |         args
 68 |     )

This is similar to needing to be able to check the Android API level described at #76671, except it is a build-time constant indicated by __NDK_MAJOR__ that wouldn't affect anything at runtime. Android API levels and NDK versions are unrelated (e.g., you can build for API 34+NDK 26 or API 33+NDK 27 or API 29+NDK 28), and so the Android API level cannot be used as a proxy for the NDK version.

The "just do it in C" solution might look like this:

#if __has_include(<android/ndk-version.h>)
#  include <android/ndk-version.h>
#if __NDK_MAJOR__ >= 27
struct adapted_struct = (compatible cast)some_ndk_struct_changed_in_27
#else
struct adapted_struct = (compatible cast)some_ndk_struct_in_26
#endif
#endif

A crude solution might be to have the Android SDK post-install script add per-NDK build variables for everything up to the current NDK, so that when building against NDK 26, swiftc would look like:

swiftc -DANDROID_NDK25 -DANDROID_NDK26 File.swift

and when building against NDK 27, it would look like:

swiftc -DANDROID_NDK25 -DANDROID_NDK26 -DANDROID_NDK27 File.swift

That way the Swift code could perform checks like:

#if os(Android)
#if ANDROID_NDK28
typealias adapted_func_signature = (Int) -> (Int)
#elseif ANDROID_NDK27
// NDK 27 changed nullability for this signature
typealias adapted_func_signature = (Int?) -> (Int)
#elseif ANDROID_NDK26
// NDK 28 changed nullability *again* for this signature!
typealias adapted_func_signature = (Int?) -> (Int?)
#else
#error("This package needs NDK 26+")
#endif
let param: adapted_func_signature = {  }
let result = call_ndk_func(param)
#endif

A solution built into Swift might look like:

#if _ndkVersion(>=27)
…
#else
…
#endif
@finagolfin
Copy link
Member

You're right that continually added nullability annotations in each NDK causes us to have to keep changing our C function signatures to match. That said, I doubt that almost anyone will actually use this feature to target multiple NDKs in the same Swift codebase, so I think we should just espouse the best practice that all Swift code should match the current LTS NDK. If someone wants to build with some other NDK, it is up to them to patch all their Swift code to work.

I'm not against adding this #if _ndkVersion feature though, as a few people may use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Android Platform: Android cross-compilation Area → utils: Cross-compilation of project sources
Projects
None yet
Development

No branches or pull requests

2 participants