顯示具有 C_Cpp 標籤的文章。 顯示所有文章
顯示具有 C_Cpp 標籤的文章。 顯示所有文章

2020年5月16日 星期六

第一次跳槽 vscode 就上手

故事是這樣子的,小弟第一次學寫 code 的時候,是在大一修計算機程式(嚴格來說是高三下學期上了幾個小時的 C,不過那實在稱不上是"學")的時候,第一個使用編輯器是破舊破舊的 Dev C++ ,我打這篇的時候差點都忘了它叫 Dev C++ 了。
當然那時候的功力跟現在實在是天差地遠,淨寫一些垃圾,啊雖然現在也是淨寫一堆垃圾…。
總之後來應該是大二,被同學們拉去演算法課上當砲灰,第一次接觸了工作站 + vim,從那時候把 Dev C++ 給丟了跳槽到 vim,就一直用到現在,之中當然也會用一下其他的編輯器,像是改 windows 的 .NET 程式用到 Visual Studio,但大體還是以 vim 為主力,算算也是超過 10 年的 vimer 了。

不過這兩三年在工作上、日常 project 上面,多多少少都見識到 vim 的不足之處,例如新語言(主要是 Rust)支援不足、跟編譯除錯工具整合不佳、跟 GUI 整合不佳、跟 Git 整合不佳要另外開終端機跟 gitg、自動格式化/排版操作麻煩而且通常排不好;正好此時 Microsoft 回心轉意擁抱開源,推出了 vscode,隔壁棚的 emacs 有大神跳槽鬧得風風雨雨,台灣 CUDA 第一把交椅強者我同學 JJL 也跳槽 vscode 惹還來傳教。

正好最近寫 code 沒什麼靈感,而且最近正好武漢肺炎的關係時機歹歹,就來試著跳槽一下吧(?,到目前為止用 vscode 對最近碰的一個 ncollide package 做了一些除錯的工作,筆記一下到目前為止的設定還有使用方式的筆記。

vscode 基本上的優勢就是是它編輯/建構/除錯三位一體的編輯介面;還有它的擴充功能,用過的都說讚。
擴充方面主要參考的文件有兩個:VSCode 如何提高我的寫扣效率小克的 Visual Studio Code 必裝擴充套件,另外台灣 CUDA 第一把交椅強者我同學 JJL 大大也有推薦一些:

擴充的安裝方式是按快捷鍵 Ctrl + P,打入 ext install 後面接套件名,下面擴充的連結裡面也有顯示安裝的指令:

vim 擴充,讓 vscode 的編輯介面套用 vim 的操作方式,想要手跟鍵盤黏踢踢就一定要裝
語言相關:
C/C++ 擴充:還沒試用只是覺得起家的 C++ 必須裝一下:
Python 擴充:一樣還沒試用只是覺得有一天會寫到先裝一下:
Rust 擴充:這個是這次語言唯一試用過的,雖然結果不怎麼樣
codelldb 除錯擴充,可以用 LLVM 的 lldb 對程式除錯,裝了這個是為了要對 Rust 除錯

工具類:
Git Graph:整合 gitg 類似的圖形化顯示工具到介面,git 管理上當然可以靠打字,但看歷史還是看圖方便
GitLens:還沒試過,強者我同學 JJL 推薦的
TODO tree:統一管理 project 內部的 TODO, FIXME, XXX
Trailing Spaces:自動刪掉程式碼行尾的空白
Markdown Github Style:編輯 markdown 文件時可以直接預覽輸出的格式,解決每次編輯 Github README.md 都要一直 push -f 直到格式完全改對為止,這點很強烈的突顯出 vim 等純文字編輯器的弱項,無法和圖形整合,以致在 markdown、LaTex 這類文字和顯示有相互關係的文件編輯會很吃虧(好啦好啦我知道有人能人腦 render latex 的)。

怎麼建構專案?
在 vscode 裡面的建構叫 task,在選單 terminal 下面的 run Task 跟 run Build Task (Ctrl + Shift + B),沒有 cargo 預設的話就要自行編輯 tasks.json,以下是我這次 debug 時使用的 tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "cargo run",
      "type": "shell",
      "command": "cargo",
      "args": ["build"],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}
應該滿直覺的,就是呼叫 cargo build 幫我編譯整個專案;在寫完 code 之後使用快捷鍵 Ctrl + Shift + B 就能編譯專案了。

如何除錯:
除錯是 vscode 一項殺手級的功能,vscode 公開一個 API 讓安裝的語言擴充使用,需要什麼語言的除錯安裝擴充就好,像我上面就安裝了 C/C++, Python, Rust 的擴充。如果我記憶沒錯的話,跟 visual studio 一樣,vscode 快捷鍵是也執行 Ctrl + F5 跟除錯 F5:

用 Ctrl + Shift + D 展開 debug 介面。
理論上用滑鼠在原始碼旁邊點一下就能加上 breakpoint 不知道是 Rust 還是 lldb 的問題,我用滑鼠加上去的 breakpoint 都煞不住,至少一定要先在 debug console 裡下一個 b main 讓程式煞住之後,用滑鼠加的 breakpoint 才會有用,真的很奇怪。
另外就是 debug console 的指令跟習慣的 gdb 有點不同要重新習慣,最奇怪的大概是按了 enter 竟然不會重複上個指令,這樣要一直按 n + enter + n + enter 怪麻煩的,只能去習慣 vscode 的除錯指令:F10/next、F11/step、Shift+F11/finish 了。

這次最主要的目的是要對 Rust 程式除錯,我參考的是下面這篇文章…不過試用之後沒有成功
首先我們要加上一個 launch.json 告訴 vscode 要怎麼跑除錯的程式:
{
  "version": "0.2.0",
  "configurations": [
  {
    "name": "Debug example contact_query2d",
    "type": "lldb",
    "request": "launch",
    "program": "${workspaceRoot}/target/debug/examples/contact_query2d",
    "args": [],
    "cwd": "${workspaceRoot}",
  }]
}
再來用 F5 就能開始除錯了,但不知道為什麼我 step into 一個函式,瞬間都變成 assembly code,連 stack 資訊都爛掉了,根本無從 debug 起,感覺是 vscode 哪裡跟 lldb 沒弄好,不過我覺得這不是 vscode 的問題,畢竟我在終端機用 rust-gdb 一樣會有問題,正好反過來如果下 b main 停下來的話,rust-gdb 下一步會停不下來,一口氣跑到 main 的尾巴…。
這個問題一時之間好像無解,也許要等 rust 跟 codelldb/gdb 真的接好之後再來看看了。

下面就是個零碎操作:
Ctrl + KT 叫出 color theme 設定,我用的是 light 的 Solarized Light ,最近眼睛好像不太適合全黑的畫面了QQ。

回頭一看怎麼一堆快捷鍵,不過算啦,跟 vim 的快捷鍵比起來這還是算少的吧XD;話說大概是「把手留在核心區」這個哲學的關係,vim 大部分的按鍵都少有用 Ctrl/Alt 開頭的,剛好一般的圖形應用程式包括 vscode ,大部分的快捷鍵都是 Ctrl/Alt 開頭,也因此在操作上面,vim 很容易就能跟桌面應用程式整在一起,就像是瀏覽器的 vimium 跟 vscode vim 擴充。

2019年12月14日 星期六

用 Qt Graphics 做一個顯示座標的工具 - 細節調整

其實這篇才是我寫文的主因,前面兩篇其實都是前言(欸),總之就是我埋頭自幹 AZ 大大吃一頓飯就寫得出來的工具,結果過程中寫得亂七八糟,有時候該出現的東西就是不出現測到頭很痛,或是測一測就進無窮迴圈後來發現是自己蠢,發了 v0.1 之後被靠北顯示怎麼這麼醜,想改又很難搜不到解法QQ。
當然最後還是搜到了,但就希望可以多寫一篇文,讓這些解法更容易被搜到,如果運氣很好真的幫到人也算功德一件,如果你真的被幫到的話,麻煩幫我這篇文章留下一個 like 然後順手按下旁邊的訂閱和小鈴鐺(醒醒這裡是 blogger 不是 youtube。

畫上座標線:

這算是一個附加功能,一般的顯示工具只要設定 Scene 的 background brush,設定一個黑色的 brush,就能畫出黑色背景了。
但如果我們要更精細的話,就要去實作 scene 或是 view 的 drawBackground 函式:
void drawBackground(QPainter *painter, const QRectF &rect)

下面是我實作的程式碼節錄,有幾個可以注意的地方:
  1. Qt 的座標系統左上角是 0,0,往右往下是 x 遞增跟 y 遞增,所以用 top 的值會比 bottom 小。
  2. 這個函式參數是一個 QPainter 跟一個 QRectF,painter 好理解就是目前作畫的對象,在這個 painter 上面作畫就會畫在背景上;QRectF 一般會認為就是現在顯示背景的矩形,不過不對,它是「現在要更新背景的範圍的矩形」。
    例如我現在顯示的 Scene 大小是 -50,-50 ~ 50, 50,但我實作滑鼠可以在 scene 上面拉一個選取的小框框,在我拉框框的時候,Qt 會判定只有這個小框框裡面的背景是需要重畫的,QRectF 就會設定到這個小框框上;其實某種程度來看這樣也對,而且更節省重畫的資源。
  3. 承上點,所以有抓到現在 scene 的大小要怎麼辦?這也是我為什麼選擇是實作 View 的 drawBackground 而不是 Scene 的,因為我可以透過 mapToScene(viewport()->rect()).boundingRect() 取得這個 View 現在顯示 Scene 的大小。
後來就是一些數學計算,在對應的座標點上呼叫 drawPoint 了,下面的 code 我有略為刪節過了,要用的話不要全抄。
void
MyViewer::drawBackground(QPainter *painter, const QRectF &rect) {
  qreal left         = rect.left();
  qreal right      = rect.right();
  qreal top         = rect.top();
  qreal bottom = rect.bottom();

  QRectF sceneRect = mapToScene(viewport()->rect()).boundingRect();
  qreal size = qMax(sceneRect.width(), sceneRect.height());
  qreal step = qPow(10, qFloor(log10(size/4)));

  qreal snap_l = qFloor(left / step) * step;
  qreal snap_r = qFloor(right / step) * step;
  qreal snap_b = qFloor(bottom / step) * step;
  qreal snap_t = qFloor(top / step) * step; 

  // print coordinate point
  for (qreal x = snap_l; x <= snap_r; x += step) {
    for (qreal y = snap_t; y <= snap_b; y += step) {
      painter->drawPoint(x, y);
    }
  }

  // print coordinate line
  painter->drawLine(qFloor(left), 0, qCeil(right), 0);
  painter->drawLine(0, qCeil(bottom), 0, qFloor(top));
  QGraphicsView::drawBackground(painter, rect);
}

不動物件:

第一個是所謂的不動物件,也就是 QGraphicsItem 透過設定了 ItemIgnoresTransformations flag,這樣這個 item 就不會受到 view 視角變化的影響。

使用情境也很單純,像是在畫面上打個 marker 或是寫上文字,如果視窗縮小就看不到就奇怪了,所以這個 marker 就要設定這個 flag,放大縮小都會顯示一樣。
改變也就是呼叫一下
setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
就可以了。

要注意的是在設定了這個 flag 之後,在這個 item 裡面的位移似乎會失去效果(還是行為會變很怪,我有點忘了),一般要在一個位置例如 100, 100 畫一個正方形,我們可以用 QGraphicsRectItem,在 100, 100 的地方畫正方形;如果是 ignore transformation 的物件,我是變成在 0,0 的位置畫一個正方形,然後把物件的位置用 setPos 設定在 100, 100。
這部分當初真的弄超久,後來覺得這樣不行,把放在 dropbox 裡面的 <c++ GUI Programming With Qt 4> 拿出來翻翻,沒想到在第八章 Qt graphics 章節就講了要怎麼寫類似的東西,還有範例 code ,果然寫程式還是要多看書而不是瞎攪和,弄了好一陣子的東西其實書上的範例都寫了。

填充物:

上一篇文的最後一張圖,應該很明顯可以看到,我用 brush 填進去的東西,非常的…不均勻,一格一格的非常醜,實際運作的 code 也是,只要放大縮小填充物的就會變得不連續。
這是因為 QBrush 在填東西的時候,用固定密度在填充,不會隨著螢幕的放大縮小改變填充物的密度,要修成也只需要一行,在 paint 函式裡面加上這個:
QBrush m_brush;
m_brush.setTransform(QTransform(painter->worldTransform().inverted()));
painter->setBrush(m_brush)
把現在場景的變形反轉補償回去就可以了;這個解答出自 Stack Overflow

隨放大縮小調整長度:

同樣的是另一張圖的箭頭,在放大縮小的時候,箭頭的部分會跟著放大縮小,這是我們不想要的,因為縮太小的時候箭頭會看不到,這時候就要用到我們上篇提到的 QStyleOptionGraphicsItem,在 paint 函式裡面,可以用這個東西從 painter 的 transform 裡取得 level of detail (LOD):
qreal qScale = option->levelOfDetailFromTransform(painter->worldTransform());
qreal len = 10 / qScale;
qScale 就是目前放大值,可以用它調整我們要畫的長度 len;這個解答出自 Heresy's Space

下面列一下參考書目:
  • C++ GUI Programming With Qt 4
  • Game Programming using Qt 5 Beginner's Guide: Create amazing games with Qt 5, C++, and Qt Quick

2019年12月11日 星期三

用 Qt Graphics 做一個顯示座標的工具 - 客製化元件

上一篇我們介紹了 Scene, View, Item 的關係,這篇就來客製化一下,畢竟 Qt 的元件沒客製化功能都非常受限,預設行為幾乎什麼都沒有,這時候就是好好重溫 C++ 最美妙功能––繼承的時候了。
首先是 Scene 跟 View,都用繼承的方式建一個自己的 class,才能在裡面實作各種信號跟插槽,設計上 Scene 是 View 的 data member,View 上面接到什麼東西直接 pass 給 Scene,實作就不一一介紹,下面是一個簡單的修改列表,因為是內部用的工具就沒辦法把程式碼貼上來給大家笑了

View實作事件:

  • keyPressEvent:客製化按鍵盤的行為。
  • wheelEvent:連接到放大縮小的函式。


View實作插槽:

  • clearScene:清空畫面
  • addItem:接收讀進來的物件,往下直接呼叫 Scene 的 addItem。
  • zoomToAll 跟 zoomRect:放大縮小。


Scene 實作事件:

  • mousePressEvent/mouseMoveEvent/mouseReleaseEvent:定義滑鼠行為。


Scene 實作信號:

  • rectSelected:滑鼠事件會發出這個信號,通知 View 跟 MainWindow。
  • mouseClick:同樣用來通知 MainWindow 使用者點在哪裡。


當然我們也要客製化自己的 QGraphicsItem,當然如果要顯示的東西沒太多特別要求,只靠 Qt 提供的那些 QGraphicsXXXItem 也是 OK 的。
我記得在實作時候也有考慮過是不是繼承 QAbstractGraphicsShapeItem 就好,後來好像因為什麼原因,還是繼承 QGraphicsItem。

照著 Qt 的說明文件,要實作自己的 QGraphicsItem,重點在打造兩個函式:
QRectF boundingRect() const override
這個函式要回傳一個 QRectF,標示這個 Item 的大概位置,讓 Scene 能用它作分類跟檢索,如果設錯的話就有可能變成 item 明明在某個位置視窗卻打死不顯示它,因為 Scene 在檢索的時候就不認為這個 Item 有需要顯示。
void paint(
  QPainter *painter,
  const QStyleOptionGraphicsItem *option,
  QWidget *widget) override
Paint 就是操作 Painter 裡面的函式盡情的作畫,不過我自己是省得麻煩,都是創一個 QPainterPath 然後呼叫 Painter 的 drawPath,這樣比較簡單。
舉例來說我自己實作的一個物件是在視窗上面標上一個箭頭然後顯示文字,大略來說程式碼就是這樣:
QPainterPath path;
path.moveTo(0, 0);
path.lineTo(0, 8);
path.lineTo(5.656, 5.656);
path.closeSubpath();
path.lineTo(7.65, 18.48); // len 20, tilt 22.5
path.setFillRule(Qt::WindingFill);
QFont serif("Helvetica", 12);
path.addText(QPoint(0, 0), serif, m_text);
paint 裡再用 drawPath 把這條 path 畫出來就可以了。

Paint 的另外兩個參數 QStyleOptionGraphicsItem 和 QWidget,前者帶著顯示上要用的參數,在下篇做一些細部設定的時候會用到;後者則指向目前繪圖中的物件,通常可以不用管它,我也還不確定什麼時候會用到它。
這次實作全面採用 C++ 的新關鍵字 override,個人認為真的好用,像是上面的 boundingRect 如果沒寫 const 的話其實是在實作不同的函式,加了 override 編譯器就會跳錯誤,比較不會犯這種不容易注意到的錯。

實作 QGraphicsItem 以我們的應用這樣就夠了,如果還需要更精細的管理,可以再實作函式:
QPainterPath shape() const
這個函式用來檢查碰撞、滑鼠有沒有點在物件上等等,不實作預設就是以 boundingRect 代替。

自己幹完 QGraphicsItem 之後,整個程式也有了個樣子了,繼承自 QGraphicsItem 的物件可以直接用 addItem 塞進 Scene 裡面,下面截兩張運作起來的樣子,分別是顯示箭頭跟一個三角形的多邊形:



還有很多細部設定沒做所以看起來會有一點粗糙,下一篇預定就是要講這些細部的東西。

2019年12月5日 星期四

用 Qt Graphics 做一個顯示座標的工具

故事是這樣的,平常小弟在公司處理的東西多半是一些 polygon, line 之類的資料,除錯的時候總不能看著 gdb 印出來的 x, y 座標 debug,所以公司同仁有自幹一套 debug 的工具幫忙把這些資料畫出來;不過呢…這套工具好像在記憶體模型那邊有點問題,資料量大的時候會變超慢;畫圖是直接 call xlib,每次放大縮小都要重畫所有物件,對記憶體的負擔又更嚴重。只要 polygon 數量突破幾萬個的時候,一次的 refresh 就會花上好幾秒。

前陣子手上暫時沒其他緊急的事情,乾脆就用 Qt Graphics 重寫一個,不準在留言問我為什麼不用 nodejs 寫,MaDer 公司的工作站就沒有 nodejs。
完工後自己試了一陣發現幾十萬個物件的時候放大縮小都超流暢,不愧是 Qt Graphics,雖然程式行數比我預期的多了些,但架構比本來的東西清楚很多。
其實過程中一直參考強者我同學 AZ 大大的 QCamber,覺得 AZ大大實在太過神猛狂強溫,5-6 年前就寫得出這麼複雜的 project,我一直覺得我寫 code 的時候的整體感很不夠,都是在單一 class 裡面塗塗抹抹,小地方會動可是大架構沒辦法在一開始就訂好,後續要修改的成本就非常高。 Anyway 總之它現有個樣子了,我覺得中間碰過一些實作的問題值得記錄一下,預計可能寫個三篇左右吧。

----

首先圖形顯示的部分,使用的是 Qt 的 graphics framework,可以用來繪製大量的 2D 物件,支援選取、縮放等等,我們這裡只是要顯示而已,也不用搞得這麼複雜。
Graphics framework 裡的三個基本元件就是:
  • QGraphicsScene:場景,可以把它想成一塊巨大的畫布,可以在上面自由放上各種 item,Scene 會幫你管理物件的顯示和更新,個人經驗 Scene 負擔到 10 萬個元件左右還很流暢,上到百萬個的時候就會有點頓了(又或者是我把所有 item 都放在 scene 裡面的關係)。
  • QGraphicsItem:物件,可以想像成畫素描的時候放的那些石膏,在一個場景上擺上東西,Qt 有提供基本的幾種物件:橢圓 QGraphicsEllipseItem、路徑 QGraphicsPathItem、多邊形 QGraphicsPolygonItem、矩形 QGraphicsRectItem跟文字 QGraphicsSimpleTextItem。
  • QGraphicsView:View 是唯一可以在 Qt 的 MainWindow 畫面上出現的物件,可以把 View 想成一台相機,場景 Scene 是不動的,相機從各種角度自由取景,並把取到的景顯示出來,如果取景的尺寸比畫面還要大,跟其他的物件一樣, View 能自動出現捲軸,也可以接收畫面上的滑鼠、鍵盤事件。
只用 Qt 原生的 QGraphicsScene, QGraphicsView, QGraphicsItem 只能組出最基本的顯示工具,變化量非常少,以下就示範一個最基本的設定:
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>

int main(int argc, char *argv[]) {
  QApplication app(argc, argv);
  QGraphicsScene *scene = new QGraphicsScene;
  scene->setSceneRect(0, 0, 400, 400);
  scene->addItem(new QGraphicsRectItem(50, 50, 150, 100));

  QGraphicsView *view = new QGraphicsView;
  view->setScene(scene);
  view->show();

  return app.exec();
}
編譯執行就可以看到這個畫面:

反正這個設計只是一開始建來試驗用的,看一下顯示的效果,很快下一篇就會被我們拆掉了。

2019年9月8日 星期日

從 C 呼叫 Lua 函式

故事是這樣子的,小弟在公司裡面,主要是負責維護一個沒人在用的產品,遠觀來說這個產品滿複雜的,內建兩種不同的演算法實作,為的是要應對不同的狀況,有些狀況用第一種演算法比較快,有些用第二種。
那故事是這樣子的,我們的程式裡面有一個函式會在每筆資料結束之後,用上筆資料的結果來判斷下一次要選哪個演算法,問題是這個函式目前是直接寫在整個引擎的 C code 裡面,於是如果想要改變一下判斷的標準……sorry 重新 build,雖然公司弄了套分散編譯可以編很快但還是要幾分鐘。
上星期自己試了一下,成功把 Lua 編到公司的 code 裡面,就能把判斷邏輯寫在 Lua script 裡面,要改判斷標準只要改 Lua 就可以了;我一般聽到會這樣用的是遊戲公司,因為遊戲一樣涉及大量的邏輯判斷,例如血要扣多少之類的,而遊戲又會大量的去變動這些參數,使得彈性變得非常重要,總不能要改參數就把整個遊戲引擎全部重建構一次……說是這麼說我也不曾證實哪家公司真的這麼做就是,如果我的讀者真的是這樣搞的麻煩留個言讓我知道一下。

總之自己試過之後其實非常簡單,難怪大家都說 Lua 最厲害的就是嵌入到 C 程式當 Sup 角 ,我主要是參考這個網頁(看來是舊金山大學 CS 的課程頁面);下載 Lua 就從官網下載即可,哪一版應該是無所謂,或者是 Linux 的話安裝開發套件也 OK。
不愧是以輕量著稱的腳本語言,連 Makefile 看起來都是手寫的,直接下 Make 讓它編譯完成,雖然說我在這步被環境變數 TARGET_VAR 卡了很久,makefile 不知道為什麼自己把它加到編譯參數裡了。
再來做下面幾件事就能呼叫 Lua 函式了:

1. 引入 Lua 的標頭檔:

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
然後在編譯的時候記得給一下 lua 標頭檔的位置,以及在連結的時候 -llua。

2. 生成 Lua state:

Lua 的 state 包含核心的函式,後面所有的函式都會需要這個 state;在 open 和 close 中間就能呼叫 Lua 函式了:
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// write lua_call ... code here
lua_close(L);

3. 載入檔案,呼叫函式:

使用 luaL_dofile 打開 lua 檔案,等於是呼叫 lua 直譯器執行這個檔案,如果有直接執行的東西這時候就會有效果,像是 print("hello world") ,我們這裡只是定義變數 "add" 對應到相加的函式。
要呼叫 lua 函式,我們以函式 add 為例:
-- add.lua add function
add = function(a, b)
  return 42
end
那麼在 C 裡面就是:
luaL_dofile(L, "add.lua");
lua_getglobal(L, "add");
lua_pushnumber(L, a);
lua_pushnumber(L, b);
lua_call(L, 2, 1);  // 2 parameter, 1 return value
int sum = (int)lua_tointeger(L, -1);
lua_pop(L, 1)
簡單來說就是先用 lua_getglobal 拿到存在 global 裡面的函式 add(在 luaL_dofile 的時候建立的);把參數推到堆疊上;執行 lua_call,指定兩個參數跟一個回傳值,取出回傳值,把回傳值彈出堆疊。
如果函式有多個回傳值的話,會依序放在堆疊的 -1, -2 … 上;lua_pop 也要彈出更多值;Lua 的有一系列的函式來把東西推到堆疊上/彈出堆疊,簡單的應用,通常就是 lua_pushinteger/lua_tointeger, lua_pushnumber/lua_tonumber 來推/彈整數跟浮點數到堆疊上,詳情請參考文件

這裡只記錄最基礎的應用,實際上應該還有更複雜的應用,例如我要做決策的參數如果很多的時候該怎麼辦?或者其實我想要直接推一個 struct 進到 lua 是可以的嗎?
這兩個問題我目前都沒有答案,還有待研究,目前只知道 luajit 這個看起來好像停擺的專案有提供類似的東西,可以準備好 C 的 struct 來吃。
如果有大大們有答案的話麻煩教一下小弟,小弟現在很需要。

2019年2月12日 星期二

關於費式數列的那些事

最近費式數列實在有點紅,讓小弟忍不住也來玩一下。
費式數列給一個初學程式的人都能寫得出來,例如早年我忘了哪位大大在推坑我 python 的時候,就寫了個只要 4 行列出費氏數列的 python 程式,一方面展現 python 在大數運算上的實力,一方面展視了它的簡潔,像是 a , b = a+b, a 這種寫法。
a = b = 1
while b < 1000000000000:
  print(b)
  a, b = a+b, a
當然會寫是一回事,深入進去就沒那麼簡單了,詳細請參考這個網頁
最簡單、最直覺的遞迴寫法,但這其實是會噴射的,每次遞迴都會做重複的計算,於是計算以指數的方式成長,比如說我用 python 的 timeit 去測一個遞迴的費式數列函式,很快執行時間就會爆炸,大概到了 fib(30) 以上就會跑得很吃力了。
如果我們用單純的加法,從 1 開始往上加,其實只要進行 n 次的加法就能得到 fib(n) 了,執行複雜度為 O(n);如果再套用更快的 fast fibonacci,更可以把執行時間拉到 O(lg n) 的程度,只要 fib(94) 就超過 64 bits 的整數的情況下,用 O(lg n) 的演算法其實跟常數時間所差無幾。
不過呢,費式數列還有一個公式解呢,也就是:
$fib(n) = \frac{1}{\sqrt{5}}(\frac{1+\sqrt{5}}{2})^n-\frac{1}{\sqrt{5}}(\frac{1-\sqrt{5}}{2})^n$
為什麼不用這個算式算呢?公式解不是常數時間嗎?

數學上來說:是的,但實際上會遇上一些問題,例如我們看看 64 bits 整數裡面最大的 fib(93) 為例,整數算的解為:
12200160415121876738
如果是 python 寫的公式解呢?
def fib(n):
    return (math.pow((1+math.sqrt(5))/2, n) - math.pow((1-math.sqrt(5))/2, n)) / math.sqrt(5)
print(int(fib(93)))
12200160415121913856
登登,問題大條了,答案不一樣。
何以致此,問題就來到浮點數的不精確問題,這時候就要先來一張經典的漫畫了:
我們在計算完 sqrt(5) 之後,只能用一個近似的值來表達結果,在 python 內預設是以雙精度浮點數在儲存,它跟真正的 sqrt(5) 還是有細微的差距,在隨後的 n 次方、除法上,這個細微的誤差都會被慢慢的放大,最終導致這個巨大的誤差。

幸好我們不是沒有解法的,參考了 C/C++ 版上,傳說中的 Schottky 大大曾經分享如何使用 gmp 或 mpfr 兩個函式庫,算出 e 到小數點下一億位pi 到小數點下一億位,這兩個 gnu 函式庫是所謂的<無限>位數的整數跟<無限>精確度的浮點數,當然他們不是真的無限,只是完全壓榨記憶體來記錄儘可能多的位數以求精確,理論上記憶體撐上去就能把精確度逼上去,只是有沒有那個必要就是,像是把一些無理數算到一億位(欸。
究竟這個函式庫有多麼的強大呢?我們可以來寫個簡單的,例如來算個黃金比例,只要這樣就結束了:
mpfr_t phi;
unsigned long int precision, x=5;
uint64_t digits = DIGITS;

precision = ceill((digits+1)*logl(10)/logl(2));
mpfr_init2(phi, precision);
mpfr_sqrt_ui(phi, x, MPFR_RNDN);
mpfr_add_ui(phi, phi, 1.0, MPFR_RNDN);
mpfr_div_ui(phi, phi, 2.0, MPFR_RNDN);
mpfr_printf("%.10000Rf\n", phi);
mpfr_clear(phi);
唯一要注意的是 mpfr 內部用的 precision 是以 2 進位為底,所以我們在十進位需要的精確度,要先換算為 2 進位的位數,再來就能直接算出 phi 啦,試著算過 50000 位數再對個網路上找的答案,數字是完全一樣的。
這個 library 算得非常快,一百萬位的 phi 也是閃一下就出來了,一億位在我的 64 bit Linux, 2 GHz AMD Ryzen 5 需時 37s,相比 e, pi 這類超越數,phi 只需要 sqrt(5) 真的是非常簡單的了。

扯遠了拉回來,如果我們要用 mpfr 這個函式庫,利用公式解來算 fib(93),要怎麼做呢?
fib(93) 到底有多少位數呢?我們可以用 2^n 作為 F(n) 的上界,最後所需的位數至少就是 ceill(n*log(2)),相對應的我們運算中的浮點數精確度的要求,2^n 這個上界有點可恥粗糙但有用,頂多會浪費點記憶體,最後除出來的小數點後面多幾個零而已,如果能套用更精確的上界當然更好。
mpfr 的函式庫設計精良,呼叫上非常直覺,這段程式碼其實就是寫公式解,應該滿好懂的,程式碼在此。有了這個就可以亂算一堆 fib 了,基本上要算費式數列第一億項 fib(100,000,000) 也是 OK 的(好啦我不保證答案是對的XD,至少 fib(10000) 是對的)。

But,人生最厲害的就是這個 But,公式解真的有比較快嗎?
我個人認為答案是否定的,我們同樣可以用 fast-fibonacci 搭配 gmp 函式庫來計算,因為都是整數的運算可以做到非常快,我的測試程式碼在此

同樣是計算 fib(100,000,000):
formulafib.c: 57.39s user 2.04s system 97% cpu 1:01.09 total
fastfib.c: 4.70s user 0.20s system 75% cpu 6.524 total
O(lg n) 的 fast-fibonacci 遠比<O(1)>的公式解來得快。
問題就在於,到了所謂的大數區域,本來我們假定 O(1) 的加法、乘法都不再是常數時間,而是與數字的長度 k (位元數)有關。而上面我們有提到,基本上可以用 2^n 作為費式數列的上界,也因此費式數列的數字長度 k ~= n,加法、乘法複雜度就會視實作方式上升到 O(n) 跟 O(n^2) 或 O(n lg n) 左右。
在 fast-fibonacci,我們需要做 lg n 次的 iteration,每次三個乘法兩個加減;公式解雖然沒有 iteration,但需要計算兩次次方運算,也等於是 lg n 次的乘法跟加法,然後還有除法,我們運算的又不是整數而是浮點數,這又需要更多的成本,一來一往之間就抵消了公式解直接算出答案的優勢了。

在通常的應用上以及現今電腦的實作,我們還是可以假設整數的加減乘都能在近乎常數時間內結束,這樣我們才能好好討論資料結構與演算法的複雜度,進而把複雜度學好。費氏數列的問題在於,在數字小不用考慮運算複雜度的時候,公式解和 O(lg n) 的 fast-fibonacci 看不出差異,等到 n 終於大到看得出 O(lg n) 跟 O(1) 的差異時,已經要把運算複雜度納入考量了。
理論上我們當然可以假設有個計算模型,無論有多少位的數字,無論浮點數有多少精確度要求,四則運算與次方都能在常數時間內結束,這時公式解就能來到 O(1),但這樣的假設不像停機問題假設的萬能機器,在學術討論上看來不太有意義。
利用 gmp, mpfr 這樣的函式庫,插滿記憶體甚至把硬碟當記憶體來用、把記憶體當 cache 用,浪費幾個星期跟一堆電力,我們可以把無理數算到小數點下一億位、十億位,這是前人們精心為我們建的巨塔,可是數字還是無窮無盡,站在巨塔上反而才看得出我們跟無限有多麼遙遠,誠然人腦可以透過思考一窺數學之奧妙,但不代表我們能超脫數學的嚴格限制浮空而起,妄想記錄無限,我認為是對數學的一種褻瀆。

看了這麼多碎碎念大家想必也累了,總而言之本文透過兩個實作,讓大家體會一下所謂 O(1) 公式解並不一定是 O(1),背後一定有對應的成本;還有就是把費式數列算到一億位真的有點爽,不過我想是沒什麼公司在實務上有在賣 fibonacci 相關的產品啦,除非你想像日本一樣出個寫滿 e, pi 到一百萬位的書讓人當亂數表來用。

2019年1月6日 星期日

用 PEG 寫一個 C parser 續

自從去年十月把 nixie tube clock 完工之後,好像都在耍廢之類的,結果 11/12 月兩個月都沒有發文,其實這兩個月裡面,有的時間都在改之前寫的 C parser,其實整體完成度愈來愈高了,今天發個文來整理一下到底做了啥。
這次做了幾個改變,主要的修正就是加上 expression, declaration, statment 的處理,也學到不少東西,這裡一一列一下:

macro

這是要對應之前寫的 parse_fail,本來 parse_fail 的用意,就是在剖析出錯的時候,把程式終結掉,然後丟一點錯誤訊息出來;本來我的實作是一個函式,利用 unreachable! 丟出錯誤:
fn parse_fail(pair: Pair<Rule>) -> ! {
  let rule = pair.as_rule();
  let s = pair.into_span().as_str();
  unreachable!("unexpected rule {:?} with content {}", rule, s)
}
這樣的實作會有個問題,在程式終止之後的位置一律都會在 parse_fail 這個函式裡,而不是真正出錯的剖析函式,要除錯必須開 stack trace 才能做到。為了避免這個狀況,我們改用 macro 實作 parse_fail,這樣 unreachable! 就會在出錯的位置展開,在終止程式的時候給出正確的位置。
關於 macro 小弟在很早的時候有寫過一篇貧乏的介紹文,改起來也很簡單,把原本作為函式參數傳進來的 pair,由 macro 的 $x:expr 取代,然後用 $x 取代本來 code 裡面所有的 pair,如下文:
macro_rules! parse_fail {
  ( $x:expr ) => {
    {
      let rule = $x.as_rule();
      let s = $x.into_span().as_str();
      unreachable!("unexpected rule {:?} with content {}", rule, s)
    }
  }
}
這樣所有程式裡的 parse_fail(pair) 就會自動開展成下面的三行程式碼了。

另外有一個要注意的是,假設我 parse_fail 的 macro 寫在模組的 helper.rs 裡面,那麼在寫模組的 lib.rs 時,mod helper 要在所有其他 mod 之前,並加上 #[macro_use] 修飾,這跟 rust 模組的編譯流程有關,macro 是跟順序有關的,在 mod helper 之後這個 parse_fail 的 macro 才有定義,後面的 mod 才能使用這個 macro,詳細可以參考這篇

如果想讓使用 extern crate 的人也能使用這個 macro,就要在定義 macro 的時候在前面加上 #[macro_export] 的標籤,每個 macro 都需要單獨 export 才行。

處理 expression 的正確姿勢:

如果有看上一篇,會看到我用 PEG 套件 pest 的 precedence climbing 的功能來完成對 expression 的剖析,但其實那是不完整的,原因在於我們把的做法是把 expression 直接導向 unary_expr (op_binary unary_expr)* 的組合,這樣我們看到 expression,把它展開來就可以得到一大串 unary_expr 跟 op_binary 交錯的序列,把這串東西丟進 precedence climbing 裡面就能建好 expression tree 了。
但實際上的 C 語言比這還要複雜,expression 下面還有 assignment expression,conditional expression 等等,這些 expression 是必須存在的,例如在變數 decl 的地方就會需要 assignment expression,我們本來的寫法把 assignment expression 等等都抹掉了要怎麼辦?把它們加回去要怎麼讓本來的 precedence climbing 的 code 還能正確運作?
後來發現的正確處理方法是這樣的,在文法的部分要把 assignment expression 等東西加回去,裡面用到的三元運算子 ?: ,assignment operator =, +=, -= 等等都從 op_binary 裡面排除,像是這樣:
logicalOR_expr = _{ unary_expr ~ (op_binary ~ unary_expr)* }
conditional_expr = _{ logicalOR_expr ~ ( op_qmark ~ silent_expression ~ op_colon ~ conditional_expr)? }
assignment_expr = _{ (unary_expr ~ op_assign)* ~ conditional_expr }
silent_expression = _{ assignment_expr ~ (op_comma ~ assignment_expr)* }
expression = { assignment_expr ~ (op_comma ~ assignment_expr)* }
原本的 expression 現在只剩 logicalOR_expr,其他的都要拉出來自立條目,讓其他的文法如 declaration 能使用它,但同時都使用 _{} 讓剖析後他們不會吐一個 node 出來,這樣看到 expression 之後,展開來仍然是一串 unary_expr 跟 operator 交錯的序列。

這樣做的好處是 precedence climbing 仍然可以沿用,所有的 operator 都算在 expression 頭下,壞處是我們必須依文法去調整一些文法要不要吐出 node,現在的實作在有兩個特例:
一個是如上面所示,conditional expression 的規定是 logical_OR_expression ? expression : conditional_expression,有一個 expression 在裡面,這會違反我們的假設:把 expression 展開來看都會是 unary_expr 跟 operator 的組合,因此我們要加上一個特別的 silent_expression 在剖析完之後不會生成 expression node ,而是完全展開。

另一個剛好是反過來的狀況,在 C 的 initializer 文法(6.7.9)是這樣定的:
initializer -> assignment_expression | "{" initializer-list "}"
但…我們的 assignment_expression 是不存在的,如果 initializer 真的剖析為 assignment_expression,展開 initializer 只會得到「一團 unary_expr 跟 operator 的組合」,會跟 initiailizer-list 搞在一起,所以反過來我新增了一個 initializer_expr,把 assignment expression 封起來:
initializer_expr -> assignment_expression
initializer -> initializer_expr | "{" initializer-list "}"
這樣拿到 initializer 就能放心展開,再看內容物是 initializer_expr 或 initializer-list 來決定下一步,如果是 initializer_expr 就能放心的丟給 precedence climbing 去建 expression 了。

上面兩個例子都沒什麼道理可言,基本上就是見招拆招,大致就是兩條好像在說廢話的規則:
  1. 會展開的 rule 裡面出現這團 rule 的開頭,則開頭的 rule 代換成自動展開的版本。
  2. 會展開的 rule 跟其他 rule 並列,要再多包一層不會展開的版本。

! tag for = and ==

在這次修改之前都沒什麼機會用到 ! tag,也就是 PEG 裡的 Not predicate,這次在處理更複雜的 expression 遇到,某些狀況 = 的優先權高過 == 以致 == 先被剖析成 = 了。
這時候 op_assign_eq 就要改為:
op_assign_eq = { "=" ~ !"=" }
來確保 = 之後沒有接著其他的 =。

comment

comment = _{ "/*" ~ (!"*/" ~ any)* ~ "*/" | "//" ~ (!"\n" ~ any)* ~ "\n" }
comment 也是這次的修改之一,同樣利用了 ! 的特性,上面兩條其實都滿直覺的:
開頭是 /* 再來只要不是 */ 的內容,就可以匹配任何字元;開頭是 // 再來只要不是換行就可以匹配任何字元。
這兩個例子都使用了 Not predicate,功能很類似 C 裡面的 peek,偷看一下後面的東西而不消耗任何東西。

Hidden grammar:

這點比較不是程式的問題,而是 C 規格的問題,注意以下這些都符合 C grammar,但在工作上千萬別這麼寫,大概有十成的機率你會被電到天上飛
  • volatile, restrict, const 隨便加,加幾個都沒關係
  • 其實可以不用 type,這是符合文法的,gcc 在這裡會直接給你一個 int。
  • 也可以宣告型別,儲存類型什麼的,最後…沒變數。
所以可以寫像是:
int const volatile const volatile const volatile const volatile const volatile const;
const * restrict restrict restrict a;
說真的,看到這樣寫 code 我也會把人電到天上飛,其實我也不知道為什麼 C grammar 要允許這樣的文法就是,看到 gcc 編譯過我差點笑死。

現在離大致完成還有一個最大的難關,就是 declaration 那邊還有 struct, union, enum 等著處理,文法上是還好,更大的問題是不知道怎麼寫轉出來的 AST,之前我大部分都參考強者我學長 suhorng 大大的 haskell 實作,或者參考一些 LLVM 的 IR 實作…當然是沒辦法到 LLVM 那麼複雜啦QQ。
總之最近進度嚴重卡關,這才是我為什麼在這裡打住寫篇文的原因(誒。

自己自幹 AST,配上最近工作上做的一些改動,讓我有了下面這個體會:
資訊源自於數學,本身是無窮的,正如數線上有無數的正整數,無窮的有理數,比無窮更無窮的無理數;數學這個「概念」本身就有無限的資訊
但有了電腦一切就不一樣了,我們只有有限的位元能夠近似數學的概念,所以就有了取捨。
用 64 位元可以表示到 18446744073709551615,大約是 10^19,於是 10^19 -> ∞ 的資訊就被捨去了;同理我們決定浮點數用 IEEE 754 表示,有些小數就是無法表示,無窮的資訊對上有限資源,其間的差距令人絕望。

就如我們把 C code parse 成 AST,AST 裡面要保留多少資訊?像我這樣基本上只保留了簡單的 AST node,隨便建顆樹而已;LLVM 的 IR 就是許多嚴僅設計的物件,保留程式語言的繼承關係跟內部的屬性設計,在處理上就有更多能運用的資訊。
工作上需要的是用電腦處理幾何的資訊,像是點、線、四邊形,那麼一個線段的物件要儲存什麼資訊?可以用起點終點來表示一條線,基於效率跟空間考量,我們可能可以存一下線段是不是垂直的、水平、甚至是不是斜上跟斜下,但要不要存一個 double 的斜率呢?這就要看平常是不是很常需要算斜率了。存更多的東西自然可以方便做些處理,但線段更新時也要更新更多的資訊。

捨去是面對資訊時的必要,資訊工程處理的問題一直都不是資訊太少而是資訊太多,而要捨去什麼資訊、保留什麼,這不是科學而是技藝,這些都不是數學,不會有一個標準的答案,而是視需求去選擇,需要經驗、工具、模擬、除錯、測試……用實驗跟說理得到一個最佳近似的解;正如大學學系的名字:資訊<工程>學系。

2018年8月25日 星期六

用 PEG 寫一個 C parser

故事是這樣子的,之前我們寫了一個自創的程式語言 Simple Language ,還用了 Rust 的 pest 幫他寫了一個 PEG parser,雖然說它沒有支援新加入的函式等等,本來想說如果年底的
MOPCON 投稿上的話就把它實做完,結果沒上,看來是天意要我不要完成它(欸

總而言之,受到傳說中的 jserv 大神的感召,就想說來寫一個複雜一點的,寫個 C language 的 PEG parser 如何?然後我就跳坑了,跳了才發現此坑深不見底,現在應該才掉到六分之一深界一層吧 QQQQ。
目前的成果是可以剖析 expression 並依照正確運算子順序處理,還有部分的 statement 也能正常剖析,因為通常能處理 expression 跟 statement 剖析裡麻煩的工作就完成 50 %,決定寫小文介紹一下,啊不過我還沒處理 expression 出現轉型的時候該怎麼辦,array reference 出現要怎麼辦,所以這部分可能還會改。
本來專案想取名叫 rcc 或 rucc,但這樣的專案名稱早就被其他人的 rust c compiler 用掉了,因為寫 Rust 的人好像都很喜歡用元素來當專案名稱,這個專案的名字就叫 Carbon 了XD。

其實寫這個跟寫 simple parser 沒什麼差,只是語法更加複雜,複雜之後就容易寫得沒有結構化;PEG 有個好處是在處理基礎的結構的時候,比起用 regular expression 各種複雜組合還要簡潔,像是 floating point 的時候,這是 C floating point 的語法,雖然這還不含 floating point 的 hex 表達式,但不負責任的臆測,要加上 hex 的支援只要多個 (decimal | hex) 的選擇就好了:
sign = { "+" | "-" }
fractional = { digit* ~ "." ~ digit+ | digit+ ~ "." }
exponent = { ("e" | "E") ~ sign? ~ digit+ }
floating_sfx = { "f" | "F" | "l" | "L" }
floating = @{ fractional ~ exponent? ~ floating_sfx? }

不過脫離基本結構,開始往上層架構走的時候麻煩就來了。
我的感想是,在大規模的語言剖析上 PEG 其實不是一個很好用的剖析方式,寫起來太隨興,沒有一套科學分析的方法告訴你怎麼樣寫比較好,甚至怎麼寫是對的,就連 C standard 的寫法都是用 CFG 可以處理的格式寫的,PEG 畢竟比較年輕才沒人鳥它;在傳統 CFG,List 就是用 List -> List x 那套來表達,在 PEG 裡卻可以直接把剖析的文法用 +, * 重複,層數相較 CFG 可以有效扁平化,相對應的壞處是很容易寫到壞掉,目前為止花了很大的心力在調整語法各部分的結構,非常耗費時間。

例如在剖析函式的內容,C 語法大概是這樣定義的:
compound_statement -> block_list
block_list -> block_list block
block -> declaration_list | statement_list
declaration_list -> declaration_list declaration
statement_list -> statement_list statement

上面這段大致體現了 declaration 跟 statement 交錯的寫法,一開始寫的時候,直譯就翻成
compound_statement -> block*
block -> declaration* ~ statement*

很直覺對吧?但上述的語法會直接進到無窮迴圈,上下層兩個連續的 * 或 + 是 PEG 語法的大忌,當上層跳入 * 無窮嘗試的時候,下層就算沒東西 match 了,照樣可以用 * 的空集合打發掉;同理上層是 + 下層是 * 也不行,理由相同;真正的寫法是上層用 * ,下層用 + ,在沒東西的時候由下層回傳無法匹配讓上層處理。
這個例子最後的寫法其實是這樣:
compound_statement -> block*
block -> (declaration | statement)+

或者是這樣,反正轉成 AST 之後也沒人在意 block 到底是只有 declaration 、只有 statement 還是兩個都有,乾脆把所有 declaration 跟 statement 都攪進一個 block 裡:
compound_statement -> block
block -> (declaration | statement)*

這個例子很明顯的體現出 PEG 文法的問題,透過文法加上 ?+*,我們可以很有效的把本來的 list 打平到一層語法,但連接數層的 +* 就需要花費時間調解層與層之間的融合,是一件複雜度有點高的事情。
很早之前在看參考資料的時候有看到這句,現在蠻有很深的體會:「我覺得用 PEG 類工具可以很快的擼出一個語法,是日常工作中的靠譜小助手,但要實現一個編程語言什麼的話,還得上傳統的那一套。」(註:原文為簡體中文此處直翻正體中文)
像是我的 simple parser 跟 regex parser,用 PEG 寫起來就很簡明,一到 C 這種複雜語言就頭大了;CFG 那邊的人大概會指著 PEG 的人說,你們 PEG 的人就是太自由了,哪像我們當年(誒

剖析寫完再來還要把文法轉譯成 AST。
在實作上大量參考強者我學長 suhorng 大大的 haskell C compiler 實作,想當初跟學長一起修 compiler,那時候我還很廢(其實現在也還是很廢),光是寫 C lex/yacc 能把作業寫出來不要 crash 就謝天謝地了,然後學長上課的時候 haskell 敲一敲筆電就把作業寫完了 QQQQ。

用 rust 的 pest PEG 套件寫轉換的程式,大部分都是 match rule ,看是哪種 Rule 之後去呼叫對應的函式來處理。
在expression 的部分可以直接使用 pest 提供的 precedence climbing 功能,無論是文法或建 AST 都很簡單,文法甚至可以收到一行,因為 expression 都是一樣的格式:
expression -> unary_expr (binary_op unary_expr)*
再到 precedence climbing 為所有 op 分出順序,就像 climb.rs 裡面那壯觀的 C operator 優先次序:
fn build_precedence_climber() -> PrecClimber<Rule> {
  PrecClimber::new(vec![
    Operator::new(Rule::op_comma,    Assoc::Left),
    Operator::new(Rule::op_assign_add,   Assoc::Right) |
    Operator::new(Rule::op_assign_sub,   Assoc::Right) |
    Operator::new(Rule::op_assign_mul,   Assoc::Right) |
    Operator::new(Rule::op_assign_div,    Assoc::Right) |
    Operator::new(Rule::op_assign_mod,  Assoc::Right) |
    Operator::new(Rule::op_assign_lsh,    Assoc::Right) |
    Operator::new(Rule::op_assign_rsh,    Assoc::Right) |
    Operator::new(Rule::op_assign_band, Assoc::Right) |
    Operator::new(Rule::op_assign_bor,    Assoc::Right) |
    Operator::new(Rule::op_assign_bxor,  Assoc::Right) |
    Operator::new(Rule::op_assign_eq,     Assoc::Right),
    Operator::new(Rule::op_qmark,    Assoc::Right) |
    Operator::new(Rule::op_colon,    Assoc::Right),
    Operator::new(Rule::op_or,       Assoc::Left),
    Operator::new(Rule::op_and,      Assoc::Left),
    Operator::new(Rule::op_bor,      Assoc::Left),
    Operator::new(Rule::op_bxor,     Assoc::Left),
    Operator::new(Rule::op_band,     Assoc::Left),
    Operator::new(Rule::op_eq,       Assoc::Left) |
    Operator::new(Rule::op_ne,       Assoc::Left),
    Operator::new(Rule::op_gt,       Assoc::Left) |
    Operator::new(Rule::op_lt,       Assoc::Left) |
    Operator::new(Rule::op_ge,       Assoc::Left) |
    Operator::new(Rule::op_le,       Assoc::Left),
    Operator::new(Rule::op_lsh,      Assoc::Left) |
    Operator::new(Rule::op_rsh,      Assoc::Left),
    Operator::new(Rule::op_add,      Assoc::Left) |
    Operator::new(Rule::op_sub,      Assoc::Left),
    Operator::new(Rule::op_mul,      Assoc::Left) |
    Operator::new(Rule::op_div,      Assoc::Left) |
    Operator::new(Rule::op_mod,      Assoc::Left),
  ])
}

match 之後一定有些規則是無法處理的,例如 match statement 的時候就不用管 op_binary 的 rule,這裡我寫了一個函式來承接這個規則,Rust 函式的 ! 型別相當於 C 的 noreturn,已經確定這個還是不會回傳值了,印出錯誤訊息後就讓程式崩潰;這個函式就能在任何 match 的 _ arm 上呼叫。
fn parse_fail(pair: Pair<Rule>) -> ! {
  let rule = pair.as_rule();
  let s = pair.into_span().as_str();
  unreachable!("unexpected rule {:?} with content {}", rule, s)
}
這樣這個函式就能出現在任何地方,例如 match 當中,每一條分支都應該要得到同樣的型別,不過這個函數可以是例外,畢竟它確定不會再回來了:
match (rule) {
  Rule::op_incr => Box::new(CastStmt::StmtPostfix(CastUnaryOperator::INCR, primary)),
  Rule::op_decr => Box::new(CastStmt::StmtPostfix(CastUnaryOperator::DECR, primary)),
  _ => parse_fail(pair),
}
我自己是覺得,把 PEG 文法剖析出來的結果轉換到 AST 上面,麻煩程度差不多就跟寫一個 recursive descent parser 差不多,而且用了 PEG 套件很難在使用者給了不正確程式時,給出有意義的錯誤訊息,我用的 pest 最多只能指著某個 token 大叫不預期這個 token ,預期要是哪些哪些。
到頭來要給出好一點的錯誤訊息跟發生錯誤的回歸能力,也許還真的只能像 gcc, llvm 一樣,直接幹一個 recursive descent parser。

不過以下是我不負責任的想法,我我暗暗覺得 PEG 的語法和 recursive descent parser 之間應該有某種對應的關係,也就是說,如果設計好 PEG ,應該能給出一個不錯的 recursive descent parser 的骨架,搭配使用者設計好在哪些文法遇到哪些錯誤該如何處理函式群,生出一個 recursive descent parser 來;不過以上只是不負責任的臆測,請各位看倌不要太當真。

這個專案其實還在草創階段,還有超多東西沒有支援(其實連個像樣的功能都沒有吧...),各位看倌大大手下留情QQQQ

下一步我也還沒想好怎麼做,之前有看到 rucc 的專案,是直接使用 rust 跟 LLVM 組合的套件,把剖析的程式碼直接轉成 LLVM IR,也許可以參考這種做法也說不定。


2018年7月21日 星期六

開始使用 Google Test:基本設定

故事是這樣子,最近突發奇想用一些零碎時間寫了一個 C++ 的 regex project,因為已經好久沒有寫 C++ 都在寫 Rust,回鍋發現 C++ 怎麼可以廢話這麼多,長得又醜,以後哪個人再跟我說 Rust 的的生命週期很醜的,我就叫你去看 C++ 的 template code,看你還敢不敢再說 Rust 醜。
扯遠了,總之這次寫的 C++ 專案,其實只是當個練習,看能不能藉由實作專案熟悉 C++ 11、14的功能,也決定引入 CMake 和 Google test 等等我之前一直都沒有學會的東西,從做中學這樣。

這篇先來介紹一下 Google test 超基礎設定,詳細請參考官網。

當我們把程式準備好,通常會自己偷懶寫一個 main,然後在裡面呼叫所有測試用的函式,測試就是手動執行編譯出來的執行檔,但是這樣做的問題不少:諸如缺乏足夠的 assert 跟框架支援,測試很難擴張,沒有量化多少測試通過等等(有時也會重新發明這些輪子來測試就是XDD)。
這當然是個問題,所以很多公司或單位都推出測試框架,讓寫測試變成一件愉快大家都願意做的事,Google Test 就是其中一個,看一看覺得不難寫就決定用了,同樣在使用 Google Test 的包括像 Google 自己的 chromium,LLVM 編譯器等等;其他的測試框架像是強者我同學 JJL 推的 catch2。
要使用 Google test 的第一步是先安裝,Linux 只要用套件管理員安裝即可,Windows 的話我救不了你,請到 Google test Github 網頁看編譯教學。
接著我們要設定專案,首先寫一個測試,如果是自幹一個主程式的話,通常會像這樣:
void test_something();

int main(int argc, char *argv[])
{
  test_something();
  return 0;
}

void test_something() {
  assert(true);
}
執行下去沒噴掉就是測試通過了,我知道可能有些人在暗自竊笑:怎麼可能有人這樣子寫測試程式,不過真的很對不起我以前真的就這樣子寫測試,為了要測不同的東西還分成許多執行檔,導致 Makefile 裡面一堆項目,各種雷,請大家叫我雷神王。
不過如果你已經這樣子寫測試了,把它改成 Google test 也是相當簡單的事情;每一個測試的函式會對應到Google test裡面的 TEST,用 TEST(test_suite_name, test_case_name) 代替;主程式會由 Google test 生成不用寫,整個測試會修改成這樣:
// include google test
#include "gtest/gtest.h"

TEST(testSuite1, test_something) {
  EXPECT_TRUE(true) << "This should not fail";
}

變得簡短很多,不需要再維護我們有多少個測試,把測試寫到主程式也省掉,只需要維護好每一個單一測試即可。
Google 測試提供一系列的 assert 跟 expect 兩種測試用的函式,兩者的差別在於 assert 會直接將中止程式,expect 只會顯示錯誤之後繼續執行測試;基本上可以的話用 expect 就對了,在一次測試中回報盡可能多的結果。
只要是能夠透過 ostream 輸出的內容,都可以用接續在 assert 跟 expect 後面,作為失敗時的輸出。
Assert 跟 Expect 完整的列表可以在 Github 上 Google test 的文件找到。

再來我們就可以來編譯了,如果你是用 Makefile 的話,在編譯的時候加上 Google test 安裝路徑,在連結的時候 -l gtest 即可。
https://gist.github.com/mawenbao/9223908

如果是用 CMake 的話我,是參考這個連結
# Google Test
add_executable(unittest unittest.cpp)
enable_testing()
target_link_libraries(unittest gtest gtest_main)
add_test(unittestSuite unittest)

我們要產生一個新的執行檔 unittest,在 link library 加上 gtest 跟 gtest_main,CMake enable_testing 我也不確定是什麼意思,老實說我覺得 CMake 對我來說有一點複雜到不透明了,我很難理解每一個 command 是在做什麼,要做到什麼功能需要加上什麼 command,如果不 Google 的話也很難知道怎麼寫,有一種在寫 javascript 的感覺(誒

編譯完成之後就會出現 unittest 這個執行檔,此時再執行即可:
[==========] Running 3 tests from 1 test cases.
[----------] Global test environment set-up.
[----------] 3 tests from testDFA
[ RUN ] testDFA.test_dfa_rulebook
[ OK ] testDFA.test_dfa_rulebook (0 ms)
[ RUN ] testDFA.test_dfa
[ OK ] testDFA.test_dfa (0 ms)
[ RUN ] testDFA.test_dfa_design
[ OK ] testDFA.test_dfa_design (0 ms)
[----------] 3 tests from testDFA (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test cases ran. (1 ms total)
[ PASSED ] 3 tests.

祝大家 google test 愉快,我相信隨著我 project 變大,很快的我會遇到更多 CMake 跟 Google Test 的用法,到時候再整理出來發文了。

2018年3月18日 星期日

實用的gdb 指令

最近工作上大量使用到 gdb,想說來整理一下一些常用的 gdb 使用方式,以及對應的場景;當然,這絕對不是 gdb 完整的功能介紹,只是目前我遇到比較多的使用方式而已。

檢視原始碼:
gdb 的文字介面可以顯示四種視窗:命令,原始碼,組合語言跟暫存器,與視窗相關的鍵盤組合:
Ctrl + x + a 它會分成上原始碼下命令兩個視窗
Ctrl + x + o 切換焦點所在的視窗,如果焦點視窗放在原始碼那邊,命令視窗的一些鍵會無效,例如上下鍵會變成原始碼視窗的上下瀏覽原始碼,得用 Ctrl + P (previous) 跟 Ctrl + N (next) 才能瀏覽歷史命令。
使用Ctrl + x + 1,Ctrl + x + 2 可以設定原始碼視窗分為一個或兩個,連續 Ctrl + x + 2 會依序在原始碼+組合語言、組合語言+暫存器、暫存器+組合語言三種組合中切換。
這功能可以直接看到現在執行到哪裡,特別是進到除錯熱區時,gdb 用 list 指令都會自動加10 行,要印出當下除錯的程式總有點難度,用這個可以徹底排除這個問題;不過我也覺得還沒到除錯熱區的時候,配合編輯器來看原始碼即可。

快捷鍵(這個不止是 gdb,而是大部分 shell 都適用):
除了上述的 Ctrl + p, Ctrl + n,Ctrl + l 可以清空目前的 buffer,Ctrl +w, Ctrl + u 可以刪除命令列上一個單字或全部內容,也滿好用的。特別是 gdb 裡面 shell 下清空 buffer 的指令 clear 沒有辦法用,Ctrl + l 使用機會滿大的(前提是原始碼視窗沒開)。

中斷點設置:
有關gdb 裡面的四個點:breakpoint, watchpoint, catchpoint跟tracepoint,到現在我也只用過前兩個,99 %都是 breakpoint:
breakpoint 又分為永久跟暫時,對應 break/tbreak 後面接要中斷的位置,可以用函數名稱或者 <sourcefile>:<linenum> 兩種格式。
還可以在 breakpoint 的後面加上條件,例如 break <function> if <var> == 10,就能開始 debug 某個狀況下的執行,這在除錯有迴圈或多次呼叫函式的程式時非常好用。
start 指令,自動設置 temporary breakpoint 在主程式開始處,C/C++ 就是 main。
Watchpoint 則是用watch/rwatch 指令設置,可以監看一個變數什麼時候被動過,指令監看的目標可以是變數、記憶體位置(如 watch *0x12345678)或是幾個變數的運算也可以。
watch、rwatch、awatch 分別監看變數寫入、讀取跟讀寫。

無論是breakpoint watchpoint其實都是… breakpoint(watchpoint 有時叫 data breakpoint),可以用 info break 看到所有相關設定,還有例如它已經碰到幾遍之類。
有一個相關的技巧如下,可以計算某個函數究竟遇到幾次:
break foo
ignore <breakpoint num> 100000 //大到遠超過可能執行的數量
continue
等程式執行完就能用 info break 看到這個 breakpoint 被碰幾次。

另外 debug 時,可以利用save breakpoints <filename>把設好的breakpoint 存到檔案裡,下次可以直接source它們,save 的 breakpoint 最好只設在函數名稱上面,行數可能在debug 時變化,下次source 時就會break在不同的地方了。
我習慣上會存成 gdbinit 這樣的檔案,一進 gdb 時 source gdbinit 即可。

修改執行:
執行中可以用
set var <variable name>=<value>
set <memory location>=<value
的方式修改變數/記憶體位址內容。
例如我們發現某個條件被跳掉會導致錯誤結果,可以在 gdb 內用 set 修改變數,使它符合條件,省去重新編譯的麻煩(甚至一些狀況下,修改程式可能動到本來的邏輯,或很難使它符合條件)

跳出函數執行:
指令 finish,執行直到離開當下函數(當然遇到 breakpoint 還是會被擋)。

介面
上面的功能,有些因為最近比較常用 ddd 而非 gdb,也就沒有常用到。
另外強者我同學強強林有推一款 web-based 的 gdb gui:https://github.com/niklasb/webgdb
,據說 vscode 的 debug 功能也滿生猛的,也許找時間都來試試看。

2017年5月27日 星期六

使用 doxygen 產生程式文件

這應該是個過時的題目,相關的文件已經滿天飛了,不過最近程式寫得肥了,又是和其他人的合作項目,總不能老是這樣打開 Office Word 寫文件(掩面),然後程式跟文件老是不同步,還是把註解改好用 doxygen 產生文件省事些,順帶得就寫個文章記錄一下。
doxygen 是一套文件產生程式,會自動 parse 原始碼、標頭檔等,搭配設定的模版,自動產生不同種類的文件,如 manpage, html , latex…。
要用 doxygen 第一步是先安裝 doxygen,這在 unix 下配上套件管理員應該跟喝水一樣簡單,略過不提。

doxygen -g <config-file>
產生設定檔,預設的名字會是 Doxyfile

下一步要編輯設定檔,我比較過預設設定,大概要改下面這些地方:

  • PROJECT_NAME:當然要改成自己的名字
  • OUTPUT_DIRECTORY:我設定在 doc 資料夾,並且把 doc 資料夾加到 .git 中
  • OUTPUT_LANGUAGE:設定語言
  • EXTRACT_ALL:設成 YES,這樣才會解析所有的檔案,否則必須要用 \file 或 @file 定義過的檔案才會被解析,否則它只會解析 .h 檔
  • INPUT:用空白分隔的輸入檔案或資料夾,我是 src 資料夾
  • FILE_PATTERNS:設定要分析的檔案,這裡我只保留 .c 跟 .h
  • EXCLUDE:把不需要的資料夾剔掉,因為我有一個測試的 test 資料夾,所以把它加上去
  • GENERATE_*:設定要輸出的格式,我只選擇輸出 html ,設定 GENERATE_HTML 是 YES

為了在程式中加上更多資訊,可以在程式碼裡面為函式寫註解,我是使用下列的型式:
/*!
 *
 */
另外搭配以下幾個註解命令:
\brief 可以提供一行描述,簡短敘述這個函式的功能
\see 關鍵字,可以產生連結跳轉去其他的內容
\param 參數描述,有符合原始碼內的參數名稱,在排版上會自動上色並縮排
\return 對回傳值的描述
在 \brief 之後空一行,其它的內容則會歸為長敘述中,如果沒空行的話這些內容都會被濃縮成一行歸在 brief 裡面,所以,一個完整的函式註解如下:
/*!
 *
 * \brief dest = src xor dest
 *
 * do xor on src buffer and dest buffer, store result in dest buffer
 * \param src, dest buffer to xor
 * \param len buffer size both buffer should have enough space for len
 * \return none
 *
 */
int32_t xor2(
  uint8_t *src,
  uint8_t *dest,
  uint32_t len)

接著就可以下
doxygen config_file
產生文件,文件會在 doc/html 中
話說新一代的程式語言好像都自帶文件產生器,如 golang 的 godoc,rust 也有 rustdoc,也許 doxygen 這樣的東西,未來也用不太到了?

Doxygen Manual:
http://www.stack.nl/~dimitri/doxygen/manual/index.html

2017年4月25日 星期二

Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14

中譯書名是「Effective Modern C++ 中文版:提昇C++11與C++14技術的42個具體作法」

包括前兩本 effective C++ 跟 more effective C++,其實這已經是作者第三本關於 C++ 的書了,我真的滿好奇作者哪來對 C++ 這樣無比熱情,因為我已經很久很久沒有寫 C++ 了,有關底層的東西大部分還是用C,雖然有時候遇到沒有STL 支援的資料結構還是會哀一下,其他程式大多用Python 來寫;寫最多 C++ 的地方大概是 leetcode XD,想練演算法又想貪圖 C++ STL 提供的好處;大型 C++ 的譔寫經驗好像也沒多少,所以說我到底為什麼要看這本書呢……(思

扯遠了,先拉回來。

總而言之,C++ 在 11 跟 14 加入了許多新的語法和功能,這本 Effective Modern C++ 包含42個C++ 的使用項目,分佈在下面7個主題當中:
  • Deducing Types:C++ 的template 是如何推導型別 
  • auto:何時該用 auto ,何時又會出錯 
  • Moving to Modern C++:C++ 11,14 引入的新的寫法 = deleted, = override, 自動產生的物件方法
  • Smart Pointers:推薦使用smart pointer 代替 raw pointer ,以及 smart pointer 背後的實作 
  • Rvalue References, Move Semantics, and Perfect Forwarding:C++ 右值的 move, forward 的使用。 
  • Lambda Expressions:如何正確使用 C++ lambda 語法 
  • The Concurrency API:C++ 提供的 thread 跟 future 要在哪些情況才能正確使用 
  • Miscellaneous:其他兩個,介紹 emplace 何時要用 copy-by-value 
我是覺得到了 concurrency API 就已經看不太懂,平常大概也不會用到這樣,不然請大家回想一下你上次用 C++ 寫非同步的程式是什麼時候XD,但前面幾章就還看得滿有感覺的。

整體來說,C++ 從開始發跡到 C++11, 14,零零總總經歷各種不同的設計,無論是文法上或執行效率的考量,新增了很多全新的建議寫法或者關鍵字,每個關鍵字的背後可能都是一段故事,就例如存在已久的 typename :
http://feihu.me/blog/2014/the-origin-and-usage-of-typename/

書裡每個項目都包含從C++98或 C++11 開始到C++14最新的設計範例,作者還會告訴你為什麼C++11和C++14要這樣設計這些標準,哪些用法會在造成不預期的結果,或者直接了當的編譯錯誤,我認為,學習 C++ 而不去了解這些故事,等於是錯過一段精彩的設計思辯,非常可惜。

所以,還是回到那些話:學習C/C++的問題並不是學不會什麼生猛功能,而是要在各種實作的方法中學習:「哪些方法是比較<適合>的方法,避免哪些有問題的寫法,比較好的方法和概念是什麼?以及為何如此」

還在感嘆自己不懂C++ 11 跟C++ 14?趕快到書店買本 effective modern C++ 吧

不過告訴你一個悲慘的事實,今年C++又要推出更新的C++17了,希望今年Scott Meyers 也能再出本新書(More effective modern C++ (誤))幫大家指點迷津。

末語只能說:人生很難,可是C++,更難

2017年4月8日 星期六

Makefile header 檔的相依性檢查

Makefile 使用 make depend 進行相依性檢查
在寫Makefile 的時候,一般的規則是這樣的:
Target: Dependency
  Command (Makefile文件是寫 recipe,不過我這邊就寫command)
如果是C 程式,通常也會把 header 檔寫在 dependency 內,否則header 檔改了結果程式沒有重新編譯就奇怪了,如果每次改了header 都要 make -B 也不是辦法。
project 長大的時候,手填 header dependency 變得愈來愈不可行,特別是Makefile 大多是寫 wildcard match,例如這樣把object files 都填入變數 OBJS 之後,直接用代換規則將 .c 編譯成 .o:
SRCS = file1.c file2.c
OBJS = $(SRCS: .c=.o)

target: $(OBJS)
  $(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
  $(CC) -c $(CFLAGS) -o $@ $<
在這裡翻譯了這篇文章,裡面給出一個針對大型專案完整的解決方法,重要的是它有把為何這麼寫的理由講出來:
http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/

最簡單的方法,是在Makefile 裡面加上一個depend 的目標,在這個目標利用一些分析工具,例如makedepend建立 dependency,但這樣等於是把 make depend 跟真正的編譯分開了,缺點有兩個,一是需要使用者下make depend 才會重建 dependency,第二是如果有子資料夾也要依序下 make depend,我們需要更好的方法。

首先我們可以利用 makefile 的include 功能,直接 include 一個含有 dependency 的makefile ,把所有source file 設為 include file 的dependency,這樣只要有source 檔更新,make 時就會自動更新dependency到最新,完全不用使用者介入;但這樣也有缺點,只要有檔案更新就要更新的dependency,我們可以做得更好。

首先是 gnu make 推薦的方法:
https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html
對每一個原始碼檔,產生一個.d 的相依性資訊
%.d: %.c
  @set -e; \
  rm -f $@; \
  $(CC) -M $(CPPFLAGS) $< > $@.Td; \
  sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.Td > $@; \
  rm -f $@.Td
先開 set -e 設定只要後面的command 出問題就直接結束,然後把 name.d 檔案刪掉;接著使用gcc 的 -M 參數(或者用 -MM ,如果不想要產生的dependency 包含system library的header),會讀入 name.c 之後,產生如下的dependency,寫到 name.d.Td 檔案中。
name.o: name.c aaa.h
之後用sed將上想 gcc -M 產生的內容,加上name.d 這個target:
name.o name.d: name.c aaa.h
對每個.c 檔都產生過 .d 檔之後,就能直接include所有.d 檔案了。
include $(sources: .c=.d)

因為 .d 的dependency 包括對應的 .c 檔,這樣只要.c 檔更新了,makefile 也會重build .d 檔,保持相依資訊在最新,同時只更新需要的 .d ,不會浪費build 的時間。
這個做法有幾個問題,首先是re-invocation 的問題,使用include的問題在於Makefile 本身的設計,因為include 可能帶入新的變數定義,目標等等,一但include 的目標有更新,它就會強制重跑一次本體的Makefile (參考這一篇:http://make.mad-scientist.net/constructed-include-files/ ),這在大型的專案上會帶來可觀的成本;更嚴重的是,如果我們把 .h 檔給刪除或更名,在編譯的時候會出現錯誤:
make: *** No rule to make target 'name.h', needed by 'name.d'. Stop.
這是因為name.d 相依於 .h ,.h 已經不存在而且又沒有產生 .h 的rule,Makefile 就會回傳錯誤;雖然改 .h 的狀況不常見,一但遇到就只能手動刪掉所有 .d 檔重新產生。

為了解決上述兩個問題,首先是Makefile 每次都會重新make 的問題,問題是這樣:如果有個source 更新了要重新編譯,那我們知不知道新的dependency 也沒差--反正它鐵定要重編譯的,該做的是為下一次的編譯產生新的 dependency 資訊;所以我們可以把產生 .d 檔這步,移到編譯 .c 檔的時候,大略如下:
OBJS = $(SRCS: .c=.o)
%.o : %.c
  # 產生 .d 檔的位置
  $(CC) -o $@ $<
include $(SRCS:.c=.d)
第二個問題比較棘手,這次用的招式是:Makefile 中如果有目標但沒有command 或dependency,這個目標又不是個已存在的檔案,那Makefile 無論command有沒有執行,會預設這個目標已經「被更新到最新」,注意它是有更新的,所以相依於這個目標的目標,也會依序更新下去。
http://www.gnu.org/software/make/manual/html_node/Force-Targets.html
例如下面這個例子,因為FORCE 每次執行都會被更新,所以clean 也一定會更新而執行
clean: FORCE
  rm $(objects)
FORCE:
所以這裡用的技巧就是,把那團相依的.h 檔變成無dependency/command 的目標,產生.Td之後,下面我是分成多行的結果,實際上可以寫得緊密一點,所做的工作包括:用 sed 依序移除comment;移除 : 前的target;移除行尾接下一行的反斜線;移除空白行;把行尾變成 : ,這樣本來的dependency 通通變成目標了。
  cp $*.Td $*.d; \
  sed -e 's/#.*//' \
  -e 's/^[^:]*: *//' \
  -e 's/ *\\$$//' \
  -e '/^$$/ d' \
  -e 's/$$/ :/' < $*.Td >> $*.d; \
  rm -f $*.Td
最後的問題是,如果我們移除 .d 檔, .c .h 檔又沒有更新則Makefile 也不會重新產生 .d 檔,因為 .d 檔並不是編譯時相依的target;但 .d 檔又不能是個真的target ,否則又有老問題:include 的時候它會產生 .d 檔,然後整個Makefile 會重跑一次;這裡的解法是把 .d 加到 .o 的dependency ,然後加一個空的target,這樣一來,.d 檔如果存在,因為commands 是空的所以什麼事都不做;如果 .d 被刪除,在make 時 .o 會觸發 .d 的更新,在空的target 更新之後, .o 也跟著更新的時候,一併產生全新的 .d 檔。
%.o: %.c %.d
  command
%.d:
最後是一些針對 dependency file 的處理,完整的Makefile 會長這個樣子:
DEPDIR = .depend
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.Td
POSTCOMPILE = mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d

%.o : %.c $(DEPDIR)/%.d
  $(CC) $(DEPFLAGS) $(CFLAGS) -c $<
  $(POSTCOMPILE)

$(DEPDIR)/%.d: ;
.PRECIOUS: $(DEPDIR)/%.d
include $(patsubst %,$(DEPDIR)/%.d,$(basename $(SRCS)))
第一段定義depend file 的存取地點,然後定義 DEPFLAGS,利用編譯時插入 DEPFLAGS 把編譯跟產生 dependency 一併做完;相關的 flags 都是 -M 開頭,可參考:
https://gcc.gnu.org/onlinedocs/gcc-5.2.0/gcc/Preprocessor-Options.html
  • -MT 定義產生出來的目標名稱,預設是 source 檔的suffix 換成 .o 後綴,並去掉前綴的任何路徑,例如 -MT www 則生成的相依資訊就變成 www: name.h,所以這個選項不一定要加。
  • -MM or -M 要求產生 dependency 資訊
  • -MP 直接幫忙對 header file 產生空的 dependency target (顯然gcc 開發者有注意到 .h 並非目標產生的問題),上面提到那一大串 sed ,如果是用 gcc 加上這個選項就不需要了
  • -MF 設定輸出到的檔案,相當於 redirect >
POSTCOMPILE 則把暫時的dependency file .Td 改名為.d,如果沒有要對 .Td 做什麼,拿掉這行,指定 -MF 直接寫到 .d 檔也是不錯的選擇。

第二段在.o 的dependency 加上 .d,command 編譯 .o 檔,同時gcc 產生.d 檔。
第三段寫入空的 .d 檔目標,以便處理 .d 檔被刪掉的狀況,.precious 讓 .d 檔在Makefile 當掉或被砍掉的時候不會被刪掉;最後 include 產生出來的 .d 檔。

大概的解法就是這樣,其實,如果project 很小的話,根本就不用這麼大費周章,像下面這個解法,直接把所有SRC送給gcc 產生dependency ,寫到 .depend 檔案,然後每次都include .depend,對 99.9%的project 來說應該都足夠了:
http://stackoverflow.com/questions/2394609/makefile-header-dependencies

2014年11月17日 星期一

使用autotool 編譯qt project

在寫這篇,我發現我曾經寫過類似的內容:
「使用gnu make編譯Qt 專案」
http://yodalee.blogspot.tw/2013/08/gnu-makeqt.html

總之,這次又是在qucs專案上遇到的問題,之前專案裡的使用者介面,不知道是哪根筋不對,竟然全部都是用手爆的啊啊啊!正好這個project現在進入巨量refactor階段,在改其中一個部分時,順手把其中一個使用者介面換用Qt的Designer來做。
結果,還要改編譯的autotool,讓它使用UIC解決才行,網路上找找沒什麼資料,只好印autotool的文件下來看,以下是我最後弄出來的Makefile.am設定:

首先,定義目標:此資料夾的靜態函式庫及它的原始碼:
noinst_LIBRARIES = libdialogs.a
libdialogs_a_SOURCES = xxxxx.cpp

原始碼後面可能有一大串,一般來說autotool有這樣有夠了,不過對Qt project,首先我們需要MOC: Meta object compiler將標題檔編譯為moc.cpp原始碼,並把它們加到靜態函式庫的原始碼中:
MOCHEADERS = xxxxx.h
MOCFILES = $(MOCHEADERS:.h=.moc.cpp)
.h.moc.cpp:
    $(MOC) -o $@ lt;
nodist_libdialogs_a_SOURCES = $(MOCFILES)
這會讓編譯器去編譯MOCFILES,找不到就用副檔名規則呼叫MOC產生.moc.cpp檔。

UIC比較麻煩一點,因為標頭檔不像原始碼一樣,會被加到編譯時的相依規則中,若像原始碼在相依規則裡,相依性不符時,Makefile會自動去尋找有沒有可以產生此相依的規則,如上面的.h.moc.cpp;但標頭檔就算全不見了,不設定的話Makefile也不會做什麼。
這裡我們使用autotool的BUILT_SOURCES來解,被加到這個變數的內容會優先被編譯:

UICFILES = ui_yyyy.h
BUILT_SOURCES = $(UICFILES)
ui_%.h: %.ui
    $(UIC) -o $@ lt;
noinst_HEADERS = $(MOCHEADERS) $(UIHEADERS)
如此一來就能順利的呼叫UIC幫我們產生標題檔了;不過含有%這種寫法是Makefile.am使用Gnu Extension達成的,因些在產生Makefile.in時會收到警告,如果不用這種寫法就只能寫明所有*.ui轉成ui_*.h的規則了。

rcc的話我是沒有用,不過它是將qrc檔轉成原始碼檔,因此估計使用方法跟moc比較像。

參考資料:
1. autotool BUILT_SOURCES:
http://www.gnu.org/savannah-checkouts/gnu/automake/manual/html_node/Sources.html

2013年12月15日 星期日

mex編譯環境設定


mex是matlab提供的編譯器,可以在matlab編譯外部的c/cpp(也許還有fortran =w=?)程式,可以用matlab的方式把資料塞給外部程式,回傳資料也可以用matlab收下來。

幹嘛沒事叫外部程式,用matlab寫不就好了?據同學強強林的說法是編外部的cpp code會跑得比較快,實際情形是如何其實作者也是啊哈哈不是很清楚。

最近因為修課的關係需要用到matlab mex,就順手編了一下,把過程記錄一下:

--

因為mex是用其他編譯器來編譯,因此要先設定系統的c/cpp編譯器

如果是Unix/MacOS的話,大概都已經裝了gcc/g++了,在matlab內打mex -setup,應該可以看到選項;windows的話請看參考資料一,這部分沒有研究。
這裡會遇到一個問題是,matlab未必支援最新版的gcc/g++,要看你的matlab版本
我是2012a版支援到gcc 4.4.6,偏偏我用archlinux,套件幾乎都會升到最新(4.8.2),這時候matlab就會回報用了太新版本的gcc。

解決方式是安裝舊版的gcc/g++ 如果是archlinux的話可以用AUR安裝,其他發行版應該有自己的解決方式。
因此先重編了4.4.6,桌電上50MB的檔案我編了快1hr …(結果筆電用-j8下去編跑得桌電快lol)

--

接著要設定gcc版本,在matlab安裝路徑(我的是/opt/MATLAB/R2012a)下有bin/mexopts.sh,把裡面的gcc/g++都換成舊版的
# C++keyName: GNU C++
# C++keyManufacturer: GNU
# C++keyLanguage: C++
# C++keyVersion:
CXX='g++' <-換掉這行g++ → g++-4.4
CXXFLAGS='-ansi -D_GNU_SOURCE'
CXXFLAGS="$CXXFLAGS -D_FILE_OFFSET_BITS=64"
CXXFLAGS="$CXXFLAGS -fPIC -pthread"
CXXLIBS="$RPATH $MLIBS -lm"
CXXOPTIMFLAGS='-O -DNDEBUG'
CXXDEBUGFLAGS='-g'

我是archlinux就是換成gcc-4.4/g++-4.4,其他發行版我不是很確定舊版裝好是像這樣加個版號,還是要把本來的gcc替代掉
接著在matlab裡面輸入
$mex -setup
代入舊版的gcc/g++,之後mex就可以正常使用囉。

參考資料:
1. http://www.physik3.gwdg.de/tstool/HTML/node9.html
2. archlinux AUR gcc44
https://aur.archlinux.org/packages/gcc44/

2013年8月8日 星期四

使用gnu make編譯Qt 專案

最近白天跑EM,夜深寫QT。

當然這完全沒什麼,其他同學至少兩年前就寫過了LOL。

只能說Qt 真是相當強大的工具,最基本的signal & slot的概念,如果對GTK的callback熟悉的話,很快就能上手。
一般在寫Qt時,最常用的還是用qmake來產生Makefile,畢竟qmake寫得還不賴,打一次就會產生好Makefile,接著make即可;不過有時個人習慣還是偏好用gnu-make,可以自己編寫Makefile,做一些細部的調整,用qmake的話只要重新產生一次Makefile,這些細部調整就要重新再修改一次。
這篇就是說明一下,要如何使用gnu-make來處理Qt專案。

--

基本上Qt的運作原理,是對原有的C++進行擴展,然後透過Qt提供的解析程式,產生額外的原始碼檔,少了這些額外的原始碼,在編譯的時候會產生一堆可怕的錯誤資訊,要用gnu-make編譯Qt專案,其實只要呼叫這些解析程式來產生原始碼即可;總共會用到的程式有三個:moc, rcc, uic;分別作用是:產生meta object(故名Meta-Object Compiler)、處理resource(resource compiler)檔、產生介面檔(User Interface Compiler)。

一般Makefile裡面會有這些東西,用來把所有的source轉換成object file。
SOURCE_FILE = main.cpp 
OBJECT_FILE += $(addsuffix .o, $(basename $(SOURCE_FILE)))

為了Qt,我們加上下列的變數定義:
#==================================================
# Qt special function
#==================================================
QT_LIBS = -lQtCore -lQtGui -lQtOpenGL
QT_PATH = /usr/lib/qt4/bin
QT_MOCFILE = mainwindow.h
QT_RCCFILE = resource.qrc
QT_UICFILE = first.ui
QT_MOCSOURCE = $(addprefix moc_, $(addsuffix .cpp, $(basename $(QT_MOCFILE))))
QT_RCCSOURCE = $(addprefix qrc_, $(addsuffix .cpp, $(basename $(QT_RCCFILE))))
QT_UICSOURCE = $(addprefix ui_, $(addsuffix .h, $(basename $(QT_UICFILE))))

這一整個區塊與QT有關,所有參數都加上QT為標示。
QT_PATH設定Qt的執行檔位置,如果安裝在其他地方、或要用其他版本就要自己換地方。
QT_*FILE是先定義,moc, rcc, uic分別要處理的檔案,moc會處理所有.h檔,產生含meta object的cpp檔;rcc會處理qrc file,產生相對應的cpp檔;uic則會處理ui,產生可包入的header file。
透過makefile的suffix, prefix,轉成我們需要轉出的檔案名稱,個人的習慣是在這些檔名前加上關鍵字moc_, qrc_, ui_。

--

接著是利用implicit rules來compile所有的物件檔:
TARGET = program
BIN_DIR = bin
LIBRARY_DIR = library
SOURCE_FILE = main.cpp $(QT_MOCSOURCE) $(QT_RCCSOURCE)
OBJECT_FILE += $(addsuffix .o, $(basename $(SOURCE_FILE)))

由於moc和rcc會產生新的cpp檔,因此需要將它們列入;然後就可以用implicit rules執行:

%.o:%.c
  $(CC) -c lt; -o $@ $(CFLAGS) $(INCLUDE)
  @$(MOVE) $@ $(LIBRARY_DIR)

%.o:%.cpp
  $(CXX) -c lt; -o $@ $(CXXFLAGS) $(INCLUDE)
  @$(MOVE) $@ $(LIBRARY_DIR)

moc_%.cpp: %.h 
  $(QT_PATH)moc $(DEFINES) $(INCLUDE) lt; -o $@

qrc_%.cpp: %.qrc
  $(QT_PATH)rcc lt; -o $@

ui_%.h: %.ui
  $(QT_PATH)uic lt; -o $@
前兩項是將c/cpp-編成 .o檔,當遇到QT_MOCSOURCE, QT_RCCSOURCE的檔案,就會由moc, rcc產生。
另外在編譯主程式的相依性中,原本我們只要它編譯所有的OBJECT_FILE,現在還要加上由uic產生的header file:
$(TARGET): $(OBJECT_FILE) $(QT_UICSOURCE)
  cd $(LIBRARY_DIR); \
  $(LINKER) -o $@ $(OBJECT_FILE) $(LIBS); \
  cd ..
  mv $(LIBRARY_DIR)/$@ $(BIN_DIR)
如此一來,就會觸發uic產生出header file。

--

透過以上的設定,即可完成Qt專案的編譯,不過最後還是要說,雖然我這裡是這麼寫,但其實真的用的時候還是用qmake來產生Makefile =w=,套句AZ大神的話「它寫得這麼好幹嘛不用?你白痴嗎。」

結論:這篇網誌是一篇垃圾,寫Qt請愛用qmake。 lol

參考資料:
1. C++ GUI Programming with Qt 4
2. http://woboq.com/blog/how-qt-signals-slots-work.html

2013年1月29日 星期二

Design Pattern

最近小弱弱版主在看設計模式(design pattern)相關的書,寫一下我對它的感想。

我認為design pattern中相當重要的是繼承和polymorphism的概念:
繼承讓我們能定義一個基礎的型別,然後依據不同類型的物件,都繼承自這個基礎型別
Polymorphism則允許我們利用基礎型別的pointer,依據執行時指向的物件來取用它的資料。

從design pattern來看,Polymorphism有兩個最重要的功能:
1. 也就是上面提過的,是在執行期決定要執行哪個function,只要用父代的pointer去access繼承產生的子代,就可以視生成哪個物件來取用它的function,這也是一般教polymorphism時會提到最基本的用處,相信設計師都很清楚了。
2. 第二個功能延伸自第一個,利用polymorphism給予我們執行期時的彈性,延伸出來的是,在設計、修改時保有彈性(也就是Design Pattern),把每個不相干的部分給分開,讓我們不必要每次要新增功能、新增新的物件時,都要進到Implementation的層次做修改,浪費許多時間在尋找該加上新功能的地方;另外也能減少增加新東西時,因為相依關係而要重新編譯的部分。
事實上我認為第二個功能的重要性遠高於第一項,設計的彈性增進工作效率,才能將Object oriented的火力全面發揮出來。

說個自己想,沒什麼用的例子:
比如說我們要寫個選角色的程式好了,每個角色都有一個主要的招式master spark之類
所以說,最簡單的寫法就先設定一個基礎的角色:class character,內含一個main_spell()
然後各個角色繼承這個character.h,定義自己的main_spell()裡面要幹什麼,這時候所有角色的介面就一致了,可以用一個character的pointer來操作每一個角色的功能。
但這樣做其實不夠好,如果今天我們希望有些角色的main_spell不要動作怎麼辦?然後每個人招式又都不一樣?
我們可以打開每個角色的原始碼,然後重載(overload, 這個翻譯=w=)main_spell()
可是如果我們要加上10個角色呢?
所以Design Pattern就會要求,不止character,連main_spell也要是一個base class,不同的招式則是繼承它後修改,character則包含一個main_spell的pointer,用polymorphism的方式取得不同招式;如此一來,可以很方便的加上新的main_spell,只要新增main_spell的class即可,在character的介面卻完全不需要更動, 在維護跟更新上會更有效率。

這個composition over inheritance應該算是最基本的design pattern的概念了,Design Pattern所教的,就是過去神級的設計師,針對許多設計上的問題提出適當的解決方案(雖然感覺就是愈會變動的地方就會加上更多虛擬層,或是愈切愈細),據說在STL裡面用上許多Design Pattern,如果小弱弱板主學有心得再上來獻醜。

參考資料:
1. Head First Design Pattern:
http://shop.oreilly.com/product/9780596007126.do
會用google的可以在後面加上pdf
2. http://sourcemaking.com/design_patterns
包山包海的Java/C++ source code

2012年11月13日 星期二

c++ static 修飾詞

最近寫基因演算法的project,有需要使用到C++ class的static variable,因為我之前這個東西怎麼寫都會出錯,這次好不容易在強者陳仕勳(GodGodMouse)的指導下成功寫出可以用的static,在這裡記錄一下怎麼使用C++的Static(是說這些文章也多到滿出來了)。

靜態物件static的標記包括:在主體程式中使用,在函式中使用,和在class中作為靜態資料使用。
總歸三種使用方式,都是要在程式的各處中使用共有的一份資料,下面針對三種使用方式,分別寫明使用方法及使用的時機: 

1. Main程式:

在global 的地方使用static,可以讓該變數由程式的全域變數變成檔案內的全域變數,使用static的全域變數幾乎永遠是好習慣,畢竟其他程式設計師在用你的程式時只想把你的程式當黑箱子,要是哪個全域變數名字相衝……那大概不是一兩個幹字可以解決的問題,例如下文中的debug變數,我每個檔案都想要有debug變數,這時就要加上static。
簡單的例子,我們幾個cpp檔裡面都有一個debug變數(總不能main裡的叫maindebug,gui裡的叫guidebug吧?),如果不加上static
//main.cpp
int debug  = 0; 
//src1.cpp
int debug  = 0; 
這樣在最後編譯的時候,g++ main.o src1.o的階段,就會回傳錯誤,因為debug在這裡被重複定義。 因此,請為全域變數加上static。
Static int debug = 0
或者,與static相對的就是extern,宣告變數是在別的檔案裡有定義,這時候不能再給初始值(因為初始值是從別的檔案來的):
extern char* filename;

2. 在函數裡使用:

在函數裡加上static的變數,不同於一般的變數「在函數執行時起始,於函數終結時清除」,static變數在程式一執行時即被宣告。我想這個已經芭樂到不能再芭樂的地步了,例子也不勝枚舉,如果我們要輸出某函數被call幾次,請愛用static。
Void function(){
 static int i=0;
 cout << ++i << endl;
}

3. 在class裡使用:

在class裡使用,其功用在於,這個class產生的任何一個object(or instance if you like),都可看到static的變數或函數。 比如說我project寫的基因演算法的程式,經由隨機產生了10000個子代,每個子代都要對某一筆測試資料進行處理,看看結果好不好;這時候當然不可能每一個子代都保留一份測試資料的副本(記憶體不用錢也不是這樣花的吧…),就要用static的方式保存資料,讓每個子代都能看到。
在class裡面用會複雜一點,假設現在有一個叫static_test的class,架構如下:
//static_test.h
class static_test { 
public:
    static int static_var;
    static void output();
    int i;
};
所以我們宣告了一個static的integer跟一個static 的function,在.h宣告中,大部分的東西是一樣的。
但在定義式裡,就有些不同,由於static物件在是class共有的一部分,宣告class物件時並不會初始化static的物件,因此在.cpp裡面第一件事就是先初始化static物件:
//static_test.cpp
int static_test::static_var;
要不要給初始值沒什麼關係,但這行指明:static_test中有一個static_var,不寫這行編譯時會出現undefined reference to static_var。 取用static物件時,也要用'::'指明:我要取用static_test的物件,例如:
cout << static_test::static_var << endl;
static_test::static_var = 10;
static function是類似的事情,每一個class都可以取用這個function,但static function裡面,只能用到static的member,因為它並非任何一個object的function,自然不能取用其中的內容,就如文中的output()不能取用 i 這個變數。
據說static member最常用的地方是統計目前程式中生成了幾個object,在contructor中對static member+1,destruct的時候-1,不過作者到目前還是沒遇到相關的場合,所以…就算了吧,能用就好。

 4. 致謝: 

本篇文章,感謝陳仕勳(GodGodMouse)強者的指導。

2012年10月29日 星期一

autoEM project: 系列二:option parser

最近因為寫autoEM用到的了python optparse函數,就把相關用法整理了一下。
另外之前看一個BASH譔寫的圖形處理程式(接近700行的code就能完成許多驚人的功能,真的有一點點誇張),也發現到原來BASH內就有內建getopts的功能,雖然說查了manpage發現: All parsing is done by the GNU getopt(3) routines,根本就是直接用C的getopt來做的嘛…
不過getopt在寫大型程式時常用到,就整理一下 C、BASH跟python optparse函數,讓自己不要忘掉,也給大家作個參考:

1. C:
在c裡面用的是unistd.h的getopt,這個方式不接受--XD之類的寫法,getopt_long有提供相關功能,用法還滿相似的,就給大家自己去研究:
getopt(argc, argv, optstring) 並有四個外部變數
extern char* optarg
extern int optind, opterr, optopt optstring
為指定getopt要辨識的參數,大略上有3條規則:
1. 在其中的character,都是合法的參數選項。
2. 後面有“:”的character,需要有後面附加的參數,如-f filename,getopt會將optarg指向該串文字。
3. 加上”::”的character,可以用直接連接的方式加上長參數,如 -oargument,此時optarg會指向argument,但使用者忘記寫argument的話,optarg會指向空字串。
getopt每parse一個參數,就會回傳parse到的參數到opt,如果掃完了,就會回傳-1;遇到不合法的參數,會回傳?;加上:的參數卻沒有在之後寫上對應的參數,則回傳?,如果在optstring第一個字元是:,則上述無參數的狀況會回傳:。

看不懂對不對,看一下怎麼用就明白了:
while((opt=getopt(argc,argv,":o::e:"))!=-1){
  switch(opt){
    case'o':
    printf("%s\n",optarg);
    break;
  case'e':
    printf("%s\n",optarg);
    break;
  case':':
    printf("didn't specify the argument\n");
    break;
  case'?':
    printf("unknown argument\n");
    break;
 }
}
上面只是在範例,把選項輸出一下,實際上要做什麼就做什麼,那其實BASH的情況下是幾乎一樣的寫法:
while getopts o:e: OPTION
do
  case $OPTION in
    o)
      …
    e)
      …
    ?)
      …
  esac
done

2. python的部分:
python下有兩個可行的方法 optparse或argparse,不過optparse好像不再支援,以aptparse為主體。
因為我2.7用的還是optparse,之後有空再來整理aptparse,應該不會差太多…吧?
1. 首先是引入optparse:
from optparse import OptionParser make_option
parser = OptionParser(usage = 'Usage: autoEM [OPTION...] PAGE...',
 option_list=option_list)
usage的部分會自動出現在./program -h時的第一行,同時python會自動將現在的option 2. 定義options: 有兩種方法定義options,一種是呼叫parser
parser.add_option('-v', '--version', action='store_true', dest='version', default=False, help='show version information')
另一種個人比較喜歡,先用make_option把選項都寫在一個set option_list 裡
option_list = [ 
    make_option('-v', '--version', action='store_true', dest='version', default=False, help='show version information') ] 
然後如上面paser = OptionParser的地方,用option_list=option_list把選項一口氣寫進去。

上面這個option,會接下的選項為:
-v foo
-vfoo
--version foo
--version=foo store
會設定options.X的值,X依序為dest ->長參數(version) → 短參數(v)如果前一個沒有給定的話,例如上面會設定options.version。
後面的參數可以加上type=int, float,不設的話store會以string的方式儲存。
另外有store_true, store_false,此時會將dest設為true或false。

3. 處理options:
呼叫parser_args處理argument,default會使用sys.argv[1:]作為參數,當然我們傳自己設定的argument。
options, args = parser.parse_args() args會儲存parse到多少個options。
接著就可以透過options.X的方式,一一寫下各自的處理方式,例如:
if options.filename:
 filename=options.filename
一般來說如果使用者忘了傳入特定參數,如-f忘了寫檔名,optparse都會自動輸出上面的usage,跟相關的錯誤訊息。如果我們要自行呼叫error,可以用parser.error()呼叫,如:
if options.a and options.b:
    parser.error('-a and -b can exist together')
另外optparse還有不少功能,不過上面的對於一般程式來說應該相當夠用了,剩下的就請大家自己去研究了。

3. 參考資料:
1. C的部分:
Linux manpage:
man 1 getopt for BASH
man 3 getopt for C
beginning linux programming 4/e
2.Python部分:
http://docs.python.org/library/optparse.html

2012年9月30日 星期日

程式上色

其實這是整理在BBS上的舊文,想說就把它轉到這裡來,是記錄如何在程式裡面加上顏色的控制,如果熟BBS的人應該很熟悉,似乎是一個公訂的標準上色方式,大家參考參考,也許可以讓你的程式增添不少色彩,能不能為人生上色就說不準了。

1. ANSI C:
開始上色格式:\033[x;y;zm
結束上色格式:\033[0m //其實就是設定為無屬性
\033就是escape字元,可以用\e來代替
x,y,z三數字分別定義字型格式、前景、背景。

x:
0 無屬性
1 明亮字體
2 暗淡字體
4 底線字體
7 反白字體
8 匿蹤字體 

y,z:
30~37 為字體顏色 黑紅綠黃藍紫青白
40~47 為背景顏色 黑紅綠黃藍紫青白

ex: printf("\033[1;33mtest\033[0m")
ex: printf("\e[1;33mtest\e[0m")

2. BASH shell:
第一種方法,可以用printf來實作,這和C就是一樣的了
至於echo,要用echo -e的方式,讓它解讀escape跳脫 ,之後的寫法也一模一樣。
值得一提的,這個設定對bash prompt也有用,比如說我的PS1參數的設定: [\e[1;32m\u\e[0m@\e[1;34m\h\e[0m \e[1;33m\W\e[0m]\$
很難看懂,總之,綠色的使用者名稱,藍色的電腦名稱跟黃色的路徑,還滿潮的XD

3. curses.h 雖然說現在用curses.h的程式好像滿少了,不過就寫一下吧,主要有五個步驟,那時候在ubuntu上測試是OK,還寫了一些東西,這次把它拿出來看發現已經看不懂了Orz。
    一、利用has_color() 偵測是否有顏色支援
    二、start_color() 開啟顏色支援
    三、init_pair(pair,f,b) 設定屬性
    四、attrset(COLOR_PAIR(pair)) 設定接下來使用這個屬性來輸出
    五、attroff(COLOR_PAIR(pair)) 關掉這個屬性
第三部的init_pair裡:
pair指的是要存在第幾個顏色設定暫存中 可以指定1~COLOR_PAIRS這麼多組,可以用printf("%d",COLOR_PAIRS)來得知,ubuntu10.10終端是64組。
f 指字體顏色,b 指背景顏色 可以用數字,也可以用在curses.h中定義好的代碼
COLOR_BLACK    0
COLOR_RED 1
COLOR_YELLOW 2
COLOR_GREEN 3
COLOR_BLUE 4
COLOR_MAGENTA 5
COLOR_CYAN 6
COLOR_WHITE 7
其實順序跟就跟之前提的一模一樣…
屬性的部分,也有預先定義好的可以使用,ex直接attrset(A_BOLD即可),不同的屬性可以用 | 來加總,不過也別加太多= =。
A_NORMAL 正常
A_STANDOUT 些許高亮度
A_UNDERLINE 底線
A_REVERSE 反白
A_BLINK 閃鑠
A_DIM 暗淡
A_BOLD 粗體
A_PROTECT 沒試過…不清楚
A_INVIS 匿蹤
A_ALTCHARSET 沒試過…不清楚
A_CHARTEXT 沒試過…不清楚

4. 參考資料:
基本上色:
https://wiki.archlinux.org/index.php/Color_Bash_Prompt
其餘關於ncurses:
http://doc.linuxpk.com/55549.html
http://fanqiang.chinaunix.net/a4/b2/20020626/060200258.html
http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/

5. 結論: 顏色上色固然漂亮,可是也會讓輸出的內容變得較難閱讀,請不要太頻繁的使用。