Skip to content

Commit

Permalink
XCTestDynamicOverlay: prefer delay loading XCTFail (#73)
Browse files Browse the repository at this point in the history
* XCTestDynamicOverlay: prefer delay loading `XCTFail`

Similar to the ObjC XCTest, prefer to delay load the XCTest runtime.
This is likely to fail on release distributions as XCTest is a developer
component. However, if it is available in the library search path on
(non-Darwin) Unix or in `Path` on Windows, it will be used.

Windows does not (yet) ship a static XCTest library. On Linux, it is
unclear whether the linkage is dynamic or static, so we perform a little
trick. We pass `RTLD_NOLOAD` to `dlopen` which will get a handle to
`libXCTest.so` if it is currently loaded in the address space but not
load the library if not already there. In such a case, it will return
`NULL`. In such a case, we resort to looking within the main binary
(assuming that XCTest may have been statically linked).

For both platforms, if we do not find the symbol from XCTest, we will
proceed to simply use the fallback path.

Note that both `GetProcAddress` and `dlsym` do not know how to cast the
resulting pointer to indicate the calling convention. As a result, we
rely on the use of `unsafeBitCast` to restore the calling convention to
`swiftcall`. To completely avoid the undefined behaviour here, we would
need to resort to C to perform the cast as these methods return type
erased pointers (i.e. `void *`).

* Update ci.yml

* XCTestDynamicOverlay: add an adjustment for Windows

Add a cast for Windows after the last round of tweaks for Linux.
Additionally, use `LoadLibraryA` instead of `LoadLibraryW` as we know
that the library name is ASCII compliant and the array buffer does not
work with `LoadLibraryW` and would require pulling in Foundation.

---------

Co-authored-by: Stephen Celis <[email protected]>
  • Loading branch information
compnerd and stephencelis authored Jan 17, 2024
1 parent 6f357ee commit 89a3632
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 12 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ jobs:
strategy:
matrix:
include:
- { toolchain: wasm-5.5.0-RELEASE }
- { toolchain: wasm-5.7.1-RELEASE }

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: echo "${{ matrix.toolchain }}" > .swift-version
- uses: swiftwasm/swiftwasm-action@v5.5
- uses: swiftwasm/swiftwasm-action@v5.7
with:
shell-action: swift build

Expand All @@ -66,4 +66,4 @@ jobs:
# this issue is fixed 5.9 so we can remove the if once
# that is generally available.
if: ${{ matrix.config == 'debug' }}
run: swift test
run: swift test
68 changes: 60 additions & 8 deletions Sources/XCTestDynamicOverlay/XCTFail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,69 @@ public struct XCTFailContext {
}
?? false
}
#elseif canImport(XCTest)
// NB: It seems to be safe to import XCTest on Linux
@_exported import func XCTest.XCTFail
#else
@_disfavoredOverload
public func XCTFail(_ message: String = "") {
print(noop(message: message))
private typealias XCTFailType = (_: String, _ file: StaticString, _ line: UInt) -> Void
private func unsafeCastToXCTFailType(_ pXCTFail: UnsafeRawPointer) -> XCTFailType {
// The function itself is a Swift function and must be marked as
// `__attribute__((__swiftcall__))`. However, translating the Swift
// signature `(_:file:line:) -> ()` to C is slightly tricky as we cannot
// guarantee the formal parameter set matches the actual ABI of the
// function. Work around this by exploiting some undefined behaviour. Take
// a pointer to the raw pointer, cast the pointee to the appropriate Swift
// signature, and then return the pointee. Given that the pointer itself
// is to a `.text` location which should not be unmapped, we should be
// able to deal with the escaping pointer remaining valid for the lifetime
// of the application. Unloading dynamically linked libraries is fraught
// with peril, and is generally unsupported.
withUnsafePointer(to: pXCTFail) {
UnsafeRawPointer($0).assumingMemoryBound(to: (@convention(thin) (_: String, _: StaticString, _: UInt) -> Void).self).pointee
}
}

#if os(Windows)
import WinSDK

private func ResolveXCTFail() -> XCTFailType? {
let hXCTest = LoadLibraryA("XCTest.dll")
guard let hXCTest else { return nil }

if let pXCTFail = GetProcAddress(hXCTest, "$s6XCTest7XCTFail_4file4lineySS_s12StaticStringVSutF") {
return unsafeCastToXCTFailType(unsafeBitCast(pXCTFail, to: UnsafeRawPointer.self))
}

return nil
}
#else
import Glibc

private func ResolveXCTFail() -> XCTFailType? {
var hXCTest = dlopen("libXCTest.so", RTLD_NOW)
if hXCTest == nil { hXCTest = dlopen(nil, RTLD_NOW) }

if let pXCTFail = dlsym(hXCTest, "$s6XCTest7XCTFail_4file4lineySS_s12StaticStringVSutF") {
return unsafeCastToXCTFailType(pXCTFail)
}

return nil
}
#endif

enum DynamicallyResolved {
static let XCTFail = {
if let XCTFail = ResolveXCTFail() {
return { (message: String, file: StaticString, line: UInt) in
XCTFail(message, file, line)
}
}
return { (message: String, _ file: StaticString, _ line: UInt) in
print(noop(message: message))
}
}()
}

@_disfavoredOverload
public func XCTFail(_ message: String = "", file: StaticString, line: UInt) {
print(noop(message: message, file: file, line: line))
public func XCTFail(_ message: String = "", file: StaticString = #file, line: UInt = #line) {
DynamicallyResolved.XCTFail(message, file, line)
}
#endif
#else
Expand Down

0 comments on commit 89a3632

Please sign in to comment.