Skip to content

Commit 89a3632

Browse files
XCTestDynamicOverlay: prefer delay loading XCTFail (#73)
* 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]>
1 parent 6f357ee commit 89a3632

File tree

2 files changed

+64
-12
lines changed

2 files changed

+64
-12
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ jobs:
3535
strategy:
3636
matrix:
3737
include:
38-
- { toolchain: wasm-5.5.0-RELEASE }
38+
- { toolchain: wasm-5.7.1-RELEASE }
3939

4040
steps:
41-
- uses: actions/checkout@v3
41+
- uses: actions/checkout@v4
4242
- run: echo "${{ matrix.toolchain }}" > .swift-version
43-
- uses: swiftwasm/swiftwasm-action@v5.5
43+
- uses: swiftwasm/swiftwasm-action@v5.7
4444
with:
4545
shell-action: swift build
4646

@@ -66,4 +66,4 @@ jobs:
6666
# this issue is fixed 5.9 so we can remove the if once
6767
# that is generally available.
6868
if: ${{ matrix.config == 'debug' }}
69-
run: swift test
69+
run: swift test

Sources/XCTestDynamicOverlay/XCTFail.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,69 @@ public struct XCTFailContext {
144144
}
145145
?? false
146146
}
147-
#elseif canImport(XCTest)
148-
// NB: It seems to be safe to import XCTest on Linux
149-
@_exported import func XCTest.XCTFail
150147
#else
151-
@_disfavoredOverload
152-
public func XCTFail(_ message: String = "") {
153-
print(noop(message: message))
148+
private typealias XCTFailType = (_: String, _ file: StaticString, _ line: UInt) -> Void
149+
private func unsafeCastToXCTFailType(_ pXCTFail: UnsafeRawPointer) -> XCTFailType {
150+
// The function itself is a Swift function and must be marked as
151+
// `__attribute__((__swiftcall__))`. However, translating the Swift
152+
// signature `(_:file:line:) -> ()` to C is slightly tricky as we cannot
153+
// guarantee the formal parameter set matches the actual ABI of the
154+
// function. Work around this by exploiting some undefined behaviour. Take
155+
// a pointer to the raw pointer, cast the pointee to the appropriate Swift
156+
// signature, and then return the pointee. Given that the pointer itself
157+
// is to a `.text` location which should not be unmapped, we should be
158+
// able to deal with the escaping pointer remaining valid for the lifetime
159+
// of the application. Unloading dynamically linked libraries is fraught
160+
// with peril, and is generally unsupported.
161+
withUnsafePointer(to: pXCTFail) {
162+
UnsafeRawPointer($0).assumingMemoryBound(to: (@convention(thin) (_: String, _: StaticString, _: UInt) -> Void).self).pointee
163+
}
164+
}
165+
166+
#if os(Windows)
167+
import WinSDK
168+
169+
private func ResolveXCTFail() -> XCTFailType? {
170+
let hXCTest = LoadLibraryA("XCTest.dll")
171+
guard let hXCTest else { return nil }
172+
173+
if let pXCTFail = GetProcAddress(hXCTest, "$s6XCTest7XCTFail_4file4lineySS_s12StaticStringVSutF") {
174+
return unsafeCastToXCTFailType(unsafeBitCast(pXCTFail, to: UnsafeRawPointer.self))
175+
}
176+
177+
return nil
178+
}
179+
#else
180+
import Glibc
181+
182+
private func ResolveXCTFail() -> XCTFailType? {
183+
var hXCTest = dlopen("libXCTest.so", RTLD_NOW)
184+
if hXCTest == nil { hXCTest = dlopen(nil, RTLD_NOW) }
185+
186+
if let pXCTFail = dlsym(hXCTest, "$s6XCTest7XCTFail_4file4lineySS_s12StaticStringVSutF") {
187+
return unsafeCastToXCTFailType(pXCTFail)
188+
}
189+
190+
return nil
191+
}
192+
#endif
193+
194+
enum DynamicallyResolved {
195+
static let XCTFail = {
196+
if let XCTFail = ResolveXCTFail() {
197+
return { (message: String, file: StaticString, line: UInt) in
198+
XCTFail(message, file, line)
199+
}
200+
}
201+
return { (message: String, _ file: StaticString, _ line: UInt) in
202+
print(noop(message: message))
203+
}
204+
}()
154205
}
206+
155207
@_disfavoredOverload
156-
public func XCTFail(_ message: String = "", file: StaticString, line: UInt) {
157-
print(noop(message: message, file: file, line: line))
208+
public func XCTFail(_ message: String = "", file: StaticString = #file, line: UInt = #line) {
209+
DynamicallyResolved.XCTFail(message, file, line)
158210
}
159211
#endif
160212
#else

0 commit comments

Comments
 (0)