po는 어떻게 동작하는가? 다른 방법으로 볼 수는 없을까?
Xcode에서 브레이크 포인트를 걸면, 다음과 같이 변수를 볼 수 있다.
그 옆에서는 lldb에 직접 커맨드를 보낼 수 있다.
예시로 쓸 코드
struct Trip {
var name: String
var destination: [String]
}
let cruise = Trip(
name: "Mediterranean Cruise",
destination: ["Sorrento", "Capri", "Taormina"]
)
po
print object description을 뜻한다
특정 타입의 텍스트 형태의 description을 출력한다.
(lldb) po cruise
🔽 Trip
- name: "Mediterranean Cruise"
🔽 destination: 3 elements
- 0 : "Sprrento"
- 1 : "Capri"
- 2 : "Taormina"
기본 제공 되는 것이 있지만, CustomDebugStringConvertible을 이용하면 커스텀도 가능하다.
다만 이 경우는 Top level desription만 바뀐다.
그 아래의 구조까지 커스텀하고 싶으면, CustomReflectable을 써야 한다.
Objective-C도 debugDescription() 메소드를 구현하면 동일하게 동작한다.
extension Trip: CustomDebugStringConvertible {
var debugDescription: String { "Trip description" }
}
(lldb) po cruise
🔽 Trip description
- name: "Mediterranean Cruise"
🔽 destination: 3 elements
- 0 : "Sprrento"
- 1 : "Capri"
- 2 : "Taormina"
실제로는 해당 지점에서 컴파일 가능한 임의의 표현식을 평가하는 것이 가능하다.
(lldb) po cruise.name.uppercased()
"MEDITERRANEAN CRUISE"
(lldb) po cruise.destination.sorted()
🔽 destination: 3 elements
- 0 : "Capri"
- 1 : "Sprrento"
- 2 : "Taormina"
이는 사실 po가 alias이기 때문이다.
expression --object-description -- cruise
alias는 유저도 얼마든지 가능하다.
command alias my_po expression --object-description
(lldb) po cruise.name
"Mediterranean Cruise"
po의 동작 원리
(lldb) po view
사용하고 있는 언어의 표현력을 활용하기 위해, LLDB는 직접 표현식을 평가하지 않는다.
대신 컴파일이 가능한 소스코드를 주어진 표현식을 기반으로 만든다.
// 실제와는 다를 수 있지만, 대략 이런 식으로
func __lldb_expr() {
__lldb_res = view
}
만들어진 소스코드를 내장된 컴파일러로(여기서는 Swift) 컴파일하고, 디버깅 중인 프로그램 context에서 실행하고 그 결과를 돌려받는다.
결과 값을 받았으니 여기서 object desription을 가져와야 한다.
앞에서 가져온 결과 값을 다른 코드로 래핑한다.
func __lldb_expr2 -> String() {
return __lldb_res.description
}
이 코드 역시 아까와 마찬가지로 컴파일 된 뒤 디버깅 중인 프로그램 컨텍스트에서 실행 후, 결과를 가져온다.
이렇게 가져온 결과를 유저에게 보여준다.
p
object desription이 없는 print
po와는 결과값의 모양이 다르게 나올 것이다. 하지만 담긴 정보는 동일하다.
(lldb) p cruise
(Travel.Trip) $R0 = {
name = "Mediterranean Cruise"
destination = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
}
$R0는 LLDB의 특별한 컨벤션이고, expression을 실행할 때 마다 뒤의 숫자가 올라간다.
이후 표현식 평가에서 해당 변수를 프로그램 안에서의 다른 변수처럼 그대로 쓸 수 있다.
(lldb) p $R0.description
([String]) $R1 = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
po 역시 alias다
expression
동작 원리
처음 결과를 가져오는 부분까지는 po와 동일하다.
결과를 가져오면, dynamic type resolution을 수행한다.
dynamic type resolution은 무엇인가?
swift에서는 소스코드에서의 static representation과 runtime의 dynamic type이 일치할 필요는 없다.
protocol Activity { }
struct Trip: Activity {
var name: String
var destination: [String]
}
let cruise: Activity = Trip(...)
그래서 LLDB에서는 최대한 정확한 데이터를 보여주기 위해서, 결과값의 메타데이터를 확인해서 보여주게 된다.
결과 값에 대해서만 dynamic type resolution을 수행하기 때문에, 실제로 코드상으로 유효하지 않은 표현식(static)은 p 명령어가 실패하게 된다.
p cruise.name
error: <EXPR>:3:8: error: value of type 'Activity' has no member 'name'
성공하게 하려면, 강제로 캐스팅해야 할 것이다.
(lldb) p (cruise as! Trip).name
(String) $R0 = "Mediterranean Cruise"
이후, LLDB의 Formatter 서브 시스템으로 넘겨서 사람이 읽을 수 있는 형태로 포매팅하게 된다.
String이 Formatter를 거치지 않는 경우
(lldb) expression --raw -- cruise.name
(Swift.String) $R0 = {
_guts = {
_object = {
_countAndFlagBits = {
_value = 7305804402515733574
}
}
}
...
}
LLDB는 일반적으로 쓰이는 여러 타입에 대해서 기본적인 Formatter를 가지고 있다.
v
기본적으로는 p와 동일한 결과
Xcode 10.2에서 추가된 alias다
(lldb) frame variable cruise
위의 두 커맨드와 다르게 컴파일과 코드 실행 단계를 거치지 않기 때문에 더 빠르다.
대신 사용하는 언어와는 별개의 고유 문법을 가지고 있다.
동작 원리
(lldb) v variable.field1.field2
dynamic type resolution을 여러번 하면 뭐가 다르지?
코드상으로는 해당 프로퍼티가 보이지 않아도, 실제로 해당 프로퍼티가 있다면, 찾아서 읽을 수 있다.
protocol Activity { }
struct Trip: Activity {
var name: String
var destination: [String]
}
let cruise: Activity = Trip(...)
(lldb) v cruise.name
(String) cruise.name = "Mediterranean Cruise"
정리
LLDB Data Formatter 커스터마이징하기