読者です 読者をやめる 読者になる 読者になる

iOSエンジニアのための LLDB Plugin 入門

f:id:dealforest:20160902102340p:plain

iOSDC」「AKIBA.swift×Swift愛好会」「iOSDC Reject Conference days2」で話させていただいたんですが、発表ではなるべく興味を持ってもらえるような内容になっていて、LLDB Plugin の作り方など詳細についてはふれていませんでした。

本来ならブログ書いてあるので見てください。と言いたかったんですが、なかなか手が回らず今になってしまいました。 できることも多いので、回数を分けて書いていければなと思います。

言語選択

LLDB の Plugin は Python で作られています。 ただし、Python が全くわからなくても作れているんで安心してください。

LLDB で実行する際に iOS エンジニアなら2つの言語から、どちらで実行するかを選択する必要があります。

正直どちらを選んでもいいと思っています。 どちらにせよ LLDB に寄り添わなければ意図した通りには動かないので。。。

以下に違いをまとめました。

Swift の場合

  • 実行する Xcode のバージョン (toolchain) に 依存してしまう
  • Objective-C の instance を扱うのが面倒くさい (頑張る必要がある)

Objective-C の場合

  • Swift の instance を扱おうとすると面倒くさい (頑張る必要がある)
  • Swift から使う場合でも頑張れば共通化して使うことができます
  • 関数の定義はできない

なので、自分のプロジェクトに合う、もしくは書きたい言語で書けばいいかなと思います。

facebook/chisel という facebook が作っている LLBD コマンド集があるのですが、これは Objectvie-C を採用しています。興味ある方は見てみると面白いかと思います。 ただし Swift から使う場合には少しクセがあるため慣れが必要となります。

chisel については「iOS開発のデバッグツールchiselの紹介」を見てもらえればどういうものか分かるかと思います。

テンプレート

これが一番シンプルなテンプレートです。 それぞれ Foundation 等 framework 内で完結するようなマクロでしたらこれで問題ありません。 例えば UIImage をファイルに出力したり、API レスポンスの NSData をデシリアライズして表示したりなどです。

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("""
    expr -l swift --
    func $<functionName1>() {
        ...
    }
    """.strip())

    // 1命令1行の制約があるので、別の関数を定義しようと思うと2回実行する必要がある
    lldb.debugger.HandleCommand(""" 
    expr -l swift --
    func $<functionName2>() {
        ..
    }
    """.strip())

    lldb.debugger.HandleCommand('expr -l swift -- $<functionName>()')

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f <fileName>.process <commandName>")
    print"<commandName> command enabled."

Swift の場合は、処理を関数内に閉じ込めれば LLDB の事を意識せずに書けます。 なぜ 関数を定義して実行しているのかは、この資料を見てもらえればわかるかなと思います。

簡単にまとめますと

  • 1命令1行
  • 実行時に po をつける必要がある
  • 変数と関数の定義、参照をする場合に $ をつける必要がある(上書きできないという制約付き)

これらを回避するための工夫が関数にすることです。 なので Playground などで挙動を確認して最後に LLDB のコマンドにするといったフローで作るのがオススメです。

あと Optional は forced unwrapping するのでいいのじゃないかと思っています。 何かおかしい時に把握しやすいので僕はそうしてます。

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("expr -l objc -O -- ...")

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f <fileName>.process <commandName>")
    print "<commandName> command enabled."

Objective-C の場合は LLDB で実行する時と同じことに注意しないといけません。 上で簡単にまとめた内容を全て意識する必要が出てきます。 LLDB で実行する命令のマクロみたいな認識がわかりやすいかもしれませんね。

いざコマンドを作る時に自分で決める必要があるものが3つあります。 (process と言う名前は変えても問題ありません)

  • fileName
  • commandName
  • functionName

実際に使う時に影響があるのは commandName だけなのでどういうコマンド名にしたいかさえ決めれば作れます。 悩むくらいなら全部同じ名前でもいいんじゃないですかね。

あとは、どういう挙動をさせたいかを書けば出来上がりです。

簡単なサンプル

まず初めに簡単に引数を出力する関数を作ってみましょう Xcode7.3.1 で試しています。

コマンド名以降が全て command という引数に渡ってくるので

Swift

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("""
    expr -l swift --
    func $process(text: String) {
        print(text)
    }
    """.strip())
    lldb.debugger.HandleCommand('expr -l swift -- $process(' + command + ')')

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f echo.process echo")
    print "echo command enabled."

Objective-C

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("expr -l objc -O -- NSLog(@" + command + ")")

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f echo.process echo")
    print "echo command enabled."

LLDB

(lldb) command script import <path_to>/echo.py
echo command enabled.
(lldb) echo "dealforest"
dealforest

毎回読み込むのが面倒くさい場合は ~/.lldbinit に書いておけば LLDB 起動時に読み込んでくれます。

command script import <path_to>/echo.py --allow-reload

--allow-reload つけておけばデバッグの時に楽なのでつけておくことをオススメします。

他にも、ちょっとしたコマンドを dealforest/dotfile に置いてあるので参考までにどうぞ。

追記: 別リポジトリにしました。 github.com

  • show_image.py - 画像を保存し Preview で表示する
  • slack.py - ファイルを slack へ送信
  • ambigurous_layout.py - ambigurous layout の場合、一定間隔でレイアウトが変更される

その他にもあるのですが、少しトリッキーな感じで作っているので、そのうち綺麗にして追加していこうかなと思っています。

次回は Python, LLDB, Swift を連携させるやり方を書こうかなと思います。 ではでは。