2013年12月29日 星期日

在 Ubuntu 下用 gdb debug CPython

用法和其它 library 稍微不同, 安裝 python-dbg 後, 是提供另一個含有 debug symbol 的 python 執行檔 python2.7-dbg, 而不是 gdb 自己另外載入 python 的 debug symbol。

可從 dpkg -L python-dbg 的結果得知這事:

$ dpkg -L python2.7-dbg | grep bin/python
/usr/lib/debug/usr/bin/python2.7-gdb.py
/usr/lib/debug/usr/bin/python2.7
/usr/bin/python2.7-dbg
/usr/bin/python2.7-dbg-config
/usr/lib/debug/usr/bin/python2.7-dbg-gdb.py
$ nm /usr/bin/python2.7
nm: /usr/bin/python2.7: no symbols
$ nm /usr/bin/python2.7-dbg | head
                 U ASN1_INTEGER_get@@OPENSSL_1.0.0
                 U ASN1_STRING_data@@OPENSSL_1.0.0
                 U ASN1_STRING_length@@OPENSSL_1.0.0
                 U ASN1_STRING_to_UTF8@@OPENSSL_1.0.0
                 U ASN1_TIME_print@@OPENSSL_1.0.0
                 U ASN1_item_d2i@@OPENSSL_1.0.0
00000000008857e0 d AST_type
0000000000967588 b Add_singleton
00000000009675e8 b Add_type
0000000000967560 b And_singleton

實際執行的結果:

$ gdb --args python2.7-dbg -c 'import time; time.sleep(10)'
GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...
Reading symbols from /usr/bin/python2.7-dbg...done.
(gdb) r
Starting program: /usr/bin/python2.7-dbg -c import\ time\;\ time.sleep\(10\)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
^C
Program received signal SIGINT, Interrupt.
0x00007ffff69ad003 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:82
82      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) bt
#0   __select_nocancel at ../sysdeps/unix/syscall-template.S:82
#1   floatsleep at ../Modules/timemodule.c:943
#2   time_sleep at ../Modules/timemodule.c:206
#3   PyCFunction_Call at ../Objects/methodobject.c:81
#4   call_function at ../Python/ceval.c:4021
#5   PyEval_EvalFrameEx at ../Python/ceval.c:2666
#6   PyEval_EvalCodeEx at ../Python/ceval.c:3253
#7   PyEval_EvalCode at ../Python/ceval.c:667
#8   run_mod at ../Python/pythonrun.c:1353
#9   PyRun_StringFlags at ../Python/pythonrun.c:1316
#10  PyRun_SimpleStringFlags at ../Python/pythonrun.c:969
#11  Py_Main at ../Modules/main.c:583
#12  main at ../Modules/python.c:23

順便備忘一下相關的文章: Debugging C/C++ and CPython using GDB 7′s new Python extension support 提到如何在 gdb 內使用 python extension 來 debug python code, 可以從 gdb 內看到 C 函式對回去 python script 的檔名和行數。

2013年12月25日 星期三

了解 2D Graphics Library 的基本知識

有很多套不同的 2D Graphics Library, 像是 Linux 下的 Cairo, OS X 和 iOS 下的 Quartz 2D 還有跨平台的 Skia, 但它們的概念應該差不多, 若看不懂其中一套, 可以參考別套的文件再回頭理解 (特別是 iOS 的文件最齊)。這篇記錄看了那些文件協助我了解 2D Graphics Library 的基本知識。

Cairo Tutorial

Cairo Tutorial 說明 Cairo 的架構, 配合圖示和簡短的範例程式, 相當實用。

Cairo 的架構分成: source, mask, destination, context。可以想像 destination 是一塊畫布 (pixel buffer), 可能對應到 bitmap、螢幕、PDF 或印表機。使用 Cairo API 設定好 source (如顏色、漸層模式 (gradien) 或另一個畫布) 和 mask, 再決定繪圖的動作, 就會將 source 經過 mask 的結果複製到 destination。

context 管理目前設定的各種屬性, 若有用過 OpenGL 或其它繪圖函式庫, 應該不會對 context 的概念感到陌生。繪製的動作會參考 context 內設定的屬性。

整體來說, 使用 2D Graphics Library 就是先產生一個 pixel buffer (Cairo 稱為 cairo_surface_t), 然後產生一個包含 pixel buffer 的 context (Cairo 稱為 cairo_t)。接著設定 context 屬性、source、mask, 然後決定繪圖動作 (如 fill、stroke)。

Path

Cairo Tutorial 有介紹 path 是什麼。Cocoa with Love: 5 ways to draw a 2D shape with a hole in CoreGraphics 提到如何用 path 畫出不同的填色效果。用同一個例子的不同作法說明為何需要 non-zero winding ruleeven-odd rule

Blending

參考 Porter/Duff Compositing and Blend Modes 了解各種 blend mode 的計算方式和圖例, 另外 CGContext reference 裡的 CGBlendMode 有同樣的說明。表中沒有 "source over", 因為 kCGBlendModeNormal 的效果就是 source over。

2013年12月21日 星期六

在 Chrome DevTools 修改 CSS 並同步回檔案裡

Chrome DevTools 最近的新功能, 參考 Chrome DevTools Revolutions 2013 - HTML5 Rocks, 作法如下:

  1. DevTools: settings -> workspace, 點選 add folder 加入開發用的目錄。允許 Chrome 讀寫該目錄下的檔案。
  2. 開啟網址 (本機或網路上的), 在 DevTools 的 source panel 找到要同步存檔的檔案, 按右鍵選 Map to Network Resource。
  3. 重新載入該頁。

注意:

  • 即使直接開啟 workspace 目錄內的檔案, 仍需要執行 "Map to Network Resource" 該步, 沒作這步讓我卡了一陣子 ...。
  • 只能同步存外部 CSS 檔, html 本體無法同步存檔, 仍得自己按 Ctrl+S。

成功後改網頁就快多啦, 在 html 內寫好骨架, 幫主要元件加上 class, 之後就在 DevTools 內選 tag, 直接在 class 內加 CSS rules, 調整參數。調整的當下就會存回檔案了, 真是超爽的啦。

備註: 依官方的說明, 也可以同步 Sass。

Mobile Safari 和 Chrome on Android 上的 1 pixel 間距

參考 CSS Tip: How to Prevent Div Seam Lines from Appearing in Apple's Mobile Safari | Oddo Design, 解法很簡單:

position: relative;
top: 1px;

似乎是 webkit 的 bug, 搜 "mobile safari 1 pixel gap" 可看到很多討論。

position: relative 真好用, 不只可以用來疊底, 加裝飾, 也可以 workaround 瀏覽器的 bug。

2013年12月15日 星期日

網頁顯示順序的規則: stacking context

瀏覽器在繪出網頁內容時, 並不是單純在 CSS 的 z-index 排序。而是依 stacking context。

有兩篇很棒的說明文章:

2013年12月1日 星期日

g++ 最佳化 switch-case 的一個小例子

最近看到一段程式如下:

bool isOkay(Type type) {
  switch (type) {
    case A:
    case C:
    case D:
    case E:
    case G:
    case I:
    case K:
    case L:
    case Y:
      return false;
    default:
      ;
  }
  return true;
}

原本想說 switch-case 和一串 if 沒差太多, 這樣寫應該比 set 慢。畢竟從演算法的角度來看, 前者是線式比對, 複雜度和 type 特例的數量成正比, 而 set 保證是 O(1)。

後來經 command 提醒, 想說搞不好 compiler 有做最佳化, 還是來看產生的組語好了。結果 compiler 真的有做最佳化, 從組語來看, switch 不見得會比較慢。觀察用的程式見這裡, 用 g++ -O2 產生的組語見這裡

結論是:

  • 寫成「一串 if」, 結果的確是一串 cmp 和 je, 複雜度如原本預期。
  • 但是 switch 讓 compiler 知道比較同一個變數, compiler 有機會做最佳化, 變成少數幾個計算和比較。
  • 使用 set 有額外呼叫函式的成本, 對於簡單計算的例子, 是不可忽略的額外成本。

使用 -O2 後, set 的版本有些複雜, 沒辦法藉由「大概看看+腦補推測」搞懂。實測的結果如下:

$ g++ -O2 benchmark_switch_cases_check.cpp -DSWITCH; time ./a.out;
1700000000

real    0m6.398s
user    0m6.396s
sys     0m0.000s
$ g++ -O2 benchmark_switch_cases_check.cpp -DALOTIF; time ./a.out
1700000000

real    0m8.392s
user    0m8.389s
sys     0m0.000s
$ g++ -O2 benchmark_switch_cases_check.cpp -DIF; time ./a.out
1700000000

real    0m8.402s
user    0m8.389s
sys     0m0.008s
$ g++ -O2 benchmark_switch_cases_check.cpp ; time ./a.out
1700000000

real    0m10.092s
user    0m10.089s
sys     0m0.000s

所花時間是竟然是 switch < if < set。

演算法分析可以去掉明顯的壞主意, 但是 compiler 做了太多事, 單就演算法分析不足以推斷出實務上的好壞, 果然還是需要實測來最佳化效能。

2013年11月19日 星期二

拆解使用純 CSS 做的碼表

難得看到一個簡單的純 CSS 卻含控制功能的網頁 Stopwatch in CSS, 分析了一下怎麼做到的, 才發覺 CSS 有許多神奇的語法:

顯示

1. 時分秒的內容一個由 00 ~ 59 共60個文字數字組成的長方形, 用 overflow:hidden + line height 只顯示第一列; 毫秒則是 00 ~ 99

2. 文字內容用 content 產生, 不用圖片的好處是可以用 CSS 做出更多微調 http://www.w3schools.com/cssref/pr_gen_content.asp

3. 設定 position: relative, 然後用 animation 控制 top offset。 各區塊用不同長度循環播放, 比方說秒是以 60s 為單位, 將60列文字用 60 步逐步往上捲出畫面 http://www.w3schools.com/css/css3_animations.asp

控制

1. 用 selector ":target" 改變 CSS 的屬性, 藉此控制 animation-play-state http://www.w3schools.com/cssref/sel_target.asp

#start:target 對到running, 其它 (包含預設) 對到 paused

在 CSS 中要做出點擊控制效果的關鍵可能就是 :target。由 URL 的 anchor 記錄狀態, 不同狀態提供不同 CSS 屬性, 就能切換不同畫面了。

2. stop 則是連結, 直接重新載入同一頁面

2013年11月18日 星期一

gdb "backtrace full" 不會停的可能原因

當程式有狀況的時候, 可以用下面的指令取得所有 thread 的 backtrace 含帶區域變數的資訊:

gdb --batch --quiet -ex 'thread apply all backtrace full' -p PID

但是以前遇過幾次下完指令後不會結束的狀況, 不知道確切原因為何。最近剛好遇到一個容易重製的情境, 總算找到一種可能的原因。答案是: 有區域變數使用 reference 指向 dangle pointer, 於是 gdb 讀不出區域變數的資訊, 變成輸出到這個 reference 的時候, 持續輸出一堆 "Cannot access memory ..." 而不會結束。

若區域變數是指標, gdb 只會輸出指標的值。但 reference 等同於一般變數 (不知「非指標非參考」的正式名稱是什麼), gdb 會輸出整個變數 (物件) 的值, 於是 reference 指向 dangle pointer 就成了糟糕的組合。

我遇到的情況 dangle pointer 剛好含有 vector, 可能因此找不到 vector 的結束點而不會停止。Btw, 原本想說試看看用 python gdb 看看能不能自己寫個 backtrace 不會卡住, 可惜用 python gdb 一樣會卡住, 沒有丟出 exception。

為什麼無法轉型 char ** 為 const char ** ?

參見 Why am I getting an error converting a Foo** --> Foo const**?, C++ FAQ。這裡摘錄文中附的例子。

class Foo {
public:
  void modify();  // make some modification to the this object
};

int main()
{
  const Foo x;
  Foo* p;
  Foo const** q = &p;  // q now points to p; this is (fortunately!) an error
  *q = &x;             // p now points to x
  p->modify();         // Ouch: modifies a const Foo!!
  ...
}

C/C++ 的轉型實在是一個我總是以為我搞清楚了, 但其實我從來沒搞清楚的東西 ...。想必過沒多久又會忘了原因吧。

2013年11月3日 星期日

LICEcap: 錄制桌機操作輸出 GIF 的工具

原本想找錄制 terminal 操作輸出成 GIF 檔的工具, 一直找不到。結果找到一個更完整更好用的工具: LICEcap, 可以錄 Windows 或 Mac OS X 的操作。安裝和使用都超簡單的。下載執行就會用了。

gj: 閱讀 C/C++ 原始碼的好幫手

兩年前剛開始接觸大型 C++ 專案時, 研究了一下尋找 symbol 相關位置的工具。最後依自己的需求, 做了一個小工具 gj。經歷了近兩年的使用和改進, gj 已成為我工作環境裡不可或缺的一塊了。

gj 的特點是:

  • 建立索引和找尋 symbol 的時間極短, 沾 ID Utils 的光, 不費任何力氣就有這樣的成果。
  • 盡可能不要漏掉可能的相關檔案, 並提供互動過濾的機制, 方便找出目標。
  • 和 vim 整合一起使用。

這裡有 gj demo 的畫面。

雖說目前已可應付我自己的九成需求, 還是有不少可改進的地方, 只是用到的情境較少就懶得補了。大家若有興趣使用, 歡迎提供建議。愈多人有需求, 愈有動力來改。或是用過 gj 後, 覺得有其它和 vim 整合得好工具, 也歡迎推薦。

2013年10月12日 星期六

縮小 VirtualBox VDI 格式占用硬碟的空間

先說作法, 再提冗長的碎碎念雜談。

作法

參考 How to shrink a dynamically-expanding guest virtualbox image | dantwining.co.uk 簡化的作法如下:

首先要在 Guest VM 將沒用到的磁區填為 0, 這樣 Host VM 才知道那些空間沒有被用到:

  • $ sudo aptitude install zerofree
  • $ sudo reboot
  • 開機時按下 left shift -> 選取 recovery mode -> 選取 Drop to root shell prompt
  • $ mount -n -o remount,ro -t ext3 /dev/sda1 /
  • $ zerofree -v /dev/sda1
  • $ shutdown -h now

奇怪的是, zerofree 填到 7x% 的時候, 整台電腦會當掉出現藍白畫面。

再來在 Host VM 壓縮 VM (VirtualBox 4.x 後支援):

  • $ VBoxManage modifyhd my.vdi –compact

結果出現 VBOX_E_FILE_ERROR, 參考 VirtualBox Solved VBOX_E_FILE_ERROR 0x80BB0004 » deltalounge 得知可能是 VDI 上面有壞軌。於是再參考 The How-To Geek Guide to Using Check Disk in Windows 7 or Vista 用 Windows 內建的圖型介面要求 Windows 7 在下次開機時使用 chkdsk 掃壞軌。這回明確找出 VDI 檔內有壞軌, 幸好沒有遺失資料。

解決壞軌問題後, zerofree 可以正常處理完整個 partition, VBoxManage modifyhd 也正常運作了。

碎碎念雜談

最近工作用的筆電三不五時就當一下, 有時是 VM 當掉 (Ubuntu), 有時是 Host OS 當掉 (Win 7), 直到某次重開機出現硬碟掃描, 掃完後說壞了八個 sector, 才確定應該是 SSD 快掛了, 只好來備份資料。

為了方便備份 Ubuntu VM, 想說買個 64GB 的隨身碟來用, 看了Tom's Hardware 針對 USB 3.0 隨身碟的評測, 想買超級威的 SanDisk Extreme (sequential write 220 MB/s), 不過價錢稍貴, 最後選了評測上沒有但是同牌的 SanDisk Ultra, 產品號稱讀取有 80 MB/s (*1)。

買好隨身碟後才發現我用的 VM 雖然沒有用到 64G, 但是 VDI 已長到 100G, 所以就照前面描述的流程瘦身、處理壞軌, 然後才能備份資料。掃完壞軌後電腦好像正常多了, 也懶得重灌, 先放著繼續一陣子, 順便了解一下 SSD 的壽命。話說用電腦用了十年多, 這是第一次在自己的工作用電腦上出現壞軌, 也是第一次大量使用 SSD ...。

Btw, 除了電腦偶而藍白一下外, 先前還有發覺 mercurial 回報 meta data 少了一些檔案, 無法進行操作, 用 find 找了一下, 發現有數個空檔案, 只好整個砍掉, 從別台電腦再複製一份過來, 壞軌真是可怕啊。

PS

  1. 實測的結果, 備份 50G 檔案的寫入速度是 10MB/s, 應該是比一般 USB 隨身碟快很多, 不過等了一小時半, 還是有點久。還沒測讀取的速度。

screen 和 vim 切換以及移動分頁的方法

在 screen 或 vim 內開太多分頁後, 常常會忘記先前在那個分頁做什麼, 雖然可以設快速鍵在分頁間切換, 還是不太方便。實驗一陣子後, 適度地重新排列分頁, 可以加快不少操作速度。備忘一下兩者的用法。

screen

$HOME/.screenrc 裡加上快速鍵 F7/F8 替代 ctrl+a p/ctrl+a n:

  • bindkey -k k7 prev
  • bindkey -k k8 next

另外, 移動分頁的方法是 ctrl+a :number N, 其中 N 表示要移往的位置。

vim

$HOME/.vimrc 裡加上快速鍵

  • nmap <leader>h gT
  • nmap <leader>l gt
  • nmap t <C-w>

前兩項對應到切換 vim tab page, 預設 leader key 是 \, 不過我習慣用 e, 比較好按。參考 vim :help mapleader 了解 leader key 更多訊息。

第三個方便在 vim window 間切換 (如 tl 表示切到右側 window), 可以少按 ctrl。

另外, 移動分頁的方法是 :tabm N, 其中 N 表示要移往的位置。

gdata-javascript-client 不能用了

今早使用 ego-post 時才發現, 原來 gdata-javascript-client 在 2012/04/20 就 deprecated 了, 不過到上上週以前還可以用。之後要改用 google-api-javascript-client - Google APIs Client Library for JavaScript, 需要先申請 Blogger API Key 才可以用, 剛才申請完後, 說五個工作天內會送認證信給我, 在那之前只好手動複製貼上到 Blogger 了。

在Windows 7 找出己安裝的 product key

若先前忘了備忘 product key, 重裝 Windows 的時候就麻煩了, 幸好經 Wens 提醒, 有軟體可以讀出 product key, 不見得適用所有軟體, 至少 可以找出Windows 7 和 Office 的樣子。

相關資料:

2013年9月26日 星期四

對 C++ 的觀感

寫了快兩年的 C++, 覺得 C++ 是學習成本高昂, 投資報酬率卻很低的語言。剛好看到 Less is exponentially more 有感, 在 G+ 寫了一些感想。提及經年累月的 C++ 經驗也會變為機會成本, 阻礙程式設計師轉向其它語言。不過在 Go 有龐大的 C++ library 和產品替代品以前, 也不容易選定用 Go 開發有效率需求的程式。實在是兩難啊。

下面取 G+ 留言的其中一段說明為什麼我覺得學 C++ 投資報酬率很低。

舉一個大家可能都會遇到的例子: 執行時多型。

在 Java 和 Python 裡, 說明執行時多型只要一句話就結束了, 就.....看那物件指到誰, 就會執行它的 method。

C++ 的話, 要明白兩件事:

  • 是否有使用指標呼叫? 等等, 還有參考的效果等同指標, 也要留意是否有用參考 ( 雖然很「理所當然」, 多了一個細節要留意 )
  • 呼叫的 method 是否有宣告 virtual? 如果你在目標類別的 method 沒看到 virtual, 記得還要順便檢查它的全部父類別是否有宣告該 method 為 virtual, 因為只要父類別有宣告 virtual, 子類別可以省略 virtual 的宣告

再來要明白 pure virtual 以及漏實作 virtual 函式造成的不易理解的連結錯誤訊息。到目前為止還只是基本語法, 還沒到實戰注意事項。

實戰注意事項包含:

  • 若類別有可能被繼承, 記得宣告 destructor 為 virtual
  • private virtual 的使用時機
  • 不能在 constructor 和 destructor 裡呼叫 virtual method

可能還有其它點, 目前有需要學到的就這幾點。明白為什麼後會覺得很「直覺」, 但是每當有必要用到時, 就得分神查一下這是在做什麼或是為什麼這樣可以運作。我明白 virtual 的出發點是 C++ 的核心精神 zero cost, 只是在看到這麼多衍生出來瑣碎的注意事項後, 我滿懷疑執行期的 zero cost 是否 >> 開發者的時間成本, 特別是獲得的好處和付出的代價是如此地不成比例。

2013年9月19日 星期四

是否能讓 C++ template 的標頭檔只含宣告不含實作?

參考《C++ Templates - The Complete Guide》 ch 6.1 ~ 6.3, 答案是: 可以。

過去一直覺得自訂 template 的時候, 將宣告和定義 (實作) 同時放在標頭檔裡最保險, 但不確定是否能將兩者拆開放到不同檔案。拆開的明顯好處是不會因為修改 template 的實作, 而需要重新編譯 include 此標頭檔的檔案。在經歷過改一行標頭檔要重新編譯十分鐘後, 我愈來愈在意這件事了。

在說明如何折開 template 的宣告和實作之前, 得先明白編譯使用到 template 程式碼過程發生了什麼事。實際上有兩個步驟需要留意:

  • 讀入 template 宣告, 檢查 caller 是否有正確使用目標函式、類別。
  • instantiate (實例化) 特定參數的 template

比方說定義 std::map<std::string, int> scores 的時候, 除了需要 map 的宣告了解 scores 是否有正確使用 map 的介面外, 還需要 map 的定義 (實作)才知道如何 instantiate std::map<std::string, int>。實例化時會檢查參數 std::string 和 int 是否支援 map 預期的介面。附帶一提, 《Effective C++》稱 template 為「隱式介面 + 編譯期多型」, 而 virtual 是「顯示介面 + 執行期多型」, 很精闢的描述。

回到原本的議題, template 標頭檔是否能只含宣告? 可以, 只要之後有辦法 instantiate 用到的 template 即可。假設 <map> 裡只有 std::map 的宣告, 就需要在某個 cpp 檔裡面 include map 的定義, 然後明確地告訴 compiler 你要 instantiate std::map<std::string, int>

以下以自訂函式說明:

t.h

#ifndef T_H
#define T_H

template <typename T>
void foo(T& t);

#endif //  T_H

t.cpp

#include "t.h"
#include <vector>

template <typename T>
void foo(T&t) { t[0] = 10; }

template void foo<std::vector<int> >(std::vector<int>& t); // *注意*

main.cpp

#include <iostream>
#include <vector>
#include "t.h"

int main(void) {
  std::vector<int> ns;
  ns.push_back(0);
  std::cout << ns[0] << std::endl;
  foo(ns);
  std::cout << ns[0] << std::endl;
  return 0;
}

編譯和執行:

$ g++ -c t.cpp
$ g++ -c main.cpp
$ g++ t.o main.o -o main
$ ./main
0
10

注意 t.cpp 的最後一行, 這行就是使用 explicit instantiation 的語法, 要求編譯器 instantiate 指定參數的 template。

反之, 若少了那一行:

$ g++ -c t.cpp
$ g++ -c main.cpp
$ g++ t.o main.o -o main
main.o: In function `main':
main.cpp:(.text+0x63): undefined reference to `void foo<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)'
collect2: ld returned 1 exit status

編譯各別 cpp 檔時沒有問題, 但是 link object 檔的時候會回報找不到實例化的函式 foo(), 因為編譯 t.cpp 時, 編譯器不知道需要實例化帶有什麼參數的 foo, t.o 裡也就沒有 main.cpp 用到的 foo 了。

雖然看似 explicit instantiation 可以幫忙拆離 template 的宣告和定義, 《C++ Templates - The Complete Guide》 卻不建議這麼做, 原因是專案變大後很可能會漏掉需要 instantiate 的型別。

說了這麼多, 最後又說建議不使用。那 explicit instantiation 到底有什麼用? 參考 Minimizing Code Bloat: Redundant Template Instantiation 得知, 若有某個型別的 template 很常被使用 (如 std::string), 可以用它來減少編譯和連結的時間。作法是用 explicit template instantiation declarations 避免程式碼 instantiate template, 然後在某個地方自己明確地使用一次 explicit instantiation。

以下以 <string> 為例, 尋找用到此技巧的相關資訊。

/usr/include/c++/4.6/string.h 會 include <bits/stringfwd.h> 和 <bits/basic_string.tcc>。前者宣告 string 如下:

typedef basic_string<char>    string;   /// A string of @c char

後者讓 include 此標頭檔的檔案不會 instantiate string:

extern template class basic_string<char>;

最後, 在 /usr/lib/debug/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16 裡可以找到 string 的實體 (應該是啦, 我沒有很仔細地比對):

$ nm /usr/lib/debug/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16 | g _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
000000000006b810 T _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
$ c++filt _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
std::basic_istream<char, std::char_traits<char> >& std::operator>><char, std::char_traits<char>, std::allocator<char> >(std::basic_istream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> >&)

或在 gcc-4.6.3/libstdc++-v3/src/string-inst.cc 中找到這段:

#include <string>

// Instantiation configuration.
#ifndef C
# define C char
#endif

namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION

  typedef basic_string<C> S;

  template class basic_string<C>;

2013年9月18日 星期三

python 快速開發: 使用 ipython 撰寫 python 程式

一但習慣某個東西的好處後, 久了就會忘了它的重要性。今天重操舊業寫了久違的 python, 覺得能用 python 寫程式已很爽了, 還能用 ipython 寫 python, 更是爽上加爽!特此記錄一下, 分享給還未試過的人。

ipython 是 python 的互動式 shell, 功能相當強大, 而且預設設定就相當好用。如今 ipython 已發展到令人難以想像的地步, 以下只是基本的使用情境。

試用別人的模組

$ ipython
Python 2.7.3 (default, Aug  1 2012, 05:14:39)
Type "copyright", "credits" or "license" for more information.

IPython 0.13.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import os

In [2]: os.pa<TAB>
os.pardir          os.pathconf        os.pathsep
os.path            os.pathconf_names

In [2]: os.path.join?
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Docstring:
Join two or more pathname components, inserting '/' as needed.
If any component is an absolute path, all previous path components
will be discarded.

In [3]: os.path.join('a', 'b')
Out[3]: 'a/b'

In [4]: os.path.join??
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Source:
def join(a, *p):
    """Join two or more pathname components, inserting '/' as needed.
    If any component is an absolute path, all previous path components
    will be discarded."""
    path = a
    for b in p:
        if b.startswith('/'):
            path = b
        elif path == '' or path.endswith('/'):
            path +=  b
        else:
            path += '/' + b
    return path

首先我先 import os, 第二行打了 os.pa<TAB> 看看有那些名稱開頭為 "pa" 的子物件或函式可用。也可以用 os.<TAB> 看 os 下的全部物件和函式。

接著找到 os.path.join, 在它後面加上 "?", 表示想知道 os.path.join 的 python doc。了解用法後就在第三行試試, ipython 預設會輸出 expression 回傳的結果 (而且是用 pretty-print 的方式呈現)。

最後第四行用 "??" 查詢 os.path.join 的實作, 有時沒有文件或看不懂文件的話, 直接看原始碼也滿方便的。另外讀別人程式發覺 import module 順序太複雜時, 也可以先用 ipython import 進來, 再用 "??" 看最後 import 到的是那一份實作。也可從裡面提到的檔名再用編輯器打開來看。

有了 <TAB>、? 和 ?? 後, 不太需要從外部找參考手冊, 直接在 ipython 裡試比較快。

開發片段小程式

試了一些片段小程式, 了解怎麼使用別人的模組後, 再來會想拼湊一些小程式。這時可用 edit 進入編輯器 (會選用環境變數 EDITOR 設的值):

In [1]: edit
IPython will make a temporary file named: /tmp/ipython_edit_bAOgl9.py
Editing... done. Executing edited code...
Out[1]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n'

In [2]: r = Rect(0, 0, 100, 50)

In [3]: r.w
Out[3]: 100

In [4]: r.area()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-83a71df0c49c> in <module>()
----> 1 r.area()

AttributeError: 'Rect' object has no attribute 'area'


In [5]: edit -p
IPython will make a temporary file named: /tmp/ipython_edit_stFwGi.py
Editing... done. Executing edited code...
Out[5]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n\n    def area(self):\n        return self.w * self.h\n'

In [6]: r2 = Rect(0, 0, 100, 50)

In [7]: r2.area()
Out[7]: 5000

在第一行按 edit 後, 畫面會切到編輯器, 寫好內容存檔離開後, ipython 會載入剛才打的內容到 ipython 裡, 也就是第一行輸出 'class Rect(object):\n def __init__ ...' 那一串。這裡我定義了 class Rect, 然後試一下 Rect 看看有沒有寫錯。接著第五行 edit -p 表示回去編輯上一次的內容, 執行後一樣會進入編輯器, 不過內容會先有上一次打過的東西。這回我多補了 area() 函式, 於是新增的 Rect 物件就有這函式了。

拼湊差不多後, 可用 edit -p 取得內容, 另外存到檔案, 或是打 hist 看打過的內容, 手動剪貼到檔案裡。

開發完整程式

想法明確時, 直接用編輯器編寫, 再輔以 ipython 測試更有效率。

首先, 終端機A的編輯器畫面如下所示, 寫了一個階乘函數:

# foo.py
def factorial(n):
    s = 1
    for i in range(n):
        s *= i
    return s

然後在終端機B的 ipython 測試:

In [1]: import foo

In [2]: foo.factorial(3)
Out[2]: 0

In [3]: reload(foo)
Out[3]: <module 'foo' from 'foo.py'>

In [4]: foo.factorial(3)
Out[4]: 6

第二行測完發覺結果不對, 於是回終端機A修改, 將 range(n) 改為 range(1, n + 1), 再回來終端機B執行 reload(foo) 重新載入 foo。接著第四行重測就得到正確的結果了。

當然, 發覺結果不對時, 也可以在 ipython 測測 range() 的用法, 或用 range? 看看說明, 很快就會找到答案。

結語

以上只是基本的 ipython 用途, 相信開發速度已可以比單用編輯器寫 python 快上兩倍以上。強烈建議有寫 python 的人一定要用看看 ipython。

2013年8月28日 星期三

C++ 的 C style cast 不等於 static_cast

即使看了幾次《Effective C++》item 27 "Minimize casting", 還是不明白 C style cast 有什麼重大問題。只覺得新的 cast 比較明確安全, 但寫起來很囉唆, 又是角括號又是括號 (愛惜手指, 請從少按 shift 做起)。一直以為 C style cast 等同於 static_cast, 這樣的話, 在使用 static_cast 的場合, 就偷懶寫成 C style cast 吧。

今天看到 Ken 寫的 On C-Style Cast in C++, 才發覺情況和我想得不同。於是寫個小程式驗證一下:

$ cat b.cpp -n
     1  class A {};
     2  class B : public A {};
     3  class C {};
     4
     5  int main(void) {
     6    A* a = new A();
     7    B* b = (B*)a;
     8    C* c = (C*)a;
     9    B* b2 = static_cast<B*>(a);
    10    C* c2 = static_cast<C*>(a);
    11    return 0;
    12  }
$ g++ b.cpp -o b
b.cpp: In function ‘int main()’:
b.cpp:10:28: error: invalid static_cast from type ‘A*’ to type ‘C*’

果真 C style cast 可以隨意轉換, static_cast 限制較為嚴格一些, 但是 pointer-to-base 仍可透過 static_cast 轉成 pointer-to-derived (即使轉型後會有問題)。若需要更安全的作法, 可以用 dynamic_cast, 看轉完的值是否為空指標, 可以知道是否能安全轉過去。代價是會增加執行時間, 詳細說明見《Effective C++》item 27 "Minimize casting"。關於轉型最重要的觀念, 大概就是盡量別轉型吧。

2013年8月27日 星期二

C++ 指標轉型後位置可能會改變

今天踩到一個和 C++ 指標轉型相關的 bug, 幸好以前看《深度探索 C++ 物件模型》時有個模糊的印象, 知道物件指標轉型後, 位置可能會變, 發現這問題後很快就想通了。

先寫個小程式實驗一下:

#include <iostream>

struct A { int x; };
struct B { int y; };
struct C : public A, B { int z; };

int main() {
  C *c = new C();
  A *a = c;
  B *b = c;
  void* v = c;
  std::cout << a << " " << b << " " << c << " " << v << std::endl;
  return 0;
}

範例輸出:

0xbd0010 0xbd0014 0xbd0010 0xbd0010

由此可得知 a、c、v 位置一樣, 但 b 不同

若用指標相等做邏輯判斷, 要留意轉型帶來的影響, 特別是中間有轉型成 void* 的時候, 可能傳了同樣的物件, 卻因型別不同造成誤判。

以下是一個示意的例子:

class Foo final
{
public:
  void Save(const Data& data, void* source);
  ...

private:
  ...
  FooCallback *m_callback;
}

void Foo::Save(const Data& data, void* source)
{
  ...
  if (m_callback && source != m_callback) {
    m_callback->OnSave(data);
  }
}

有不同的來源會呼叫 Foo::Save, 此時 Foo 會通知事先註冊的 callback, 但要避開 callback object 主動呼叫 Foo::Save 的情況。

乍看之下有檢查 source != m_callback 即可安心。但若 callback object (即有繼承 FooCallback 的類別的物件) 呼叫 Foo::Save 時這麼寫:

Foo *foo = new Foo();
...
foo->Save(data, this);

有可能因指標型別不同, 而得到不同的位置, 導致 source != m_callback 為真。

反之, 這樣的寫法才能安全過關:

foo->Save(data, (FooCallback*)this);

C++ 的世界真是處處驚奇啊!

2013年8月25日 星期日

以安裝 ccache 為例, 說明如何使用系統工具除錯

ccache 是什麼?

ccache 藉由暫存編譯過的 object 檔, 可以減少不必要的重新編譯時間。

用法很簡單:

# 安裝
$ sudo aptitude install ccache
# 啟用 ccache
$ export PATH="/usr/lib/ccache:$PATH"
$ gcc ...  # 這時會用到 /usr/lib/ccache/gcc

以我測試的例子來說, 從頭重新編譯一次是半小時左右, 裝了 ccache 後變成一分半。

ccache 官網所言, ccache 有可能重編譯不必要的程式, 但不會用到不對的暫存檔, 這也是使用這類工具時最重要的保證。

不過這篇的重點不在 ccache 的用法, 而是如何「知道如何安裝和使用它」。以前的文章或多或少有提到下文用的工具, 這篇比較有系統地用一個完整的例子使用它們。

已知

  • ccache 替換 gcc 等編譯工具, 檢查編譯條件和檔案沒變時, 直接取得暫存檔作為編譯結果。
  • 粗略地掃過 ccache manual, 得知是用 symbolic link 替換 gcc 為 ccache。

截至目前為止, 我們已得知 ccache 的核心運作原理, 接下來可以動手試看看。

安裝和使用過程

1. 首先確認是否能透過系統套件安裝 ccache:

$ aptitude search ccache
i   ccache                                                                              - Compiler cache for fast recompilation of C/C++ code
p   ccache:i386                                                                         - Compiler cache for fast recompilation of C/C++ code

2. 再來看套件訊息, 確定沒搞錯套件, 還有看看版號是否夠新:

$ aptitude show ccache
Package: ccache
State: installed
Automatically installed: no
Version: 3.1.6-1
...
Description: Compiler cache for fast recompilation of C/C++ code
 ccache is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. Supported languages are C, C++,
 Objective-C and Objective-C++.
Homepage: http://ccache.samba.org

對照官網版號 3.1.9, 3.1.6-1 還算 OK, 日後有必要再仔細看 release note 決定是否要抓原始檔重編。

3. 開始安裝:

$ sudo aptitude ccache

裝完後沒任何反應 ......, 依先前得知的訊息, 看看 gcc 有沒有被換掉:

$ which gcc
/usr/bin/gcc
$ ls -l /usr/bin/gcc
lrwxrwxrwx 1 root root 7 Nov 14  2012 /usr/bin/gcc -> gcc-4.6*
$ ls -l /usr/bin/gcc-4.6
-rwxr-xr-x 1 root root 353216 Apr 16  2012 /usr/bin/gcc-4.6*

結果 gcc 沒有被換掉 ......。

4. 腦袋卡住了一下下, 接著想到來確認 ccache 裝了什麼, 也許有些線索:

$ dpkg -L ccache
/.
/usr
/usr/share
...
/usr/share/man/man8/update-ccache-symlinks.8.gz
...
/usr/sbin/update-ccache-symlinks
/usr/bin
/usr/bin/ccache
/usr/lib
/usr/lib/ccache

裡面有個可疑的檔案 /usr/sbin/update-ccache-symlinks, 馬上來看看檔案內容。

結果是個 perl script, 從開頭得知重要的資訊:

  6 my $ccache_dir = "/usr/lib/ccache";
  7 my $old_gcc_dir = "/usr/lib/gcc";
  8 my $new_gcc_dir = "/usr/lib/x86_64-linux-gnu/gcc";
  9 my %old_symlinks; # Current compiler names in /usr/lib/ccache
 10 my %new_symlinks; # Compiler names that should be in /usr/lib/ccache
 11 my @standard_names = qw(cc c++);

對照後面的程式碼和這些變數名稱, /usr/lib/ccache 是關鍵的角色:

$ ls -l /usr/lib/ccache/
total 0
...
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++ -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++-4.6 -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc-4.6 -> ../../bin/ccache*
...

Bingo!! 將 /usr/lib/ccache 加到 PATH 前面, 應該會有效果。但是, 要如何快速驗證 ccache 確實有發揮效果呢? 我可不想重編一堆檔案, 過個半小時才發覺「好像沒效」。

依前面的假設, ccache 編譯時會從暫存區取出編譯過的檔案, 所以第一次編譯時, ccache 應該也會寫入編譯過的檔案到暫存區。幸好開檔的 system call 只有一個 open(2), 這時就該 strace 上場了:

$ cd ~/tmp/
$ export  PATH="/usr/lib/ccache:$PATH"
$ strace -eopen -f g++ t.cpp -c > log 2>&1

翻一下 log 會看到

 10 [pid 18133] open("/home/fcamel/.ccache/tmp/t.tmp.fc-vm.18132.ii", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4
 11 [pid 18133] open("/home/fcamel/.ccache/tmp/tmp.cpp_stderr.fc-vm.18132", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4

由此順便找到暫存目錄在 $HOME/.ccache 下。日後有需要也可以到這裡清清暫存檔。

有興趣的話可以用 strace 追更多 system call, 比對 open 開啟的 file descriptor (fd) 和後續 read、write 操作的 fd, 可以證實更多推測。到此就大功告成了!

小結

相信以 ccache 這樣成熟的套件, 仔細翻翻官網文件、man page 或 google 一下, 就可以找到答案。不過了解如何使用這些系統工具以後, 日後遇到文件不詳細或不易 google 的套件時, 有機會自行找出一線生機。

2013年8月24日 星期六

用 gdb 略過部份程式碼

尋找「誰是兇手」的時候, 以往的作法是註解掉部份程式、重新編譯、重新執行、觀察結果是否有變化。然後反覆前述動作直到找到兇手為止。編譯的時間略長的時候, 這個過程頗為痛苦。

幸好 gdb 有跳掉部份程式碼的方法。其中一個是用 return 結束目前所在的函式, 返回上一層 frame。另一個作法是先設中斷點, 再用 jump 跳到中斷點。

return 的小小缺點是: gdb 每次都會問你 yes/no, 用 jump 則無此困擾。可以用 define 寫個簡單的函式, 設暫存中斷點, 再用 jump 跳到中斷點, 藉此忽略中間的程式碼。

作法如下:

# skip the next N line statements
define j
  if $argc == 1 && $arg0 > 0
    tbreak +$arg0
    jump +$arg0
  end
end

之後就可以用 j N 跳掉接下來的 N 行程式。

2013年8月22日 星期四

有繼承的情況下, C++ method 存取到誰的 member field?

Effective C++ item 27 "盡量少做轉型" 提到下列轉型看起來像對的, 實際是錯的:

class SpecialWindow : public Window {
public:
  virtual void onReSize() {
    static_cast<Window>(*this).onResize();
    ...
  }
}

對這點感到很納悶, 於是寫個小程式實驗看看。結果發覺自己沒看清楚, 書上舉例是轉型 Window, 不是轉型 Window*, 所以 Window::onResize() 作用到新產生的物件身上, 結果不同於呼叫 Window::onResize()。

既然已經寫了小程式做實驗, 順便記在這裡供日後備忘。

要點如下:

  • 類別 C 的 method 會存取自己的 member field x, 若 C 本身沒有這個 member field, 會往父類別 A 找。
  • 承上, A 和 C 的 method 存取到的 x 是同一個 x, 也就是 A::x。
  • 若類別 B 宣告和父類別 A 同名稱的 member field x, 則類別 B 和父類別 A 各自擁有一份不同位置的 member field x。
  • 承上, A 的 method 會存取 A::x; B 的 method 存取 B::x。呼叫到誰的 method, 就知道改到誰的 x。

題外話, 允許子類別覆寫或重覆宣告同名 method 或 member field 是個糟糕的主意, 再加上可以轉型, 而且轉型後結果還會不一樣, 真是火上加油啊.......。了解 C++ 愈多, 愈覺得要從實作層面才能理解它的語法。

程式碼

範例輸出

$ ./a

b.set_x(3)
3 0
copy constructor A is called
3 0
3 30
3 30

c.set_x(3)
3 3
3 3
3 30
3 30

method address
&A::set_x     0x4006c8
&B::set_x     0x400710
&C::set_x     0x4007da

member field address
&b.x          0x7fffb4ef43a4
&((A*)&b)->x  0x7fffb4ef43a0
&c.x          0x7fffb4ef43b0
&((A*)&c)->x  0x7fffb4ef43b0

2013年8月21日 星期三

C++: 使用 private virtual 區分實作介面的 method

問題描述

Java 有語法 interface 明確定義 class 之間的接口, 但是 C++ 沒有, 只能透過「習以為常」的慣例表示, 也就是:

  • class I 宣告一組 public pure virtual function, 表示 I 是一個 "interface"
  • class A 希望實作 I, 於是透過繼承的方式實作 I
  • 需要用 I* 的 class, 取得實作 I 的物件 (也就是 A 的物件), 存成 I*

當 class A 需要實作多組介面, 或是自己也有一些 public method 供別人使用時, 看 class A 的宣告會不易找出那些是 A 的 public API, 比方說以下的例子:

class I
{
  public:
    virtual void foo() = 0;
    virtual void bar() = 0;
};

class A : public I
{
  public:
    void handle();
    virtual void process();

    // I's methods.
    virtual void foo();
    virtual void bar();
};

一眼看去有四個 method, 實際上可能只有 handle() 和 process() 才是使用 A 的人需要關心的。當 A 同時實作 (繼承) 多組介面就不易閱讀了。

解法

若宣告實作介面的 method 為 private, 像下面這樣:

class A : public I
{
  public:
    void handle();
    virtual void process();

  private:
    // I's methods.
    virtual void foo();
    virtual void bar();
};

有幾點好處:

  • 降低這些 method 可存取的範圍, 避免被誤用
  • 縮小閱讀程式的範圍
  • 間接暗示它們是作為 I 的介面使用, 若 A 自己的程式碼都沒用到它們的話, 會更明確地表明此點。特別適合用於 callback 的介面。

宣告 private virtual 的基本精神和宣告 member field 為 private 差不多: 不需要用到的, 就不要曝露出去, 易於日後維護。

相關閱讀

本篇討論的使用情境很窄, 專注於一點: 需要在 C++ 的世界裡表示出「Java interface」的語意時, 可使用「在介面 class 宣告 public pure virtual method + 在實作 class 宣告 private virtual method」。

Virtuality 提出關於 virtual 的四項要點:

  • Prefer to make interfaces nonvirtual, using Template Method.
  • Prefer to make virtual functions private.
  • Only if derived classes need to invoke the base implementation of a virtual function, make the virtual function protected.
  • A base class destructor should be either public and virtual, or protected and nonvirtual.

涵蓋關於 private virtual 的更多應用情境, 滿不錯的參考資料。

2013年8月19日 星期一

用 doxygen 產生 class hierarchy diagram

最近需要讀比較複雜 C++ 程式, 常常看到一堆 class 有個祖宗十八代, 要找出眾多子代的 class 或是某個 method 到底是那一代祖先實作的, 有些麻煩。

原本想自己玩看看 clang, 用 clang 分析原始碼產生 graph, 再套 graphviz 畫出來。轉念一想, 這麼常見的需求, 應該有人做好了。況且, 自己處理單檔可能不會麻煩, 但若多個檔案需要不同 include path (-I) 時, 到也有些頭痛。

上網查一下, 發覺 stackoverflow 有人推薦用 doxygen, 一試就靈, 最棒的是 doxygen 不需另外讀編譯 C/C++ 的設定檔, 它直接讀取指定的目錄。先跑 doxygen -g myconfig 產生設定檔 myconfig, 再修改 myconfig:

  • INPUT
  • RECURSIVE = YES

最後執行 doxygen myconfig 即可。

產生的網頁裡可看到各個 class 的祖譜, 非常方便。我猜 doxygen 可能是直接做文字分析, 不是用 compiler 下手。

實測的結果也的確有問題, Ubuntu 12.04 的 doxygen 有點舊, class 有用到 C++11 的 keyword final 時, final 反而會被當成 class name。幸好在 doxygen 1.8.2 版已解決, 先裝 doxygen 舊版, 再自己抓原始碼編譯, 用 checkinstall 封裝成 package 即可解掉這問題

checkinstall 封裝的好處是, 可從 dpkg -L PKG_NAME 看裝的內容, 日後有問題也可用 dpkg -r PKG_NAME 刪除。

Btw, doxygen 參數一堆, 有閒再來研究看看有什麼好用的東西。

2013-09-10 更新

若遇到 macro 造成 doxygen 分析錯誤, 可試著要求 doxygen 展開特定的 macro。作法是修改下列的設定:

  • ENABLE_PREPROCESSING = YES
  • MACRO_EXPANSION = YES
  • EXPAND_ONLY_PREDEF = YES
  • PREDEFINED = ABC=xyz
  • EXPAND_AS_DEFINED = ABC

其中 EXPAND_AS_DEFINED 填入希望展開的 macro。若執行後沒有展開成功, 可以設 PREDEFINED 告知 doxygen 如何展開目標 macro。見 Doxygen Manual: Preprocessing 了解和其它前處理相關的設定。

2013年8月15日 星期四

用 python gdb 客製化 backtrace 的結果 (2)

之前寫的指令 bt 加上參數 -s, 使用 -s 時, 會一併記錄 backtrace 每個 frame 附近的原始碼。

以下是一個範例輸出:

(gdb) bt -s
#0   A::hello at b.cpp:8
  | class A
  | {
  | public:
  |   void hello(int n)
  |   {
->|     std::cout << n << std::endl;
  |   }
  |
  |   void foo(int n)
  |   {

#1   A::bar at b.cpp:18
  |     bar(n + 1);
  |   }
  |
  |   void bar(int n)
  |   {
->|     hello(n + 10);
  |   }
  | };
  |
  | int main(void) {

#2   A::foo at b.cpp:13
  |     std::cout << n << std::endl;
  |   }
  |
  |   void foo(int n)
  |   {
->|     bar(n + 1);
  |   }
  |
  |   void bar(int n)
  |   {

#3   main at b.cpp:24
  |   }
  | };
  |
  | int main(void) {
  |   A a;
->|   a.foo(5);
  |   return 0;
  | }

修改後的 python script 如下:

作法大致和 《用 python gdb 客製化 backtrace 的結果》一樣, 只是改取檔案的 fullname, 再自己讀出相關的原始碼。這部份用 python 實作, 相當地輕鬆寫意。

2013年8月10日 星期六

用 macro 組合變動式參數, 強化 log 函式

程式除錯時常需要加些 log 印出變數內容, 通常會希望附帶印出所在的函式。每次要自己重打一次函式名稱太麻煩了, 可以利用 C 的 macro 避免重覆的動作。

先來看個範例程式:

以及輸出結果:

$ g++ a.cpp -o a; ./a
result: 3
$ g++ -DDEBUG_PRINT a.cpp -o a; ./a
int Calculator::add(int, int) this=0x7ffff03c1d7f a = 2, b = 1
result: 3

有 #define DEBUG_PRINT 的才會真的執行 DebugPrintf, 除了兼顧原本 printf 有的變動式參數外, 順便自動補上函式名稱和目前物件的指標; 反之則完全不會執行到, 不會增加額外負擔。

有需要的話, 可以稍微修改 DebugPrintf, 如同 《trace C/C++ function call 的方法》 記錄檔名和行數。

關於巨集這行:

#define DebugPrintf(format, args...) fprintf(stderr, "%s this=%p " format, __PRETTY_FUNCTION__, this, ##args)

用到的語法包含:

  • C 的 "abc" "def" 會組成 "abcdef"
  • args... 比 __VA_ARGS__ 易於閱讀
  • 只有一個參數的時候, __VA_ARGS__ 會自動移掉前一個 ","

可以參照 Variadic Macros - The C Preprocessor 了解細節。

2013年8月3日 星期六

用 python gdb 客製化 backtrace 的結果

需求

想要了解模組之間函式呼叫的關係時, 與其一層層比對多個類別之間的呼叫關係, 不如直接在最後一個呼叫函式放中斷點, 直接顯示 backtrace。但是當函式裡有太多參數或 template 時, backtrace 的 frame 訊息會變得很長, 不易閱讀。我的目的只是找出呼叫的函式名稱、檔名和行數, 函式帶的參數反而是困擾。

作法一: 用 gdb.execute()

一個簡單的作法是截取 gdb 的輸出, 然後解析文字去掉不要的部份:

Btw, 上面的作法還順便幫行首的標號上色。

但是, 使用 cgdb 時會無法運作, 理由是 cgdb 使用 GDB MI, gdb.execute('backtrace') 的結果不是原本看到的格式, 難以解析

作法二: 用 gdb.Frame() API

只好改用中規中矩的方式逐一讀取 frame, 取出需要的資訊:

將上面的 script 存到 /path/to/gdb/scripts/backtrace.py, 接著在 $HOME/.gdbinit 裡加入以下設定:

python
sys.path.insert(0, '/path/to/gdb/scripts')
import backtrace
end

之後就能用 bt 顯示精簡後的 backtrace 了, 也方便手動複製貼上到筆記裡。以下是一個輸出例子:

(gdb) bt
# 0  A::hello at a.cpp:8
# 1  A::bar at a.cpp:13
# 2  A::foo at a.cpp:18
# 3  main at a.cpp:25

Btw, 若是需求比較簡單, 可以試看看 Print Settings, 有些選項可以改變 backtrace 顯示的訊息。

參考資料:

2013年7月27日 星期六

vim 快速開啟目錄下的檔案

用過 XCode 的 "shift+command+o" 快速開啟檔案後, 一直很想在 vim 內用同樣的功能。按個快速鍵會跳出一個輸入框, 接著只要打部份字串, 就會濾出有可能的檔案。

看到 DKcmd 推薦用 ctrlp, 就來試用一下。試用後感覺相當不錯, 用法如下:

  1. 在 command mode 打 ctrl+p
  2. 打檔名
  3. 選定檔案, 按 ctrl+t 開在新分頁; 按 ctrl+v 開在目前分頁的新 window; 按 enter 開在目前分頁

ctrlp 除介面好用外, 另一大優點是使用 pure vim script, 理論上有 vim 的平台都能用 ctrlp。不過最近用一陣子後發覺一些小問題, 微調之後才會更好用。

在 $HOME/.vimrc 中加上這幾行:

  • let g:ctrlp_clear_cache_on_exit = 0 " 離開 vim 後不要清 cache
  • let g:ctrlp_max_files = 1000000 " 加大 cache 索引的檔案數, 否則會漏找檔案
  • let g:ctrlp_user_command = 'find %s -type f' " 使用 find 加速建索引的速度

備註: 無用的實驗觀察

之前沒讀完官網, 漏看了參數 g:ctrlp_user_command, 只好自己手動建索引加速, 下面的心得就當紀念吧。

另外 cache 預設存放在 $HOME/.cache/ctrlp 下, 若嫌 ctrlp 建 cache 太慢, 可以自己手動建:

$ cd /path/to/target
$ index=$(echo `pwd` | sed 's#/#%#g' | sed 's#$#.txt#')
$ find . | cut -b 3- | LC_ALL=C sort > $HOME/.cache/ctrlp/$index

自己建會快超多, 不過會比較不乾淨一點, ctrlp 似乎有濾掉不能開的檔案, 像是目錄或圖檔之類的。

2013年7月20日 星期六

gdb 顯示 STL container 的方法

問題描述

如下列的程式碼:

std::vector<int> numbers;
for (int i = 0; i < 4; i++) {
  numbers.push_back(i * 3);
}

std::map<std::string, std::string> contacts;
contacts["john"] = "0987-654321";
contacts["mary"] = "";

在 gdb 中不易閱讀 numbers 和 contacts 的顯示結果:

(gdb) p numbers
$1 = {<std::_Vector_base<int, std::allocator<int> >> = {_M_impl = {<std::allocator<int>> = {<__gnu_cxx::new_allocator<int>> = {<No data fields>}, <No data fields>}, _M_start = 0x6060
10, _M_finish = 0x606020, _M_end_of_storage = 0x606020}}, <No data fields>}
(gdb) p contacts
$2 = {_M_t = {_M_impl = {<std::allocator<std::_Rb_tree_node<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::basic_string<char, std::char_
traits<char>, std::allocator<char> > > > >> = {<__gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<std::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std
::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >> = {<No data fields>}, <No data fields>}, _M_key_compare = {<std::binary_function<std::basic_string<char, std
::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool>> = {<No data fields>}, <No data fields>}, _M_header = {_M_co
lor = std::_S_red, _M_parent = 0x606080, _M_left = 0x606080, _M_right = 0x606120}, _M_node_count = 2}}}

解法一: 土法煉鋼

參考這篇的作法, 先取得目標位址再取值:

(gdb) p numbers
$5 = {<std::_Vector_base<int, std::allocator<int> >> = {_M_impl = {<std::allocator<int>> = {<__gnu_cxx::new_allocator<int>> = {<No data fields>}, <No data fields>}, _M_start = 0x60601
0, _M_finish = 0x606020, _M_end_of_storage = 0x606020}}, <No data fields>}
(gdb) ptype numbers._M_impl
type = struct std::_Vector_base<int, std::allocator<int> >::_Vector_impl
    : public std::allocator<int> {
    std::allocator<int>::pointer _M_start;
    std::allocator<int>::pointer _M_finish;
    std::allocator<int>::pointer _M_end_of_storage;
  public:
    void _Vector_impl(void);
    void _Vector_impl(
    const std::_Vector_base<int, std::allocator<int> >::_Vector_impl::_Tp_alloc_type &);
}
(gdb) ptype numbers._M_impl._M_start
type = int *
(gdb) p numbers._M_impl._M_start[1]
$7 = 3
(gdb) p *(numbers._M_impl._M_start)@numbers.size()
$12 = {0, 3, 6, 9}

先用 ptype 逐步了解各欄位的型別, 繼而找到儲存資料的欄位 numbers._M_impl._M_start。 由於 _M_start 是 int*, 倒數第二個指令直接取出 numbers[1] 的值, 最後一個指令則是用 print P@N 印出從位置 P 開始 N 筆資料。

嫌麻煩的話, 可以定義新的 gdb command:

(gdb) define pv
Type commands for definition of "pv".
End with a line saying just "end".
>if $argc == 1
 >p *($arg0._M_impl._M_start)@$arg0.size()
 >end
>if $argc == 2
 >p $arg0._M_impl._M_start[$arg1]
 >end
>end
(gdb) pv numbers
$21 = {0, 3, 6, 9}
(gdb) pv numbers 1
$22 = 3

將上述的 define pv ... end 寫入 /.gdbinit, 之後就不用重寫一次。

解法二: 使用別人寫好的 pretty-printers

土法煉鋼的目的是讓我們有辦法應付日後各種需求, 但是針對 STL 這種大家都有的需求, 已經有善心人士提供完整的套件了, 見 STLSupport - GDB Wiki 的第一點。

摘要作法如下:

$ cd /path/to/gdb_script/
$ svn co svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python
$ cat <<EOF >> ~/.gdbinit
python
import sys
sys.path.insert(0, '/path/to/gdb_script/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end
EOF

載入 pretty-printers 後, 輸出變數的結果如下:

(gdb) p numbers
$1 = std::vector of length 4, capacity 4 = {0, 3, 6, 9}
(gdb) p contacts
$2 = std::map with 2 elements = {["john"] = "0987-654321", ["mary"] = ""}

有需要的時候, 也可以參考 python/libstdcxx/v6/printers.py 學習如何用 python 寫 gdb script

trace C/C++ function call 的方法

目的

方法一: 使用 gcc 參數 -finstrument-functions

參考資料:

優點:

  • 自動含蓋所有函式。

缺點:

  • 可能增加太多執行負擔, 雖然可以配合 -finstrument-functions-exclude-file-list 去除不想觀察的函式, 對於大專案來說有些不方便。

方法二: 自己寫 logger

使用 C/C++ 或 gcc 提供的語法, 自己寫 logger 沒有想像中的麻煩:

  • 可用 gcc 的 __PRETTY_FUNCTION__ 或標準的 __func__ 取得函式名稱
  • __LINE__ 取得行數
  • __FILE__ 取得檔名

於是可定個巨集:

#define LOG_IT() MyFunctionLogger logger(__FILE__, __LINE__, __PRETTY_FUNCTION__)

然後用 vim 的全域取代:

:% s/^{/&\r  LOG_IT();\r\r/c

這會逐個詢問是否取代下列字串:

void foo()
{
  // blah blah

void foo()
{
  LOG_IT();

  // blah blah

Btw, 由此可知, 函式的左大括號放在行首比放在行尾來得方便。

MyFunctionLogger 只是個 wrapper, 目的是利用 RAII 記錄進入和離開函式, 這樣即使函式中間有任何 return, 都不會漏記離開函式:

class MyFunctionLogger
{
public:
  MyFunctionLogger(const char *file, int line, const char *func)
      : m_file(file), m_line(line), m_func(func)
  {
    MyLogger::Instance()->EnterFunctionCall(m_file, m_line, m_func);
  }

  ~MyFunctionLogger()
  {
    MyLogger::Instance()->LeaveFunctionCall(m_file, m_line, m_func);
  }

private:
  const char *m_file;
  int m_line;
  const char *m_func;
};

MyLogger 可以做的事很多, 比方說:

  • 用 file 做為 tag 分類輸出。
  • 在 EnterFunctionCall() 和 LeaveFunctionCall() 記錄層級, 輸出 function call 時, 視 function call 的深度做對應的縮排。
  • 在 EnterFunctionCall() 和 LeaveFunctionCall() 記錄目前時間, 可在 LeaveFunctionCall() 時計算執行時間, 協助 profiling。

若 MyLogger 會被多個 thread 使用, 別忘了將 MyLogger 寫成 thread-safe, 不過以觀察或除錯的角度來說, 偷懶不作應該也OK吧。

若目標模組是純 C 的程式, 可用 gcc 的 cleanup attribute 做出 RAII 的效果, 達到和 MyFunctionLogger 一樣的效果。

Linux 寫 code 確認某個 process 仍活著

shell script 直接用 ps PID | grep ... 檢查, 無須多說。寫 C/C++ 程式的話, 基於同樣的想法, 可以用 stat("/proc/PID/stat") 檢查檔案是否存在, 但這作法有一些不確定性, 有可能目標 process 掛了, 之後有新的 process 用了同樣的 pid, 結果誤判目標還活著。

若自己有權限送 signal 給目標 process 的話 (如 child 或 parent process), 可以用 kill 來檢查:

bool IsAlive(pid_t pid)
{
    return !(kill(pid, SIGCONT) == -1 && errno == ESRCH);
}

送個無關緊要的 signal 給目標 process (若它真的有機會進入 STOP 狀態的話, 得挑別的 signal), 再檢查錯誤值判斷目標是否還活著。覺得用 kill 檢查的小技巧還滿有趣的, 備忘一下。

2014-03-28 更新

實際用了以後發現一個不方便的副作用。使用 gdb 連上 process 後, gdb 會在 process 收到 signal 時中斷。若採用送 signal 定期偵測 process 是否活著的話, gdb 會三不五時地中斷, 不方便除錯。

還是用 stat("/proc/PID/stat") 比較方便一些。或是觀察的 process 是 child process 的話, 可用 waitpid + WNOHANG 判斷。

2013年7月19日 星期五

在 iOS 刪除 App 後不會刪除 keychain 內的資料

不過理論上 App 不能存取別其它 App 的 keychain 資料, 所以這勉強不算安全漏洞吧。若希望使用者重新安裝 App 時, 不會用到之前存在 keychain 的資料, 可在安裝後第一次啟動時先刪除 keychain 資料:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //Clear keychain on first run in case of reinstallation
    if (![[NSUserDefaults standardUserDefaults] objectForKey:@"FirstRun"]) {
        // Delete values from keychain here
        NSArray *secItemClasses = @[(__bridge id)kSecClassGenericPassword,
                               (__bridge id)kSecClassInternetPassword,
                               (__bridge id)kSecClassCertificate,
                               (__bridge id)kSecClassKey,
                               (__bridge id)kSecClassIdentity];
        for (id secItemClass in secItemClasses) {
            NSDictionary *spec = @{(__bridge id)kSecClass: (__bridge id)secItemClass};
            SecItemDelete((__bridge CFDictionaryRef)spec);
        }
        [[NSUserDefaults standardUserDefaults] setValue:@"1strun" forKey:@"FirstRun"];
    }

    //...Other stuff that usually happens in didFinishLaunching
}

參考資料:

2013年7月18日 星期四

VirtualBox 使用 Bridged Adapter 的方法

打從開始用 VirtualBox 到現在, 一直沒有成功地用過 Bridged Adapter, 所幸 cmd 很自然地使用 Bridged Adapter, 比對一下成功的設置方式, 總算明白我搞錯的地方。

Bridged Adapter 的設定幾乎和 NAT 一樣簡單, 在 guest OS 關機狀態下, 從 VM 的 Settings -> Network -> Attached to 選擇 Bridged Adapter 即可。若 host OS 是透過 DHCP 取得上網 IP (比方說家裡有裝 IP 分享器), 那 guest OS 無須設定, 開機後也會從 DHCP 取得 IP 上網。

若 host OS 是用 PPPoE 連上網路, guest OS 也需要設定 PPPoE 才能連上網。可以想成 guest OS 和 host OS 是連上同一個 LAN 的另一台機器。Btw, 可以參考這裡了解 Ubuntu 設定 PPPoE 的方法, 作法也很簡單。

shell script: 用 eval 執行內含單引號的變數

某些情況下需要在指令裡加入單引號, 但是會有問題。下面是一個簡化後沒意義的例子:

cmd="ls '/etc'"
echo "> Run $cmd"
$cmd

執行結果如下:

> Run ls '/etc'
ls: cannot access '/etc': No such file or directory

但是實際在 bash 上打入 ls '/etc' 卻沒問題。從這裡得知, 解法是用 eval 執行:

cmd="ls '/etc'"
echo "> Run $cmd"
eval $cmd

另外記得除錯時可以配合 set -x 顯示 bash 實際執行的指令, 或是用 bash -x 執行 script。

加速 gdb 載入 symbol 時間

gdb 7.4 以後支援 index, 可以加速載入 gdb symbol 的時間, 參考 Index Files - Debugging with GDB 的說明, 寫了個 script 接受一個 binary 檔, 會作以下的事:

  1. 用 gdb 載入 binary 的 symbol, 並存下 symbol index 到暫存檔
  2. 加入 symbol index 到原 binary

symbol 太多時, 用 gdb index 可大幅減少載入時間, 不過每次重編又要重新產生 index 檔, 不知有沒有更好的方法, 可以在 linking 時直接在 binary 內加入 symbol index, 並且不會增加太多 linking 時間。

2013-07-19 更新

Viller Hsiao 留言裡的建議, 試了 gold 的 --gdb-index。這功能還滿新的, Ubuntu 12.04 的 gold 沒這選項, 不過 12.10 的 gold 有。可以用 objdump -h BINARY | grep index 確認 link 時加上 --gdb-index 確實有效。

2013-07-23 更新

用 gold 編譯加上 --gdb-index 後踩到雷, 在目前的專案裡 100% 會在執行 backtrace 後卡住不動, 接著一直狂吃記憶體, 漲到 4G 以後顯示gdb virtual memory exhausted can't allocate 4064 bytes, 然後和使用者說 gdb 掛了, 你願意結束這個 debug session 嗎?

交叉比對了下述三種情況:

  • 用 gold link (不含 --gdb-index)
  • 用 gold link (不含 --gdb-index), 事後補 gdb index
  • 用 gold link + --gdb-index
結果只有第三種會中獎, 所以現在改回用第二種方法了。

2013年7月17日 星期三

Objective-C 的 scope 以及 property

今天要用又忘了, 寫下來備忘一下。

scope

instance variable 有 scope, method 似乎沒有 scope。畢竟是 dynamic typing, method 加上 scope 也滿奇怪的。

instance variable 的 scope:

  • 分為 @public, @protected, @private 和 @package (看不懂 @package, 要用到再研究, 我猜是指 scope 在「函式庫」內)
  • @interface 預設 scope 是 @protected
  • @implementation 預設 scope 是 @private

property

宣告 @property Klass myObject 後, 可用 @synthesize 讓 compiler 產生基本的 getter 和 setter 實作。

或是自己在 @implementation 內實作

-(void) setMyObject:(Klass*)object
{
}

-(Klass*) myObject
{
  return ...;
}

個人不太喜歡 @property 的設計, 要找 property 相關存取程式時, 有點不方便。以 @property myObject 來說, 要先確定是否有用 synthesize, 若有的話, 要同時看 myObject、setMyObject 以及 synthesize 的 instance variable (如 _myObject), 才能確定沒有其它存取到這個屬性的地方。

MacPorts 基本操作

之前問朋友在 Mac OS X 下用那套工具, 結果強者們清一色地回答用 MacPorts, 不管它好不好用, 總之強者們意見如此一致, 跟著試就是了。

最近用一陣子後覺得還算順手, 記得依這裡的說明設 bash-completion, 關鍵的一步是要用 macports 裡的新版 bash 才能搭上 bash-completion, 但是 iTerm2 預設用系統的 bash, 要改一下 iTerm2 的偏好, 改用 macports 裝的 bash 才行。

目前只到下列幾個指令, man ports 裡都有:

  • port search PATTERN
  • port install PKG
  • port info PKG
  • port content PKG

美中不足的是, macports 沒有像 apt-file 這種從檔名反查套件的功能。不然, 有考慮日後改用 Macbook Pro 開發, 可兼顧日常使用應用程式, 以及用 terminal 開發的需求。

2013年7月15日 星期一

Optimizing the Critical Rendering Path 閱讀筆記

Optimizing the Critical Rendering Path - Velocity SC 2013 這篇滿有趣的, 看完順便筆記一下。

問題描述

  • 使用者等超過一秒就會不耐煩
  • 3G / 4G 網路 latency 很高
  • 以 3G 的情況來說, 假設 RTT 是 200ms, 去掉必須的 DNS lookup、TCP handshake、載入 html 檔 (用 http), 就只剩下 400ms 可以 render 網頁。
  • https 需要額外 1 ~ 2 個 RTT, 情況更糟
  • 載入網頁時, 雖然 html 可以邊載入邊顯示, 但是需要載入外部 CSS 檔時會延後顯示網頁。CSS 需要載入完整個檔案才可使用, 無法邊載入邊用
  • 外部 javascript 檔也會阻擋整個網頁顯示

( ps. 網頁加速的基本知識可以參閱 Best Practices for Speeding Up Your Web Site, 還有用 Chrome Developer Tool 內建的功能檢查 )

解法

  • 要下參數避免 TCP slow start, 降低 RTT
  • 一個 roundtrip 大概可下載 14KB 資料 (應該是以 MTU 1500 Bytes 來估吧), 盡量讓第一份 html 只含 14KB 以下的資料, 之後再補載入剩下內容
  • 避免在第一次顯示時用到外部 CSS 檔, 有 Google 有提供模組讓 apache2 或 nginx "inline CSS"
  • 避免在第一次顯示時用到外部 JS 檔

2013年7月14日 星期日

使用 iOS notification 的好處

先前在《iOS delegation vs. notification》提到我偏好 delegation, 這篇稍微修正一下對兩者的觀點。

開發 iOS 和 Android 以前我只有寫 Web 的經驗, 沒有寫圖形應用程式的經驗 (硬漢工程師就是要用 CLI 啊!!)。之前只覺得開發 Web 很容易上手, 不過跨瀏覽器很煩。最近多了一些 iOS 開發經驗, 才體會到為什麼 Web 這麼好寫, 個人認為的原因有二:

換句話說, 寫網頁可以輕鬆得知 model 狀態變化, 得知變化時還可以輕鬆地改變任何相關的 UI 元件。

反觀寫圖形應用程式時, 離上述兩點相當地遙遠, 寫起來綁手綁腳的。或許是出於類似的需求, iOS 有提供 NSNotificationCenter 協助 observe 和 dispatch event。

於是, 透過 NSNotificationCenter 這個全域變數, 我們可以

如此一來, 減輕不少實作負擔。不過如同 When to use Delegation, Notification, or Observation in iOS 所言, notification 過於彈性會造成一些問題。個人認為配合一些規範, 像是使用 header 檔定義 notification name 和 user info 的 key, 不要直接用 string, 應該是不錯的選擇。

備註:

GUI model 的介面設計心得

最近寫了稍微複雜一點的 iOS 介面, 多了一些心得。先前已寫過《非同步程式心得》包含 GUI 和 IO 的體會, 這篇則著重在 model 介面的設計。

如同《從需求出發理解背後技術的思考脈胳》的體會, 思考技術議題時不仿從需求切入, 比較容易掌握到必要項目, 需要在設計上做取捨時也有個依據。

先來看兩個生活中的例子:

  • 電梯外側和內側有螢幕顯示電梯目前所在的樓層。
  • 紅綠燈的秒數倒數。

這兩個例子裡, 不會因為多了螢幕顯示樓層或秒數而影響結果發生的時間, 但是會讓使用者明白現在發生什麼事, 比較不會失去耐心。相信體驗過沒提供樓層的電梯或沒秒數的紅綠燈的人, 應該更能體會沒有告知狀態變化的感覺。

回到程式開發的世界裡, model 時常會有非同步的操作, 比方說寫一個線上文書編輯器, 介面可以是 Web 或是 App UI, 但資料存在線上伺服器裡。

存檔的必要介面如下:

void Document::Save();
void DocumentDelegate::OnSaveResult(bool ok);

caller 產生一個 document, 將自己傳入作為 DocumentDelegate。呼叫 Save() 之後, 會觸發 callback OnSaveResult() 告知存檔是否成功。

乍看之下這樣的介面已滿足所有需求了, 使用者要求存檔, 介面也會回應存檔成功或失敗。但是, 若 Save() 和 OnSaveResult() 之間間隔過久, 使用者不知道剛才的操作是成功或失敗, 或是他沒有按到存檔鈕 (使用觸控面版特別容易有這樣的疑問), 甚至會懷疑應用程式是否當掉了。

或許我們會考慮在呼叫 Document::Save() 之後, 順便在 UI 上顯示訊息, 讓使用者明白已經進行 Save() 的操作。單就這例子來看沒有問題, 但是當 model 操作比較複雜時就有問題了。比方說, 後來支援了自動存檔, 有可能使用者沒有按「存檔」, 但 document 定期會執行 Save(), 介面無從反應現在是否正在存檔中。

反過來說, 若是介面改為:

void Document::Save();
void DocumentDelegate::OnSaveResult(bool ok);
void DocumentDelegate::OnSaving();  // NEW

UI 不會漏掉這個變化。即使有多個 UI 元件關心 Document 是否正在存檔、存檔成功或失敗, 也可透過 DocumentDelegate 讓這些 UI 元件和 Document 相依, 而不是某個 UI 元件通知其它 UI 元件, 減輕 UI 元件之間的相依性。

總結來說, 對於所有的非同步操作, model 至少需要告知下述三種狀態變化:

  • 正在執行中
  • 執行成功
  • 執行失敗

這樣 UI 才有能力完成必要的功能。即使 UI 用不到也無所謂, 實作 model 時原本就需要考慮好可能的狀態變化, 提供對應的 callback 只是舉手之勞。反之, 實作 UI 時發覺綁手綁腳, 要再回頭思考該改 model 還是 UI (或是偷懶不做 ...), 會比較辛苦。

2013年7月8日 星期一

iOS UIView 除錯

Refernece: Visual View Debugging | Cocoanetics

這篇實在是太實用了, 備忘一下文中提到的技巧。

取得 view 資訊

NSLog(@"%@", view);

直接輸出 UIView 即可, 不用自己笨笨的輸出 frame 之類的資訊。

輸出 view 的巢狀結構

NSLog(@"%@", [view recursiveDescription]);

[UIView recursiveDescription] 是 private method, 不過 Objective-C 有 dynamic typing 的特性, 真的要呼叫的話, 系統也不會擋你, 只是編譯時有個 warning 而已。這功能相當實用, 可以確保相關的 view 是否有加進去, 了解 sub views 之間的順序 (後面的 view 會蓋掉前面的 view)。

在多個關鍵處輸出 recursive description 後, 才發覺我在某處不小心刪掉某個 view 沒加回去。若沒有這項功能, 執行後只知道沒出現這個 view, 到底是放錯位置、沒放進去或被覆蓋掉, 完全是一無所知啊。

畫出 view 的外框

view.layer.borderColor = [UIColor redColor].CGColor;
view.layer.borderWidth = 1.0;

使用上述程式的前置動作為:

  • linking 加入 QuartzCore.framework
  • 程式加上 #import <QuartzCore/QuartzCore.h>

2014-07-23 更新

  • [UIView recursiveDescription] 會顯示本身整層的結構, 若想看更上層的結構, 要往上存取 superview。但在 lldb 中打 po view.superview.superview 會失敗, 原因是 view.superview 傳回 id, 而 id 沒有型別, 無法直接呼叫 superview。所以要寫成 po ((UIView*)view.superview).superview。一但取得目標 UIView 的位置後 (例如 0x12345678), 再來就可以用 po [(UIView*)0x12345678 recursiveDescription] 得到目標的結構。
  • 承上, 更方便的作法是在 app 產生 UIWindow 的時候設中斷點, 取得 UIWindow 的位置 (例如 0x87654321), 之後隨時想看目前整個畫面的結構, 就按 XCode 的「暫停鍵」中斷程式進入 lldb, 再用 [(UIView*)0x87654321 recursiveDescription] 取得目前畫面的 UIView 結構。很快地了解 UIView 結構後, 要了解程式行為就容易多了。而且從 recursiveDescription 得知所有 subview 的位置後, 也可以配合轉型直接輸出各個 view 的其它屬性, 像是 [(UIScrollView*)0x12341234 zoomScale]。

2013年7月7日 星期日

High Performance Browser Networking ch5, ch6 心得

Chapter 5. Introduction to Wireless Networks

http://chimera.labs.oreilly.com/books/1230000000545/ch05.html

  • 影響傳輸頻寬的項目: C=BW×log2(1+S/N)
    • C is the channel capacity, and is measured in bits per second
    • BW is the available bandwidth, and is measured in hertz
    • S is signal and N is noise, and are measured in watts
  • 訊號的強弱就像聲音一樣:
    • 若環境音很吵, 很難聽清楚對方說的話。
    • 離愈遠愈不清楚。
  • 頻段受法律影響, 不是想用那一段就可以用那一段。

Chapter 6. Wi-Fi

http://chimera.labs.oreilly.com/books/1230000000545/ch06.html

  • 802.11 (Wi-Fi) 基於 802.3 (Ethernet) 的修改而來的
  • Ethernet 的作法是 "先聽一聽, 確定沒人說話之後再說話"。若有人同時想說話, 各自隨機休息一段時間, 再重試一次。
  • Wi-Fi 的作法和 Ethernet 相似, 只是受到無線通訊硬體的限制, 送資料的時候無法偵測是否有人在說話, 所以要由接受者回應 ACK 得知是否傳送成功。
  • 這個基於 Ethernet 而來的通訊協定只適用於低負擔的無線環境, 反之會很慘。
  • Wi-Fi 有重新傳輸 packet 的機制, TCP packet lost 比例通常不會因 physical layer 使用 Ehternet 或 Wi-Fi 而有差別, 但是使用 Wi-Fi 時很可能有較高且不同的延遲時間。
  • Wi-Fi 有分 2.4GHz 和 5GHz 兩個不同頻段區間。各區間有數個 channel, 若使用者使用不同 channel, 傳輸的訊號就不會打架, 這是提升傳輸品質的最好方法。
  • 這裡有 802.11b 不同 channel 對應到不同頻段的示意圖。使用 2.4GHz 的 Wi-Fi 協定時, 選用 1、6、11 最適當, 可減少和其它 channel 相衝。
  • 5GHz 有比較多 channel, 因此不容易和別人相衝。但是頻率愈高, 使用範圍愈窄。此外, 較舊的手機 (如 iPhone 4S 以前的 iPhone 機種) 只支援 2.4GHz。
  • 總之, Wi-Fi 沒有保證傳輸的頻寬以及延遲時間。
  • 可用 inSSIDer 觀察週圍的無線品質, Android、Windows、Mac OS X 上有軟體可用, Android 上有免費軟體。
  • 開發應用程式時, 可考慮監控過去的流量變化, 決定傳輸內容的品質。不過也要留意監控的結果是否夠即時、穩定, 若變化過大的話, 動態內容的品質有可能反而幫倒忙。

2013年6月27日 星期四

XCode 新增子專案編譯 static library 的方法

這種事沒記下來立即就忘了, 我是用 XCode 4.6。

新增子專案

  1. 在 Project Navigator 點專案 A
  2. 點左下角的 '+', 選擇加新專案, 選擇使用 static shared library, 完成剩下的步驟
  3. 點專案 A: Build Phases -> Target Dependencies -> 加入專案 B 的 static shared library
  4. 點專案 A: Build Phases -> Link Binary with Libraries: 加入專案 B。若專案 A 之上有專案 M, 這步要加在專案 M

雜七雜八問題

  • 找不到 header: 在 Build Settings 搜尋 header 會找到 Header Search Paths 和 User Header Search Paths, 記得填入正確的值。可以對一下編譯的原始訊息了解設下去轉換成什麼東西。這裡的路徑是相對於專案位置的。
  • 連結失敗, 找不到 static shared library 內的 symbol: 確定 Link Binary with Libraries 裡面有加到這個 library, 還有有在正確的 project 內設 Link Binary with Libraries。
  • 連結失敗, static shared library 內有 undefined symbol: 注意在 Link Binary with Libraries 裡和其它 libraries 的順序。同樣地, 對一下編譯的原始訊息會比較清楚情況。
  • static shared library 一直是紅字: google 一下有人說疑是 XCode bug, 在實體裝置上編譯過後, 就沒有紅字了。我試的結果確實如此。
  • 找不到 C++ STL 的 symbol: 錯誤訊息像這樣:
    std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::__init(char const*, unsigned long)", referenced from:
    檢查 Build Settings -> C++ Standard Library, 這裡有三個選項:
    • libstd++ (GNU C++ standard library)
    • libc++ (LLVM C++ standard library)
    • Compilation default (be consistent)
    記得所有專案要設成一致的選項, 新增專案預設是 libc++。

結論是先有基本觀念, 出錯時比較知道怎麼找原因。雖然一樣是 google 關鍵字, 至少讀搜尋結果時, 知道應該優先讀那些文章, 效率會好很多。再來是盡量在 Build Settings 和 Build Phases 裡找和編譯以及連結相關的參數。

2015-06-09 更新

XCode 6 新增專案的方式變了,改成在 Project Navigator 點專案 A 然後按右鍵選 "New Project"。然後選 "Framework & Library" -> "Cocoa Touch Static Library"。

2013年6月25日 星期二

hg-svn 速度極慢的原因

簡短說明

使用 hg-svn 時, 底層需要操作 svn 的 python 模組。hg-svn 有封裝兩組不同的實作, 在 Ubuntu 下的套件分別是 python-subvertpy 和 python-subversion。記者要裝 python-subvertpy, 個人試用的感覺, 前者比後者快了百倍以上吧。

冗長苦水心得

每次用 hg-svn 更新一堆檔案時 (大概數十個左右), 不管是 push 還是 pull 都相當地久, 有時等上一小時都不會好, 但用 strace 看, 持續有些動靜, 但是不知道在做什麼。

原本以為這是 hg 和 svn 溝通的極限, 今天問一下 cmd 平時用 git-svn 速度如何, 結果 cmd 說沒特別感到慢過, 當場試了一下, 果真迅速無比。差點想跳糟到 git-svn 陣營, 更何況安裝容易, sudo aptitude install git-svn 就結束了。

回家後想想不太甘心, 就用老方法在 hg-svn 原始碼內塞 print 找看看瓶頸卡在那裡 (慢到這種程度不太可能是 CPU bound, 不適合用 perf 之類的工具找)。

雖然有找到瓶頸, 但是找得過程中發覺 hg-svn 使用兩個不同的模組作為 svn 的接口, 一個是我原本用 svnwrap/svn_swig_wrapper.py, 對應到 Ubuntu 的套件為 python-subversion。另一個是 svnwrap/subvertpy_wrapper.py, 對應到的套件為 python-subvertpy, 而且 hg-svn 優先使用 python-subvertpy

接著查 python-subvertpy:

$ aptitude show python-subvertpy
...
Description: Alternative Python bindings for Subversion
 Python bindings for the Subversion version control system. The aim of these bindings is to be fast, complete and to provide an API that feels native to Python programmers.
...

真相大白, 改用 python-subvertpy 後, 龜速更新的問題就好了。

2013-07-17 更新

今天疑似用了 hg mv 後踏到 subvertpy 的 bug, hg push 時出現 exception:

subvertpy.SubversionException: ("'...' path not found", 160013)

後來改回用 python-subversion 的套件就可正常 push。所以說提供兩種實作做為選擇, 還是有好處的, 至少一個不行時有個希望試看另一個。

2013年6月23日 星期日

計算 Linux 記憶體使用量

雖然直覺上會使用 top 或 free 觀看結果, 但是 top 或 free 顯示的資訊包含 kernel 使用的 cache 量, 不是「所有 process 使用的量」。因此, 我習慣看 htop 顯示的值

Viller Hsiao 提醒, 才知道原來 htop 顯示的記憶體用量和 free 第二行的數值一樣, 都是讀 /proc/meminfo, 然後得出實際使用量 = MemTotal - MemFree - Buffers - Cached。若需要寫 script 記錄系統的 memory peak, 可用 free 的輸出, 畢竟 htop 沒有 batch mode 可用。

以下是一個 free 顯示的例子:

$ free
             total       used       free     shared    buffers     cached
Mem:      11418888    3247916    8170972          0    1274988     693072
-/+ buffers/cache:    1279856   10139032
Swap:            0          0          0

我之前一直誤會第二行的意思, 原來它的意思是「考慮第一行顯示的 buffers 和 cached 後, 重新計算 used 和 free 的值」。

Viller Hsiao 翻了一下原始碼證實這件事。我自己也試著練習翻閱原始碼, 增加翻原始碼查資料的能力。

以下是查詢過程的一些記錄。

free

找 free 原始碼的過程比較麻煩一點:

$ apt-file search /usr/bin/free | grep "free$"
procps: /usr/bin/free
$ aptitude show procps
...
Description: /proc file system utilities
 This package provides command line and full screen utilities for browsing procfs, a "pseudo" file system dynamically generated by the kernel to provide information about the status of
 entries in its process table (such as whether the process is running, stopped, or a "zombie").

 It contains free, kill, pkill, pgrep, pmap, ps, pwdx, skill, slabtop, snice, sysctl, tload, top, uptime, vmstat, w, and watch.
Homepage: http://procps.sf.net/
$ apt-get source procps

但原始碼滿好讀的:

free.c:

 63         meminfo();
...
 93             unsigned KLONG buffers_plus_cached = kb_main_buffers + kb_main_cached;
 94             printf(
 95                 "-/+ buffers/cache: %10Lu %10Lu\\n",
 96                 S(kb_main_used - buffers_plus_cached),
 97                 S(kb_main_free + buffers_plus_cached)
 98             );

利用上面的關鍵變數和已知關鍵資訊存在 /proc/meminfo 裡 (man proc 然後搜 meminfo), 進一步找到 proc/sysinfo.c, 從 memoinfo() 的實作證實 free 是讀 /proc/meminfo 的特定欄位, 計算出實際使用量。

htop

htop 本身套件名就是 htop, 但程式碼比較複雜一些。

先假設 htop 也是讀 /proc/meminfo, 以 meminfo 作為關鍵字下手, 可以找到關鍵的程式碼:

ProcessList.h

 20 #define PROCDIR "/proc"
...
 28 #define PROCMEMINFOFILE PROCDIR "/meminfo"

ProcessList.c

750 void ProcessList_scan(ProcessList* this) {
751    unsigned long long int usertime, nicetime, systemtime, systemalltime, idlealltime, idletime, totaltime, virtalltime;
752    unsigned long long int swapFree = 0;
753
754    FILE* file = fopen(PROCMEMINFOFILE, "r");
755    assert(file != NULL);
756    int cpus = this->cpuCount;
757    {
758       char buffer[128];
759       while (fgets(buffer, 128, file)) {
760
761          switch (buffer[0]) {
762          case 'M':
763             if (String_startsWith(buffer, "MemTotal:"))
764                sscanf(buffer, "MemTotal: %llu kB", &this->totalMem);
765             else if (String_startsWith(buffer, "MemFree:"))
766                sscanf(buffer, "MemFree: %llu kB", &this->freeMem);
767             else if (String_startsWith(buffer, "MemShared:"))
768                sscanf(buffer, "MemShared: %llu kB", &this->sharedMem);
769             break;
770          case 'B':
771             if (String_startsWith(buffer, "Buffers:"))
772                sscanf(buffer, "Buffers: %llu kB", &this->buffersMem);
773             break;
774          case 'C':
775             if (String_startsWith(buffer, "Cached:"))
776                sscanf(buffer, "Cached: %llu kB", &this->cachedMem);
777             break;
778          case 'S':
779             if (String_startsWith(buffer, "SwapTotal:"))
780                sscanf(buffer, "SwapTotal: %llu kB", &this->totalSwap);
781             if (String_startsWith(buffer, "SwapFree:"))
782                sscanf(buffer, "SwapFree: %llu kB", &swapFree);
783             break;
784          }
785       }
786    }
787
788    this->usedMem = this->totalMem - this->freeMem;
789    this->usedSwap = this->totalSwap - swapFree;
790    fclose(file);

另一方面, 以下是唯一用到 memory 欄位的程式碼:

MemoryMeter.c

 28 static void MemoryMeter_setValues(Meter* this, char* buffer, int size) {
 29    long int usedMem = this->pl->usedMem;
 30    long int buffersMem = this->pl->buffersMem;
 31    long int cachedMem = this->pl->cachedMem;
 32    usedMem -= buffersMem + cachedMem;
 33    this->total = this->pl->totalMem;
 34    this->values[0] = usedMem;
 35    this->values[1] = buffersMem;
 36    this->values[2] = cachedMem;
 37    snprintf(buffer, size, "%ld/%ldMB", (long int) usedMem / 1024, (long int) this->total / 1024);
 38 }

Meter.h

 21 typedef struct Meter_ Meter;
...
 53 struct Meter_ {
...
 62    ProcessList* pl;
...
 65 };

證實兩者計算方式相同。

結語

有疑問的時候, 還是翻原始碼最穩, 多做幾次之後會更順手。

另外藉由這次的經驗, 發覺寫純 C 程式時, 作者自己會有一套封裝資料結構和控制的方法。不像 C++ 已內建物件觀念, 不同的 C 程式會有不同的結構和控制的手法, 讀不同專案時, 需要理解一下該專案使用的方式。也許多讀幾次後, 會發覺大家的手法也差不多吧。

除錯小技巧: 在程式中直接中斷及偵測是否被 gdb 監控中

在程式中直接中斷

《Binary Hack》 #92 所言, 在 gdb 監控 process 的情況, 使用下列程式碼會進入中斷:

raise(SIGTRAP);

對 x86 環境, 下列作法也許會更好, 少了一層 function call

__asm__ __volatile__("int3");

Scott 有個 debugbreak 有處理 x86 以外的情況, 有需要時再來參考看看。

偵測是否被 gdb 監控中

比起在程式中直接中斷, 我比較需要知道目前程式是否被 gdb 監控 (attach) 中。以下是兩個實用的情境:

  • 原本已有 log 錯誤訊息, 在 log 極為嚴重錯誤訊息 (如 severity = FATAL) 時, 檢查看看是否有 gdb 在監控, 有的話不如順便進入中斷, 提前察看錯誤點。
  • 寫 multi-thread 時, 有時會有特定 thread 負責特定的事, 像是 UI 或 IO。有時會監控這個 thread 是否正常運作。或是有網路連線時, 有時會想監控另一端連線是否仍然正常運作。其中一種監控 thread 或網路連線的方式是週期性察看有無更新資料, 超過一定時間沒更新, 就中止程序或關掉連線重連。在使用 gdb 除錯時, 有可能在中斷點待太久而觸發前述安全防護機制, 反而無法長時間除錯。這時自動關閉防護措施會比較方便。

《Binary Hack》 #92 的作法利用設 gdb 會覆寫 SIGTRAP 的 signal handler 做檢查, 這個作法可以跨比較多平台, 但可用時機太窄。我偏好 Chromium 的作法, 直接讀 /proc/PID/status, 由 "TracerPid" 的值得知目前是否有 process 控控自己。程式執行中的任何時間都可用這方法得知正確的結果, 不過只適用於 Linux 和 Android。以下是參考 Chromium 實作稍作修改的版本:

2013年6月9日 星期日

High Performance Browser Networking ch4 Transport Layer Security (TLS) 筆記

原文: http://chimera.labs.oreilly.com/books/1230000000545/ch04.html

  • TLS 1.0 相當於 SSL 3.0, 兩者沒有顯著的差異。由於 SSL 屬於 Netscape 的協定, IETF 另定了 TLS。現在我們用的 "SSL" 其實是指 TLS
  • TLS 屬於 session layer 的協定, 在 application (例如 HTTP) 之下, transport (例如 TCP) 之上
  • TLS 三個要點:
    • Encryption: 加密資料, 其他人無法竊聽
    • Authentication: 認證對方身份, 傳錯人的話, 加密也沒用了
    • Integrity: 確保資料沒有被修改。中間人無法得知資料內容, 但有可能竄改內容。多了 checksum 可以避免使用被改過的資料
  • TLS handshake: 連 TCP handshake 在內要三次 round trip time (RTT)。若一次 RTT 要個 170ms, 這表示 0.5s 後才會開始傳第一筆資料。
  • TLS 多加的兩個 RTT 是:
    • -> ClientHello
    • <- ServerHello, Certificate, ServerHelloDone
    • -> ClientKeyExChange, ChangeCipherSpec, Finished
    • <- ChangeCipherSpec, Finished
  • TLS 的主要缺點是較高的 latency, CPU、記憶體和傳輸資料量到是沒差 (後述)。
  • TLS 主要的計算量在用 public/private key 加解密 symmetric key。整體的 CPU、記憶體和網路負擔都很小。
    • 依 2010 Google 使用 Gmail 的發言:
      "... we had to deploy no additional machines and no special hardware. On our production frontend machines, SSL/TLS accounts for less than 1% of the CPU load, less than 10 KB of memory per connection and less than 2% of network overhead. "
    • Facebook 也有類似的發言:
      "We have found that modern software-based TLS implementations running on commodity CPUs are fast enough to handle heavy HTTPS traffic load without needing to resort to dedicated cryptographic hardware. We serve all of our HTTPS traffic using software running on commodity hardware."
  • 若 server 想在同一個 IP 上使用多個不同 domain name 的 TLS certificate, client 可透過 server name indication (SNI), 指定要取得那份 certificate
  • TLS 有機會減少最後一個 RTT, 其中一個方法是使用 Session Ticket:
    • server 在 handshake 最後一步時, 用額外的 secret key 加密 symmetric key 回傳給 client。
    • client 存下加密後的 symmetric key
    • 下次要建立連線時, 在 ClientHello 裡帶上加密後的 symmetric key, 由於只有 server 知道解密的 secret key, 可保證由同一個 server 取回 symmetric key
    • 這個作法的好處是: server 不用 cache 大量 session ticket, 不過要在多台 server 上部署同一個 secret key。還有免不了的維護 secret key 的生命週期, 定期換 secret key 增加安全性。
  • 傳 certificate 給 client 時, server 最好傳給 client 所有需要的 intermediate certificate, 避免 client 額外花時間去取得它們。另外沒有必要傳 root certificate, client 一定要有 root certificate, 沒有的話也不會相信剩下的 intermediate certificate 了。
  • 若 TLS record size > TCP packet size, 會造成一筆 TLS record 拆成多個 TCP packets 傳, 導致 client 要收完全部 TCP packets 才能解開這筆 TLC record。最好設定 TLC record size <= TCP packet size
  • 不要開啟 TLS compression。
  • 文末有提到用 Qualys SSL Server Test 和 command line 的 openssl 測試是否有設好 TLS。

2013年6月6日 星期四

vim-protodef: 從 C++ header 產生實作樣板的好幫手

C++ 寫一陣時間後, 覺得每次在標頭檔寫完宣告又要在實作檔裡重打一次類似的函式名稱, 有點麻煩。原本想說自己寫個 vim script 處理這件事, 轉念一想, 應該有人做過了。結果找到別人滿完整的實作: derekwyatt/vim-protodef

作法滿聰明的, 用 ctags 讀標頭檔產生索引, 然後用 perl script 清理 ctags 產生的結果, 最後用 vim script 搜尋目標函式是否已出現在實作檔裡, 沒有的話就輸出到實作檔裡。日後有需要寫比較複雜的 vim script 時, 可以回頭來參考語法。

2013年5月30日 星期四

linux thread 與 signal

雖然是基礎知識, 一陣子沒用大概會忘記, 備忘一下重點:

  • 在 multi-thread 情況下, signal() 的行為是未定義, 必須用 sigaction() 註冊 signal
  • 用 kill 送 signal 給 process 後, process 內任何一個 thread 都有可能執行 signal handler。若不希望特定的 thread 收到 signal, 可用 pthread_sigmask() 改變特定 thread 的行為, 或是用 pthread_kill() 送出 signal 給指定的 thread
  • SIGSEGV 會送給發生 segmentation fault 的 thread。所以有 core dump 的時候, gdb 可以正確找出那個 thread 造成 segfault

參考資料:

和 Ubuntu 安裝套件相關的知識

先散亂地記在這邊, 慢慢補完:

1. 用 apt-get cache FILE 從檔案反查套件名稱

2. http://packages.ubuntu.com/ 找詢是否有這套件, 那一版開始有這套件 (Distribution 選 "any")

3. 套件分類 ( 參考資料 )

  • main: Free software, officially supported by Canonical
  • universe: Free software, NOT supported by Canonical
  • restricted: Non-free software officially supported by Canonical
  • multiverse: Non-free software NOT supported by Canonical

X 版本 (如 X = precise) 的 X-updates 表示 stable release update, 會有比較新一點的套件。預設 /etc/apt/sources.list 裡應該已有 X/X-updates 和 main/universe/restricted/multiverse 組合的八種來源。

4. 用 apt-cache policy PKG 了解安裝的優先順序

例如:

$ apt-cache policy sqlite3
sqlite3:
  Installed: 3.7.9-2ubuntu1.1
  Candidate: 3.7.9-2ubuntu1.1
  Version table:
 *** 3.7.9-2ubuntu1.1 0
        500 http://free.nchc.org.tw/ubuntu/ precise-updates/main amd64 Packages
        100 /var/lib/dpkg/status
     3.7.9-2ubuntu1 0
        500 http://free.nchc.org.tw/ubuntu/ precise/main amd64 Packages

使用 vim script 自動開啟 C/C++ 程式的標頭檔或程式碼

先附上完整 script:

" Open .h if it's a cpp file, and vice versa.
function! OpenComplementFile()
  let f = expand('%')   " (1)
  let suffix = matchstr(f, '\.\a\+$')
  let pattern = suffix . "$"
  if suffix == '.h'
    let suffixes = ['.cpp', '.cc', '.mm', '.m', '.h']
    for suf in suffixes
      let target = substitute(f, pattern, suf, '')   " (2)
      if filereadable(target)
        break
      endif
    endfor
  elseif suffix == '.cpp' || suffix == '.cc' || suffix == '.m' || suffix == '.mm'
    let target = substitute(f, pattern, '.h', '')
    if !filereadable(target)
      let tmp = target
      let target = substitute(tmp, '\v(.+)\..+', 'public/\1.h', '')  " (3)
      if !filereadable(target)
        let target = substitute(tmp, '\v(.+)/(.+)\.(.+)', '\1/public/\2.h', '')
      endif
    endif
  else
    let target = ''
  endif

  if filereadable(target)
    exec 'vsplit ' target
  else
    echo "Complement file not found"
  endif
endfunction
nnoremap <silent> <F4> :call OpenComplementFile()<CR>

在 vimrc 裡放入上述程式, 然後在 normal mode 裡按 F4 會有如下的反應:

  • 若是在 X.h 裡面, 會試著在垂直分開的視窗裡開啟 X.cpp, 若沒有 X.cpp, 則會依序改試 X.cc, X.mm, X.m。
  • 若是在 X.cpp (或 .cc, .mm, .m) 裡, 會先試開 X.h, 若沒有 X.h, 再試 public/X.h。有需要的話, 可以配合自己的習慣往更深的目錄找。

1.

  • 詳見 :h expand。
  • % 表示目前的檔名。
  • 簡單的檔名轉換可以用 expand 就做完, 比方說如下的例子:
"open A_test.X if current file name is A.X
"open A.X if current file name is A_test.X
function OpenCorrespondingFile()
    let d = split(expand("%"), '_test')
    if len(d) == 1
        let name = expand("%:r") . "_test." . expand("%:e")
    else
        let name = d[0] . d[1]
    endif
    exec 'vsplit ' name
endfunction

2.

  • 寫上述 script 的時候, 花最多時間查詢如何使用函式 substitue()。參考 vim 內部的說明 :h substitute() 即可。
  • 注意要加 () 表示查詢函式的文件, 而不是查 Ex command 的 :substitute

3.

  • :h magic 說明 vim 對待特殊字元的方法。在 pattern 前面加上 "\v" 會和其它 script 的用法一致, 比較方便。

2013年5月3日 星期五

使用 --start-group 和 --end-group 解決 circular dependencies

只是想備忘這篇: GCC: --start-group and --end-group command line options - Stack Overflow

關鍵的 gcc 指令:

-Wl,--start-group -lmy_lib -lyour_lib -lhis_lib -Wl,--end-group -ltheir_lib
  • gcc man page 說明 -Wl,X 傳遞參數 X 給 linker
  • ld man page 說明 --start-group archives --end-group

這個作法的好處是不用理會 static library 在參數裡的順序 (預設是假設 A 依賴 B 時, A 要放在 B 前面), 壞處是會加長 linking 時間。

Btw, 遇到和 linking 相關的問題時, 記得要同時從 compiler 和 linker 的角度看問題。

2013年4月28日 星期日

用 mutt 作為 command line 寄信程式

預設的 mail 不方便夾帶附加檔, 備忘一下用 mutt 做這事的指令:

mutt -s TITLE EMAIL -a ATTACHMENT < CONTENT

2013年4月19日 星期五

python BaseHTTPServer 速度緩慢的原因

在用 Bottle 的開發模式時, 發覺有時候速度會異常的慢。等到受不了以後, 按下 ctrl+c 看到以下的 backtrace:

Exception happened during processing of request from ('xxx.xxx.xxx.xxx', 37515)
Traceback (most recent call last):
File "/usr/lib/python2.7/SocketServer.py", line 284, in _handle_request_noblock
self.process_request(request, client_address)
File "/usr/lib/python2.7/SocketServer.py", line 310, in process_request
self.finish_request(request, client_address)
File "/usr/lib/python2.7/SocketServer.py", line 323, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/usr/lib/python2.7/SocketServer.py", line 638, in __init__
self.handle()
File "/usr/lib/python2.7/wsgiref/simple_server.py", line 121, in handle
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
File "/usr/lib/python2.7/wsgiref/simple_server.py", line 85, in get_environ
host = self.address_string()
File "/usr/lib/python2.7/BaseHTTPServer.py", line 498, in address_string
return socket.getfqdn(host)
File "/usr/lib/python2.7/socket.py", line 137, in getfqdn
hostname, aliases, ipaddrs = gethostbyaddr(name)
KeyboardInterrupt

依照過去的經驗, 這個 gethostbyaddr 相當可疑。

作個簡單測試證實問題:

$ python -c 'import socket; print socket.gethostbyaddr("xxx.xxx.xxx.xxx")'
Traceback (most recent call last):
File "<string>", line 1, in <module>
socket.herror: [Errno 2] Host name lookup failure

再來用老招 ltrace 確認問題源頭:

$ ltrace  python -c 'import socket; print 
socket.gethostbyaddr("xxx.xxx.xxx.xxx")' 2>&1 | grep gethost
( ... 略 ... )
gethostbyaddr_r(0x7fffa5bbb834, 4, 2, 0x7fffa5bb77f0, 0x7fffa5bb7830) = 0

由 man gethostbyaddr 得知它會查 /etc/hosts, 最後在 /etc/hosts 加入一筆 "xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx", 就立即有結果了:

$ python -c 'import socket; print socket.gethostbyaddr("xxx.xxx.xxx.xxx")'
('xxx.xxx.xxx.xxx', [], ['xxx.xxx.xxx.xxx'])

2013年4月16日 星期二

C++ 執行後掛在 __cxa_pure_virtual

今天遇到一個神奇的 bug, 程式掛掉後, 從 backtrace 看到其中一個函式名稱為 __cxa_pure_virtual, 從字面上的意思來看, 我呼叫了一個 pure virtual function, 所以程式掛了。但是 compiler 應該能抓到在 C++ 內呼叫 pure virtual function (Btw, Objective C++ 則否), 沒道理在 runtime 遇到這情況。

google 了一陣子, 看到這篇說有可能是呼叫 dangling pointer 的 virtual function, "Pure Virtual Function Called": An Explanation 則解釋各種可能的觸發原因。最後抓到兇手, 確實是呼叫 dangling pointer 的 virtual function。

依文章的解釋, destructor 會改變 virtual method table pointer 指向的位置, 最後會指向 abstract base class 的 virtual table, 裡面的 virtual function 則指向某個表示 pure virtual function 的指標 (gcc 用__cxa_pure_virtual)。所以, 若刪除指標後剛好記憶體內容保持原樣, 執行後就會呼叫到 __cxa_pure_virtual

依管理記憶體的方式, 刪除指標時可能會保留原狀, 也可能填入特殊值故意觸發 crash。呼叫 dangling 可能會有不同結果。

2013年4月5日 星期五

在 linux 和 iOS 上抓 memory leak 和 heap profiling 心得

問題描述

首先要定義一下什麼是「memory leak」。它的正式定義是指「程式弄丟了未釋放空間的位置」, 之後無法使用這塊空間也無法釋放它。另一方面, 程式沒有弄丟位置, 但是配置的空間愈用愈大, 或是用完後沒有歸還, 導致記憶體不足。若要應付第一種情況, 可用 memory leak 作為關鍵字找相關討論; 第二種情況要用 memory profiler 或 heap profiler 了解記憶體用去那裡, ( 感謝 Scott 告知 )。

就我自己少量的經驗來說, 第二種比較棘手, 第一種通常是粗心, 找到後補上 free/delete 即可。但第二種可能和程式配置和運用記憶體的策略有關。我遇到的問題都是第二種。若程式有針對不同區塊自己管記憶體的話, 比較方便日後植入統計程式找出瓶頸。順道一提, 如同 TDD 的精神, 在設計程式架構時就考慮測試, 會讓程式更易測試、維護和除錯。在設計架構時就考慮除錯, 也會讓程式更易維護和除錯。

在 Linux 上找 memory leak 和 profile heap

在 Linux 上試了兩套工具 mtracevalgrind。mtrace 滿簡單的, 可用來抓 C 的 memory leak, 但不適用於 C++, 原因是 mtrace 在 malloc/free 動手腳, 可以找出那一行呼叫 malloc 後沒呼叫 free。但是 C++ 的情況是 libstdc++ 呼叫 malloc, 而我們有興趣的是誰呼叫了 new。

後來改試 valgrind 效果還不錯。valgrind 是多個除錯工具的集合體, 我試了兩個工具:

  • memcheck: 抓 memory leak 和記憶體讀寫錯誤。
  • massif: profile heap 的用量, 可看到不同時間 heap 由各函式配置的比例。

使用 valgrind 的代價是程式會慢個 20 倍左右。但是用工具協助檢測問題, 99% 的情況會比自己憑空猜想來得準。還是有些耐性使用 valgrind 會比較快找到問題源頭。

memcheck

$ valgrind --tool=memcheck --leak-check=full PROG PROG_ARG ...

跑完後會輸出結果在 console。建議先有些耐心看完官網文件。對 memcheck 的報表有疑問的話, 可參考 "Understanding Valgrind memory leak reports"。重點是 possibly leak 表示 valgrind 無法判斷它是 still reachable 或 definitely leak。still reachable 可能是某些函式庫配置自己一塊 memory pool 來用, 程式離開時才會釋放, 所以嚴格說起來不是 memory leak。在這個階段, 我們關心的是 definitely leak。

massif

$ valgrind--tool=massif PROG PROG_ARG ...

跑完後會輸出結果在 massif.out.PID。然後執行 ms_print 看結果

$ ms_print massif.out.PID

建議先有些耐心看完官網文件。我試了許多參數想抓到程式離開前的 detailed snapshot 都徒勞無功。最後發覺用 vgdb 意外的簡單。

用法如下:

  1. 先執行 valgrind
  2. 在另一個 shell 下執行 vgdb detailed_snapshot /path/to/snapshot
  3. 閱讀當下的 snapshot: ms_print /path/to/snapshot

valgrind 也可以和 gdb 一起互動, 沒想到會在這個機會下試用到 gdb 的 remote debugging 功能。

經 Scott 告知, 除了用 ms_print 看文字報表外, 也可用 Massif Visualizer 看結果。這工具看起來滿不錯的, 先備忘在這裡。

另外補充一下 Linux 下記憶體用量的意思。平常我們是看 ps 回報的 VSZ 和 RSS, 但兩者其實都不夠準確, 用 pmap -d PID 看最後一行輸出的 private writable memory, 可能比較適合。

在 iOS 的找 memory leak 和 profile heap

就用 Mac OS X 附的 Instrutments, 使用 Leak 或 Allocations, 不用看文件即可輕鬆搞定! 全圖形介面操作, 還有詳細的報表。要看各 functions 用掉 heap 記憶體的比例的話, 在 Allocations 裡點 Statistics -> Call Tree。

同事用過 Instruments 後, 甚至說「真想將 Linux 的程式 port 到 Mac, 這樣才可以用 Instruments」, 由此可知 Instruments 有多麼地好用。

2013年3月21日 星期四

比 soft link 更好用的 mount --bind

當目錄 a 所在的 partition 沒空間但又得放東西在 a 下面時, 通常會另建 partition, 然後 soft link 新 partition 下某個目錄過來, 比方說從新 partition 用 soft link 指向 a/b。

這個作法有個缺點, 若 a/b/ 下的程式想透過 ../ 存取 a 目錄下的東西, 比方說用 ../c 取得 a/c。在 soft link 的情況會找不到 c, 因為 b 其實在另一個 partition, b/.. 並不是 a。

這時可用 mount --bind 解, 作法很簡單, 就直接抄 man mount 的內容過來備忘:

Since Linux 2.4.0 it is possible to remount part of the file hierarchy somewhere else. The call is
     mount --bind olddir newdir
or shortoption
     mount -B olddir newdir
or fstab entry is:
     /olddir /newdir none bind

和 C 前置處理器相關的小技巧

在 gdb 內查詢 macro

需要在編譯時加上 -g3, info macro 等指令才會有用。

參考資料: http://sourceware.org/gdb/onlinedocs/gdb/Macros.html

列出 include 的內容

$ gcc -E a.c

編譯錯誤指出缺少某個 symbol 時, 可用這招確定到底有沒有引入目標的宣告。有時 #ifdef 太多, 看原始碼不易確認這件事。

列出 define 的內容

比方說若想知道 errno 111 代表什麼意思, 可這麼做:
$ echo "#include <errno.h>" | gcc -dM -E - | grep " 111$"
#define ECONNREFUSED 111
再來 man errno 查詢 ECONNREFUSED 即可得到答案。
另外, 可用以下的方法得知系統預先 define 的東西
gcc -dM -E - < /dev/null
參考資料: http://stackoverflow.com/questions/2224334/gcc-dump-preprocessor-defines

2013年3月20日 星期三

python memory leak

由於 CPython 使用 reference counting 管理記憶體, reference count 到 0 時會立即釋放記憶體, 遇到 memory leak 時, 至少可以用 del 稍微自救一番。

import sys

a = []
print sys.getrefcount(a)  # 2
b = a
print sys.getrefcount(a)  # 3
del b
print sys.getrefcount(a)  # 2

如上所示, 可在程式中用 sys.getrefcount() 檢查 referece count 是否符合預期, 盡量在不用以後立即呼叫 del, 當 reference count 變為 0 時, 會提前釋放記憶體。

注意 python 的 while、for 沒有形成新的 scope, 有可能因此誤判一些物件的生命週期:

for i in range(10):
    pass
print i  # 9, 並非 NameError

備註

附上 CPython 2.7.3 Include/object.h 內關於減 reference 相關的程式:

769 #define Py_DECREF(op)                                   \
770     do {                                                \
771         if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
772         --((PyObject*)(op))->ob_refcnt != 0)            \
773             _Py_CHECK_REFCNT(op)                        \
774         else                                            \
775         _Py_Dealloc((PyObject *)(op));                  \
776     } while (0)

可看出在 reference count 為 0 時, 確實會呼叫 deallocator。

今日休閒時間已用盡, 改天再來看看為什麼初始數值為 2。

2013年3月3日 星期日

Core Data lightweight migration

上份工作需要階段性處理大量資料, 當時有不少 database schema 與 data migration 的經驗, 雖然我不怎麼喜歡 Django 的 ORM, 不過配合 South 做 migration 到是滿方便的。

最近用 Core Data 不幸地在發佈測試程式後發覺需要增加欄位, 查了一下作法, 所幸 Core Data 有針對簡單的情況提供自動化 migration, 相當輕鬆, 簡記用法如下:

說明瀏覽器運作相關的文章

備忘一些不錯的概念文章:

  • WebKit for Developers - Paul Irish: 說明各家使用 WebKit 的瀏覽器到底共用的 WebKit 是什麼。另有附一份 slide 說明 "How WebKit Works", 不過 slide 講的內容就很少了。
  • How browsers work, 2011 年的文章, 說明瀏覽器運作的每個步驟

iOS delegation vs. notification

初始使用 iOS 的 NSNotificationCenter 覺得頗不錯的。UI 物件需要從一個遙遠的物件裡取得資料改變的狀態, 當初沒考慮好這個需求, 不太方便傳遞資料。這時用 notification 解套就容易多了。但是隱約覺得 notification 的用法不太妙, 查到這篇 When to use Delegation, Notification, or Observation in iOS 分析得很清楚, 摘要重點如下。
使用 notification 的缺點 (或說特色) 是:
  • notification 是非同步呼叫, 發送 notification 和接受 notification 在不同的 runloop iteration 裡, 不易除錯。
  • notification 的發送和註冊採用字串做為 key, 無法在編譯時期找到錯誤。執行時才會察覺「好像沒呼叫到」
另一方面, delegation 透過定義使用端的介面 (iOS 術語為 protocol), 運作的特點為:
  • 透過函式呼叫通知事件, 方便除錯
  • 可在編譯時期確保提供服務者和用戶端有正確串在一起
  • 除通知發生事件外, 可讓客戶提供實作供提供服務者使用
原文有提及更多項目, 不過對我來說最要緊的是「是否容易除錯」。從這個角度來看, 使用 delegation 遠比 notification 安全。所以可以的話, 盡量避免使用 notification 較好。

2013-03-03 更新

經 Sam 指正, 發覺 notification 預設是同步呼叫, 在 backtrace 裡可找到發出點和接收點。這樣就沒那麼難除錯了。重看一遍該篇文章, 作者提的除錯困難, 可能是指用太多 notification 混在一起的情況。

兩者的主要差別應該是介面的強度。介面規範強, 編譯時期自然抓到較多錯誤, 寫起來也比較麻煩一些。其它的小細節如「少了 observer 訊息、一對多 (多對一) 架構」等, 則是設計介面或使用時可補救的。

2013年2月11日 星期一

GUI 架構的考量

( 和 Scott 聊過後, 決定改寫《非同步程式新手心得: 為什麼要用單一 thread + task queue 的方式設計架構》的前半段。比較完整地描述 GUI 架構的入門心得。 )

GUI 需要低延遲的反應速度, 隨時能接收使用者輸入和並提供畫面回饋。這表示要在不互相阻檔的前提下應付三種工作:

  • 更新畫面
  • 偵測使用者輸入
  • 處理使用者輸入

甚至第四種情況:

  • 網路輸入

若是使用 multi-thread 處理上述情況 (比方說有三個 thread 分別專職處理前述三件工作), 必須留意 thread 之間有共享資料的情況。為了避免 race condition 必須使用 lock; 使用 lock 後又要擔心 dead lock。

使用 global lock 有可能成效不彰, 比方說更新畫面太久, 而沒有即時回饋使用者的輸入。另一方面, 切許多細小的 lock 處理各塊共享資料, 容易出錯造成 dead lock, 或誤以為有處理好卻仍存在 race condition。

反過來說, 若每個工作 (#1) 都切得很細, 執行時間不長, 但可能需要多個工作單元才能完成目標。這樣就能在一個 thread 內以類似 queue consumer 的方式執行全部的工作。有新的需求時, 放入工作到 queue 裡, 大致上可確保相依工作之間的執行順序 (#2)。由於全部工作都在同一 thread 執行, 自然不用擔心 dead lock 或 race condition。

於是可以看到, 像 Gtk、iOS、Android 的平台, 都將更新畫面、偵測事件和處理事件三者整合在 main thread 處理, 並且假設程式以 single thread 的方式執行 (#3)。

我不清楚其它平台的情況, iOS 有提供 NSURLRequest 應付常見的網路需求。讀寫資料和事件的 callback 也都在 main thread 內執行。

這麼一來, 框架開發者省事許多, 使用框架的開發者也可以安心地實作 callback。

備註

#1 可想像每個工作是一個函式, 在 iOS 或 Android 上, 整合這種「工作 queue」的概念到基本框架裡, 用起來很直覺, 希望盡量避免開發者從 thread 的角度去處理事情。

#2 不行的時候只好加入狀態變數了解目前該做什麼, 至少不用擔心狀態變數本身會有 race condition。

#3 若開發者有特別需求, 比方涉及檔案或網路 I/O, 或是需要大量 CPU 計算, 而無法在短時間內完成單一工作的話, 開發者需要自行另外處理。使用新的 thread 的話, 自然需要使用 lock 保護和 main thread 共享的資料, 但需要煩惱的範圍已降致最小。

async 與 non-blocking IO

以前一直沒有分清楚 asynchronous 和 non-blocking 的差異, 寫久了以後才明白兩者是不同層次的東西。

  • asynchronous programming: 程式不是一條線走到底, 可能目前做到一個段落停下來, 「過一會兒」再繼續執行
  • non-blocking IO: 呼叫讀寫函式時, 不會等到完成才返回

通常要讀寫外部資料時 (一般檔案、網路、pipe 等) , 無法確定返回的時間, 這樣會阻擋程式的運作, 所以希望這個讀取 (或寫入) 不要等完成後才返回 (即「非同步」)。

若不希望此次呼叫有不確定時間的擔擱, 有兩種可能的作法:

  • 開新 thread 或用既有的 thread 進行讀寫。總之, 停是停別人家的 thread, 和本 thread 無關
  • 設定 file descriptor 為 O_NONBLOCK, 告訴 OS 這個 fd 為 non-blocking IO, 呼叫後要能立即返回

兩者作法的優缺點分析已超出本「隨手記」的範圍 (書本作者愛用的大絕), 需要特別注意的是, 設了 O_NONBLOCK 不表示呼叫 read/write 後就結束了。

man 2 read (或 man 2 write) 的 ERRORS 提到:

  • EAGAIN The file descriptor fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.
  • EAGAIN or EWOULDBLOCK The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.

意思是說:

  • 招對實體檔案無效 (為了檔案讀寫效能, 一次做完可減少硬碟更動讀寫頭的時間)
  • 對 socket 仍有可能失效, 記得用 select / epoll 之類的 system call 請系統在有機會完成 non-blocking 操作時通知你。也就是說, 系統只保證此次呼叫不會 block 你, 不保證此次呼叫一定會成功。由開發者持續嘗試呼叫, 總有完成的時候。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...