LLDB: 「po」の先へ – WWDC2019

Session概要

LLDBは、実行時にアプリの確認とデバッグができる強力なツールです。
このセッションでは、アプリの値を表示する様々な方法、カスタムのデータ型をフォーマットする方法、独自のPython 3 スクリプトを使用してLLDBを拡張する方法についての説明 https://developer.apple.com/videos/play/wwdc2019/429/

LLDBについて

  • Xcodeで扱う変数のビューアーとして機能させるデバッガ
    • 変数と型を確認可能
  • デバッグ中にコマンドの入力も可能
    • po (print object): オブジェクトの記述・説明を返す、型のインスタンスをテキストで表す
    • カスタマイズ方法
    • コンパイルできるAPIであれば、引数として渡すことができる
    • poはexpressionコマンドのエイリアスであり、 expression –object-description を含んでいる
    • そのため、独自の「po」を実装するにはaliasコマンドを使用する
struct Trip {
 var name: String
 var destinations: [String]
}

// 最上位
extension Trip: CustomDebugStringConvertible {
 var debugDescription: String { "Trip description" }
}

// 下位
extension Trip: CustomReflectable {
...
}


(lldb) po cruise
▿ Trip description // カスタマイズした形で表示される
 - name : "Mediterranean Cruise"
 ▿ destinations : 3 elements
 - 0 : "Sorrento"
 - 1 : "Capri"
 - 2 : "Taormina"

// 独自のpo(my_po)のエイリアス登録
(lldb) command alias my_po expression --object-description
(lldb) my_po cruise

po(print object)の仕組み

poが値を出力するフロー

  • LLDBは式の解析や評価をしない
  • 与えられた式をコンパイルしてソースコードを生成する
  • デバッグするプログラムのコンテキストで実行する
  • 実行が完了するとLLDBが結果にアクセスする
  • オブジェクトの説明を表示するために、直近の結果を別のソースにラップする
  • その後ラップしたソースもコンパイルし、同じコンテキストで実行する
  • 実行結果は文字列で表現


その他のLLDBでの出力コマンド

p(print)コマンド

  • poと表示される内容は同等
  • pコマンド実行後、LLDB特有の変数が連番で生成され、後から参照可能
  • poと同様 expressionコマンドのエイリアスですが、 object-descriptionオプションを含まない

pが値を出力するフロー

  • コンパイルから式の評価まではpoと同じ
  • 結果を取得すると、LLDBが方の動的解決を行う
    • pで出力する際に対象オブジェクトの最も正確な型をLLDBが表示するため
    • 例えば、protocol Aに準拠したstruct Bをinstance化したhogeを p hoge すると型としては動的な型Bが出力される
    • ただし、LLDBがpコマンド実行時のコンパイルで参照するのは、ソースコード中の静的な型であるため、動的な型(ここではstruct B)のプロパティにアクセスは出来ない
    • エラーを出さずに実行するには、動的な型にキャストして実行する必要がある
  • 動的解決後、LLDBがフォーマッタに結果を渡す
    • ヒューマンリーダブルな形で表示するために必要
    • フォーマッタのカスタマイズも可能

vコマンド

  • 出力結果はpコマンドと同じ
  • フォーマッタも適用される
  • エイリアスは frame variableコマンドであり、Xcode 10.2から導入された
  • po, pと異なり、コンパイルされない
  • 対象オブジェクトに対して、オーバーロードせず、プロパティの評価もしない

vが値を出力するフロー

  • プログラムの状態を調べて変数をメモリに配置する
  • メモリから変数を読み込む
  • 動的解決を行う
  • サブフィールド(struct内のpropertyなど)を出力する場合は、個数分の処理を繰り返し、その都度 動的解決を行う
  • 完了後、結果をフォーマッタに渡す



各ステップで動的解決を行うため、vコマンドではデバッグ対象オブジェクトの動的な型を判断可能。この点がvがpより優れている点(キャストせず構造体を参照可能)

po, p, v コマンドの比較表

データフォーマッタ(Data Formatter)のカスタマイズ

Filter

  • 出力する表示を制限する
struct Trip {
 var name: String
 var destinations: [String]
}

let cruise = Trip(
 name: "Mediterranean Cruise",
 destinations: ["Sorrento", "Capri", "Taormina"])

// フィルターの設定
(lldb) type filter add Travel.Trip --child name
(lldb) v cruise
(Travel.Trip) cruise = (name = "Mediterranean Cruise")

// フィルターを削除
(lldb) type filter delete Travel.Trip

String summaries

  • ひと目で型がわかるように型の表示を変更する
  • 下記のように、サマリ定義する変数を var で参照してカスタマイズする
struct Trip {
 var name: String
 var destinations: [String]
}

let cruise = Trip(
 name: "Mediterranean Cruise",
 destinations: ["Sorrento", "Capri", "Taormina"])

(lldb) type summary add Travel.Trip --summary-string
"${var.name} from ${var.destinations[0]} to ${var.destinations[2]}"

(lldb) v cruise
(Travel.Trip) cruise = "Mediterranean Cruise" from "Sorrento" to "Taormina"

Python Formatter

  • 任意の計算が可能で、LLDBのAPIにフルアクセスできる
  • Xcode 11から Python 3が使用される
  • scriptコマンドでPythonインタプリタを起動
  • 現在のフレームはlldb.frameでアクセス可能で、SBFrameが返る
  • ファイル読み込みも可能
struct Trip {
 var name: String
 var destinations: [String]
}

let cruise = Trip(
 name: "Mediterranean Cruise",
 destinations: ["Sorrento", "Capri", "Taormina"])

(lldb) script
>>> cruise = lldb.frame.FindVariable("cruise")
>>> print(cruise)
(Travel.Trip) cruise = {
 name = "Mediterranean Cruise"
 destinations = 3 values {
 …
}

>>> destinations = cruise.GetChildMemberWithName("destinations")
>>> print(destinations)
([String]) destinations = 3 values {
 [0] = "Sorrento"
 [1] = "Capri"
 [2] = "Taormina"
}

>>> count = destinations.GetNumChildren()
>>> begin = destinations.GetChildAtIndex(0)
>>> print(begin)
(String) [0] = "Sorrento"

>>> end = destinations.GetChildAtIndex(count - 1)
>>> print(end)
(String) [2] = "Taormina"

>>> print("Trip from {} to {}".format(begin, end))
Trip from (String) name = "Sorrento" to (String) name = "Taormina"

>>> print("Trip from {} to {}".format(begin.GetSummary(), end.GetSummary()))
Trip from "Sorrento" to "Taormina"

pythonファイルの読み込み

// Trip.py
def SummaryProvider(value, _):
  destinations = value.GetChildMemberWithName("destinations")
  count = destinations.GetNumChildren()
  if count == 0:
     return "Empty trip"
  begin = destinations.GetChildAtIndex(0).GetSummary()
  end = destinations.GetChildAtIndex(count - 1).GetSummary()

  return "Trip with {} stops from {} to {}".format(count, begin, end)
// 用意したpythonファイルのimport
(lldb) command script import Trip.py

// summary addでフォーマットを適用する型とサマリを指定
(lldb) type summary add Travel.Trip --python-function Trip.SummaryProvider
(lldb) v cruise
(Travel.Trip) cruise = Trip with 3 stops from "Sorrento" to "Capri"

Synthetic children

  • 変数ビューの表示が複雑な型などでもカスタマイズ可能
  • pythonで、関数ではなくメソッドを実装したクラスを用意する
// Trip.py
class ExampleSyntheticChildrenProvider:
 def __init__(self, value, _):
 …
 def num_children(self):
 …
 def get_child_at_index(self, index):
 …
 def get_child_index(self, name):
 …
(lldb) command script import Trip.py

// フォーマッタを設定するため、 type synthetic addで型とクラスを指定
(lldb) type synthetic add Travel.Trip --python-class Trip.ExampleSyntheticChildrenProvider

カスタマイズしたコマンドの永続化

  • コンソールで実行したコマンドはホームディレクトリの .lldbinitファイルに保存される
  • デバッグセッションの開始時に自動でロードされる
最新情報をチェックしよう!