Programming on iOS has never just been about chasing after Apple’s new APIs; often—indeed, more than necessary—it involves implementing more or less creative workarounds to overcome issues that arise specifically with the toolchains, Xcode being the primary one.
Since the introduction of Swift—ten years ago now—the quality of debugging apps with complex architectures has always been a battleground. From time to time, we find ourselves dealing with a debugger that just won’t cooperate.
The latest issue is with Xcode 16.0; basically, the error pops up when you try to use po
error: type for self cannot be reconstructed:
type for typename "$s15IndomioControls17AdsCollectionViewCD"
was not found (cached)
error: Couldn't realize Swift AST type of self.
Hint: using `v` to directly inspect variables and fields may still work.
The error appears only when you set a breakpoint in a Swift files.
There is no issues while debugging Obj-C file (I miss you bro).
Moreover it only affects the debugging part, but it doesn’t impact the compilation or the runtime execution of the code.
You’re really just left unable to debug the code—nothing too serious, right? I mean, who needs a debugger anyway? You can always just print stuff out like it’s the good old days!
I know you’re just itching for the solution; so if you’re about to lose your mind and really can’t be bothered to learn anything new, go ahead and skip straight to the last chapter of this article.
p, po, v… what the heck?
In our case, the problem occurs for all the modules (frameworks imported via SPM) linked to the main project. It was impossible to debug any variable, even when trying with the commands p
or v
(and, to top it off, the LLDB server crashed more than once).
The funny thing is that in the Variable View of Xcode, the variables were almost always visible.
Hint: you can ge the same output of the Variable View by using the command frame variable <name_variable>
.
But, what’s po
and why isn’t working?
Before that we need to briefly talk about LLDB.
How LLDB is able to format variables
LLDB isn’t just a debugger: it’s also a compiler—yep!
With copies of both the Swift and Clang compilers, it can evaluate complex expressions (with p
/po
being just aliases) and alter the program’s state using expr
.
But how does a debugger format a variable? Let’s consider the following statement:
let data = "Hello World"
If we hit a breakpoint in LLDB and try to print the content of a variable in a raw way, we get, as expected, a bunch of bytes:
(lldb) frame variable -L data // obtain the memory address
0x0000000100008000
(lldb) mem read 0x0000000100008000 // print the address content
0x100008000: 48 65 6c 6c 6f 20 57 6f 72 6c 64 00 00 00 00 eb Hello World.....
0x100008010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Not very helpful, right?
To provide a pretty printed format of the data, LLDB needs to know the types of these data (Int
, String
, Dictionary
, MyClass
etc.).
Since Xcode 14.x, this information comes from 2 different side, which explains why different commands may work or not:
- DEBUGGER SIDE
Here thep
andframe
commands lives.
Type information is obtained from Debug Info (the.o
and.dSYM
) and by using Swift Reflection. - COMPILER SIDE
Hhere thepo
andexpr
commands lives.
Type information is obtained from Modules:.swiftmodule
,.modulemap
,.h
, andbridging-header.h
.?
Modules are how the compilers organizes type declarations.
But we’ll get back to this in a bit; have you ever heard of swift-healthcheck
?
swift-healthcheck to the rescue!
This command was added a couple of years ago and is the first thing to keep in mind when analyzing a problem related to modules.
If we execute swift-healthcheck
after a problem with LLDB occour we can get access to a log the inner process of the Swift expression evaluator.
Let’s get back to our problem and try executing the command:
(lldb) po self.adId
error: type for self cannot be reconstructed: type for typename
"$s11IndomioCore7HTTPOpsO8AdDetailCD" was not found
error: Couldn't realize Swift AST type of self. Hint: using `v` to directly inspect
variables and fields may still work.
(lldb) swift-healthcheck
Health check written to /var/folders/fp/h_f5_67x16bbsfp5g4f76jc40000gn/T/lldb
/72168/lldb-healthcheck-512ff2.log
When we open the file mentioned, we find a bunch of information that can help us out.
==== LLDB swift-healthcheck log. ===
...
SwiftASTContextForExpressions(module: "IndomioCore", cu: "HTTPOps+Ad.swift")::LoadOneModule() -- Missing Swift module or Clang module found for "IndomioCommons", "imported" via SwiftDWARFImporterDelegate. Hint: Register Swift modules with the linker using -add_ast_path.
As we said, IndomioCore
is an internal framework and the error is telling us that for some reason, it failed to import the related module.
This missing module is a problem because LLDB can’t understand the data type related to self
, and as a result, it can’t format it.
Where Modules come from and who register them?
Each module contains the interfaces for its own APIs.
Generally speaking:
System Frameworks
Modules are integrated within the .sdk
file (e.g., MacOSX.sdk
).
LLDB will find a matching SDK to read them from as it’s attacching to your program on a debug session.
External Frameworks
The .dSYM
package generated by the build system usually contains all the associated modules (.swiftmodule
, bridging-header.h
, .swiftinterface
) related to the frameworks compiled by the target itself.
Dsymutil
can package a debug info archive called a .dSYM bundle for every dynamic library, framework or dylib, and the executable itself.
Each of these .dSYM
bundle can contain binary Swift modules.
In order for a Swift module to be picked up by dsymutil
, it needs to be registered with the linker. This registration occurs automatically for dynamic libraries and executables, but not for static archives. However, in many cases, Xcode handles this for you (not for custom build systems or custom build rules).
We didn’t discover much about why Xcode sometimes fails to map these files; we suspect it might somehow relate to how we build the project through Tuist.io, but that’s just a guess for now.
The Solution
Since the issue is that Xcode (or the linker) can’t map the modules with the data types of the program’s source files (and frameworks), the only solution for now is the one proposed by swift-healthcheck.
You can explicitly specify these modules for the Debug configuration using the -add-ast-path
command passed to XLinker.
(In brief, Xlinker is a command-line tool that allows developers to pass specific linker options directly when compiling applications by using -Xlinker
followed by the desired option for fine control over linking.)
We have a series of internal frameworks in our architecture depending on the service layer. To get this working, we need to repeat the process individually for each framewor by setting the Other Linker Flags
(OTHER_LDFLAGS
) of our main app target.
"-Xlinker",
"-add_ast_path",
"-Xlinker",
"'$(TARGET_BUILD_DIR)/\(frameworkName).framework/Modules/\(frameworkName).swiftmodule/$(NATIVE_ARCH_ACTUAL)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SHALLOW_BUNDLE_TRIPLE).swiftmodule'"
where frameworkName
is the name of the framework.
Since we’re using tuist.io
for creating project this looks like this:
func debugOtherLDFlags(forFrameworks frameworks: [String]?) -> SettingValue {
var otherLdFlags = ["-ObjC"] // default flag
frameworks?.forEach { otherLdFlags += astPathForFramework($0) }
return SettingValue.array(otherLdFlags)
}
func astPathForFramework(_ name: String) -> [String] {
[
"-Xlinker",
"-add_ast_path",
"-Xlinker",
"'$(TARGET_BUILD_DIR)/\(name).framework/Modules/\(name).swiftmodule/$(NATIVE_ARCH_ACTUAL)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SHALLOW_BUNDLE_TRIPLE).swiftmodule'"
]
}
And boom, magically your debugger is back to working!
This article wouldn’t have been possible without the help of the Immobiliare.it team (first of all Stefano D’Urso) and numerous online articles that guided us in finding the right solution.
As always, the best comes from teamwork.