Code that until recently was working just fine has recently started crashing for me, in release builds only. There have been recent changes at the very bottom of the call-stack (the introduction of a Task { ... } and await), but none relating to the instantiation or usage of the type that is suffering the crash.
I've tried to reproduce the issue in a standalone app, but it doesn't suffer the same problem. The affected type is AnyEncodable here:
import Foundation
struct Submittable: Encodable {
let id: Int
let user: Int
let answers: [Answer]
}
struct Answer: Encodable {
let id: Int
var value: AnyEncodable?
}
public struct AnyEncodable: Encodable {
private let encodeTo: (Encoder) throws -> Void
public init<T: Encodable>(_ value: T) {
encodeTo = { encoder in
var container = encoder.singleValueContainer()
try container.encode(value) // Crash occurs here in real app
}
}
public func encode(to encoder: Encoder) throws {
try encodeTo(encoder)
}
}
class FormSubmitter {
func submit(_ form: Submittable) throws {
let encoder = JSONEncoder()
let json = try encoder.encode(form)
let jsonString = String(bytes: json, encoding: .utf8) ?? "<Invalid JSON>"
print(jsonString)
}
}
Changing struct AnyEncodable to class AnyEncodable seems to fix the problem, but doesn't feel like it should be required. Hopefully it gives the right minds a strong hint at the likely cause.
For replicating the Task addition, I've simply called Task { try? FormSubmitter().submit(<instance of Submittable>) }.
If anybody is able to offer any debugging suggestions, I'd be happy to try them and report back on the outcome.
I've seen two different exceptions. One is NSInvalidArgumentException - *** -[NSPlaceholderString initWithString:]: nil argument
The other (and the print suffers it too) is of the form: Thread 15: EXC_BAD_ACCESS (code=2, address=0x109ad5148)
(sorry, I should have included that in the first report)
I don't know whether it's debugging a release build that's hurting visibility of values in the debugger, or a consequence of whatever's going wrong. Most times I've looked, the best Xcode has been able to tell me about value is that it's 𝜏_0_0. Strangely this time it's telling me that it's a valid empty string (and yet there's an exception).
While I've seen two outcomes, it certainly feels like they have a common cause. Between where the model is constructed and where the crash occurs, there's nothing but direct function calls - no threads, no async. And yet somehow a model comprising only structs and a closure capturing a struct appears to be trying to access memory that has been over-written.
Hard to tell without having a reproducible standalone test case, but if it is reproducible in your full app that's great. I'd try all of these:
print("encoding '(value)'") as per above suggestion to see what's being encoded, especially the last thing.
Xcode diagnostic tools, I'd try various permutation of those
Could be a benign stack overflow (that Xcode can't reliably check and tell you about)
For the last one it's worth checking (in the debugger at the point of crash):
p pthread_get_stackaddr_np(pthread_self())
register read sp
p pthread_get_stacksize_np(pthread_self())
The difference of the first two is how much currently on the stack and this value should be lower (with a generous leeway) than the third value. It would be concerning if there's less than, say, 2K left.
Yes, I've nuked all of DerivedData. It's not just my machine either - the first TestFlight build to exhibit this was built by somebody else using Xcode 14.3. I'm on 15 and seeing the same result.
It's the end of my day here @tera , but I will work through your suggestions on Monday.
As sp is less than the "base" address, I guess the stack extends downwards. The difference is just 0x1640 (5696 decimal), so I think that means we've got plenty of room:
(lldb) p pthread_get_stackaddr_np(pthread_self())
(UnsafeMutableRawPointer) 0x16bdf7000
(lldb) register read sp
sp = 0x000000016bdf59c0
(lldb) p pthread_get_stacksize_np(pthread_self())
(Int) 536576
I hadn't (everything between creating the struct and encoding the struct happens on the same thread), but I just tried it now. It didn't flag anything.
Indeed. That doesn't completely remove the possibility of stack overflow though (there could be a quick overflow and back). How big in bytes are the structs in question?
On occasions like that sometimes I had to start with the whole app and strip it down to a minimal reproducible case (sometimes to a handful of lines!) removing the unnecessary parts, testing every step along the way ensuring the bug is still there (if the bug disappears I backtrack and remove something else). Once down to one / few / a handful of lines the issue is immediately obvious as there is no more place for it to hide. In case of a compiler bug the resulting few-lines-of-code test case is very helpful for the compiler folks to fix the issue.