しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

LLDBからRxSwiftのdebugを仕込む

毎回ソースコードいじってRxSwiftのdebug仕込むのってめんどいですよね。
そこで、スクリプトを使ってLLDBから仕込めるようにしました。

流れてくるイベントを簡単にチェックするのに役に立つかもしれません。

スクリプト

Pythonスクリプトを適当な場所においておきます。

#!/usr/bin/env python3

import lldb

def debugo(debugger, name, result, dict):
    lldb.debugger.HandleCommand('e %s.debug("::%s").subscribe()' % (name, name))

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f debugo.debugo deo')

これを.lldbinit.lldbinit-Xcodeで読み込みます。
今回は~/dotfiles/lldb/debugo.pyというパスで置いたことにします。

command script import ~/dotfiles/lldb/debugo.py

使い方

上記のコマンドをdeoという名前で登録したので、breakしたタイミングでdeo (変数名)debugできます。
例えば、addButton.rx.tapdebugしたい場合はこんな感じです。

(lldb) deo addButton.rx.tap.asObservable()
2019-09-28 00:27:08.215: ::addButton.rx.tap.asObservable() -> subscribed
(lldb) c
2019-09-28 00:27:11.430: ::addButton.rx.tap.asObservable() -> Event next(())
2019-09-28 00:27:12.439: ::addButton.rx.tap.asObservable() -> Event next(())

例のごとく、Edit Breakpointで編集すれば、勝手に実行してくれます。

f:id:scior:20190928010121p:plain

解説

引数で指定した変数に対して、.debug().subscribe()を仕込んでいます。
disposeしないことによって、その場でsubscribeが切れるのを防いでいます。

この方法では普通にdebugするのと異なり、間に挟むということができません。
RxSwiftはメソッドチェーンで書いていくので、LLDBでは文の間で切ることができず、新たにsubscribeする方式をとっています。

したがって、hotとcoldの具合によっては都合が悪いかもしれません。

おまけ

command regexを使っても書けます。

command regex deo 's/(.+)/e %1.debug().subscribe()/'

DispatchSemaphoreで非同期処理の完了を待つ

Swiftにasync/awaitがなかなかこないので書いてみました。
コールバック地獄が嫌いな人向けの記事です。

DispatchSemaphoreの使い方

なんらかの大人の事情で、非同期処理を同期的なメソッドと同じような書き方で処理したい場合があると思います。 (もちろん良いとは言えないですが) 例えば、completion handlerで順番に処理したいことが多くなり、JSのcallback hellのように、ネストが深くなってしまう場合です。

そのような場合に、DispatchSemaphoreを活用することで、処理の記述を見やすくすることができます。

https://developer.apple.com/documentation/dispatch/dispatchsemaphoredeveloper.apple.com

実装は至ってシンプルで、DispatchSemaphoreは内部でカウントを持ち、waitでデクリメントをして、カウントが0以上になるまでブロックします。
signalでインクリメントをするので、処理が完了した際にsignalを呼ぶという使い方になります。

基本の構造

非同期処理を行い、完了後completionを呼ぶ形のメソッドに対しては、このような形で使うことで、semaphore.wait()の部分で処理をブロックすることができます。

let semaphore = DispatchSemaphore(value: 0)

// 非同期処理を行うメソッド.
doSomething(completion: {
    semaphore.signal()
})
// signalが呼ばれるまで待つ.
semaphore.wait()

ただし、メインスレッドで、非同期処理を実行するメソッドに対してこのような書き方をすると、そのままメインスレッドをブロックしてしまうので注意が必要です。

たとえば、

func doSomething(completion: @escaping (Data?) -> Void)) {
    // ...
    completion(data)
}

のようなメソッドに対しては、

func doSomethingAwait() -> Data? {
    let semaphore = DispatchSemaphore(value: 0)
    var result: Data?

    doSomething(completion: { data in
        result = data
        semaphore.signal()
    })
    semaphore.wait()

    return result
}

とすれば、同期的なメソッドと同じような書き方ができます。

これを、DispatchQueue.asyncなどを使ってサブスレッドで動かせば、複数のメソッドをネストすることなく記述することができます。

DispatchQueue.global(qos: .userInitiated).async {
    let dataA = doSomethingAAwait()
    let dataB = doSomethingBAwait(dataA)
    // ...
}

これを、うまくメソッドチェーンで書けるように工夫すれば、他言語のPromiseのようなインターフェースが実現できます。
OSSでもいくつか実装例があります。

github.com

タイムアウトの設定

DispatchSemaphorewaitにはタイムアウトを指定することができます。
タイムアウトを指定することで、後続の処理が結果待ちの状態で止まってしまうのを防ぐことができます。

switch semaphore.wait(timeout: .now() + 2.0) {
case .success:
    // 時間内に結果が帰ってきた時
case .timedOut:
    // 時間切れの時
}

この場合、返り値がDispatchTimeoutResultになるので、このenumを見ることで時間切れかどうか確認できます。

(システム時間を使う、wait(wallTimeout: DispatchWallTime)もあります)

git stashされているかをシェル(zsh)に表示する

どのブランチで最後にgit stashをしたかわからなくなることがあるので、シェルに表示されるようにしてみました。
これで誤popがだいぶ減りました。

How To

stashされた一覧はgit stash listで確認できます。

% git stash list
stash@{0}: WIP on feature/introduce-flux: 6ef95e4 Add flux classes
stash@{1}: WIP on feature/remake-view-models: 72482b5 Add ViewModelType
stash@{2}: WIP on feature/enable-auto-layout-for-color-selector: 31b69e9 Enable auto layout for color selector

このような感じで、ブランチ名も出ます。
これを元にして、現在のブランチと一致するものがあった場合に、最新のものをシェルに表示するようにします。

現在のブランチ名の取得

git rev-parseで取ってきます。

BRANCH="`git rev-parse --abbrev-ref HEAD 2>/dev/null`"

stashされているかの確認

先頭のを取って来てawkstash@{0}のようなインデックスだけ取ってきます。

STASHED="`git stash list | grep $BRANCH | head -1 | awk -F: '{print $1}'`"

git stash saveなどを活用してる人は、awkでメッセージまで抜き出すとハッピーかもしれません。

全体像

zshユーザーなのでRPROMPTにくっつけます。

BRANCH="`git rev-parse --abbrev-ref HEAD 2>/dev/null`"
if [ "$BRANCH" ]; then
  STASHED="`git stash list | grep $BRANCH | head -1 | awk -F: '{print $1}'`"
  if [ "$STASHED" a]; then
    RPROMPT=$STASHED@$RPROMPT
  fi
fi

するとこんな感じです。

f:id:scior:20190821201935p:plain

自分の.zshrcは変なとこに書いてしまっているのでいつか直したいです。いつか。

(そういえばmacはCatalinaからzshらしいです。)

support.apple.com