Singleton in GO

Go本身並沒有提供Singleton,而我其實個人也不是很愛Singleton Pattern。不過有些設計缺他不可,比如說某些設計下System Context就必須要是一個Singleton,所以還是得看看要怎麼在Go裡面實作出來。

最直覺的做法

type singleton struct {
}

var instance *singleton

func GetInstance() *singleton {
    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

這做法很直覺,大多數的情況來講應該也夠用了。但是這個最明顯的問題就是instance = &singleton{}這段顯然在multi thread會有點問題...

那....第二直覺的做法

var mu Sync.Mutex

func GetInstance() *singleton {
    mu.Lock()                        
    defer mu.Unlock()

    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

多虧go有defer這個關鍵字,整個mutex的做法變得很輕鬆。但是...為什麼要鎖那麼大的範圍啊? XD 整個GetInstance的效能都被這個大範圍地圖鎖給搞爛了...

好像也頗符合直覺的做法?

var mu Sync.Mutex

func GetInstance() *singleton {

    if instance == nil {
        mu.Lock()
        defer mu.Unlock()
        if instance == nil {
                instance = &singleton{}
        }
    }
    return instance
}

有完沒完(翻桌),這做法不能說錯,不過要把code寫成這樣還真的需要滿高恥力的,幸好Go有發現這點,為了讓我們寫Singleton的時候不至於羞愧而亡,他們有提供了once

使用once?

目前來講比較常見的合理做法是使用go內建的once,這可以保證裡面的code只會被跑一次,這大概長得像這樣

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

其實可以參考一下once原始碼,寫的滿漂亮的。簡單的說,就是在裡面放一個atomic int來當作flag,當int > 0就會直接跳出,否則就會執行Do並且把atomic int +1。

原始slide在這裡。原文的範例缺了一些東西以致於compile起來會錯誤,我把缺的東西順手補上去。

以及補上一些我這邊的心得跟額外的測試方法。

範例 : Google Search

  • Google Search是幹嘛的
    • 給一個query string,回傳搜尋結果(也許還加些廣告)
  • 我們要怎麼得到搜尋結果
    • 分別送query string到Web/Image/Youtube/Map/News...等搜尋引擎,然後把結果合併起來

我們要怎麼implement這個系統?

先來Mockup這個系統的樣子吧

我們用fakeSearch來模擬一個Server,這個server可能會很快即時的反應,也可能最多會在100ms以後才反應。

var (
    Web = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
)

type Search func(query string) Result
type Result string

//第一個版本的google search, 最直觀

func Google(query string) (results []Result) {
    results = append(results, Web(query))
    results = append(results, Image(query))
    results = append(results, Video(query))
    return
}

func fakeSearch(kind string) Search {
        return func(query string) Result {
        //模擬Server延遲

              time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
              return Result(fmt.Sprintf("%s result for %q\n", kind, query))
        }
}

測試一下這個mock能不能work

func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
    results := Google("golang")
    elapsed := time.Since(start)
    fmt.Println(results)
    fmt.Println(elapsed)
}

Google Search 1.0

1.0就是我們放在mockup裡面的那個版本,google會去各種來源搜尋結果,然後把它給合併起來傳回去。

func Google(query string) (results []Result) {
    results = append(results, Web(query))
    results = append(results, Image(query))
    results = append(results, Video(query))
    return
}

Google Search 2.0

1.0顯然有些問題,他的query都是要等上一個query完成以後才能啟動,所以整個程序會被阻塞好一陣子。這個實作不需要鎖,沒有conditional variable,也不需要callbacks

func Google(query string) (results []Result) {
    c := make(chan Result)
    go func() { c <- Web(query) } ()
    go func() { c <- Image(query) } ()
    go func() { c <- Video(query) } ()

    for i := 0; i < 3; i++ {
        result := <-c
        results = append(results, result)
    }
    return
}

Google Search 2.1

那要是我們設定一個timeout呢?

c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()

timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
    select {
    case result := <-c:
        results = append(results, result)
    case <-timeout:
        fmt.Println("timed out")
        return
    }
}
return

但是顯然的,drop掉太慢的search其實不太好。假設Server回應時間就是0-100之間,而太慢的response就是會被drop掉,那有沒有什麼方法能拿到完整的result呢?

Google Search 3.0

解決drop過慢result的問題

要解決2.1的問題其實也不難,我們對同一個服務(比方說Video)做出多個request,取其中最快的一個就可以了 -- 這也是google目前的做法,所以大家知道為什麼搜尋那麼耗電了吧....

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

func main() {
    rand.Seed(time.Now().UnixNano())
    start := time.Now()
    result := First("golang",
        fakeSearch("replica 1"),
        fakeSearch("replica 2"))
    elapsed := time.Since(start)
    fmt.Println(result)
    fmt.Println(elapsed)
}

所以3.0實作大概會長這樣

記得要先在上方額外宣告Web2/Image2/Video2

var (
    Web = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
    Web2 = fakeSearch("web")
    Image2 = fakeSearch("image")
    Video2 = fakeSearch("video")
)

然後3.0會是長這樣

c := make(chan Result)
go func() { c <- First(query, Web, Web2) } ()
go func() { c <- First(query, Image, Image2) } ()
go func() { c <- First(query, Video, Video2) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
    select {
    case result := <-c:
        results = append(results, result)
    case <-timeout:
        fmt.Println("timed out")
        return
    }
}
return

3.0還有什麼問題呢?

這個留作Bonus給大家參考一下。3.0有一個相當明顯的bug就是,當First後面的所有Search都大於80的時候,會導致掉結果。比方說,c <- First(query, Image, Image2)當後面兩個回應速度都比80慢(設定上他們速度是0-100),該結果就不見了,你會看到印出timed out。

所以我們還可以研究看看 :

  • 如何避免掉資料?
  • 如何在掉資料的時候動態把容忍值拉高?好處跟壞處分別是?
  • 或者換個方法,如何動態增加replica?好處跟壞處分別是?如果動態增加replica的話,應該要注意什麼條件?
  • 事實上網路連線速度並非fakeSearch這樣的穩定在兩個數值中間徘徊,有沒有更好的模型可以模擬一段好一段壞的不穩定網路?甚至package drop?

如果要測試這個極端狀態的話,我們可以把timeout := time.After(80 * time.Millisecond)的80改成一個更小的數字,比方說40。或者說,我們可以把fakeSearch的response time動態拉到一個更大的範圍,就可以很容易重現出這個掉資料的狀態。

來比較一下1.0 2.0 3.0的效能

demo code如下,結果大約會像是這樣

Function : main.Google1 Total time : 1639 Avg time : 163
Function : main.Google2 Total time : 712 Avg time : 71
Function : main.Google3 Total time : 550 Avg time : 55

package main

import (
    "fmt"
    "math/rand"
    "reflect"
    "runtime"
    "time"
)

var (
    Web = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
    Web2 = fakeSearch("web")
    Image2 = fakeSearch("image")
    Video2 = fakeSearch("video")
)

type Search func(query string) Result
type Google func(query string) []Result
type Result string

func fakeSearch(kind string) Search {
    return func(query string) Result {
        time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
        return Result(fmt.Sprintf("%s result for %q\n", kind, query))
    }
}

func Google1(query string) (results []Result) {
    results = append(results, Web(query))
    results = append(results, Image(query))
    results = append(results, Video(query))
    return
}

func Google2(query string) (results []Result) {
    c := make(chan Result)
    go func() { c <- Web(query) } ()
    go func() { c <- Image(query) } ()
    go func() { c <- Video(query) } ()

    for i := 0; i < 3; i++ {
        result := <-c
        results = append(results, result)
    }
    return
}

func Google21(query string) (results []Result) {
    c := make(chan Result)
    go func() { c <- Web(query) } ()
    go func() { c <- Image(query) } ()
    go func() { c <- Video(query) } ()

    timeout := time.After(80 * time.Millisecond)
    for i := 0; i < 3; i++ {
        select {
        case result := <-c:
            results = append(results, result)
        case <-timeout:
            fmt.Println("timed out")
            return
        }
    }
    return
}

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

func Google3(query string) (results []Result) {
    c := make(chan Result)
    go func() { c <- First(query, Web, Web2) } ()
    go func() { c <- First(query, Image, Image2) } ()
    go func() { c <- First(query, Video, Video2) } ()
    timeout := time.After(80 * time.Millisecond)
    for i := 0; i < 3; i++ {
        select {
        case result := <-c:
            results = append(results, result)
        case <-timeout:
            fmt.Println("timed out")
            return
        }
    }
    return
}

func Benchmark(google Google) (output []Result, start time.Time, elapsed time.Duration) {
    start = time.Now()
    output = google("golang")
    elapsed = time.Since(start)
    return
}

func GetFunctionName(i interface{}) string {
    return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func main() {
    test := []Google{Google1, Google2, Google3}
    const loops = 10
    for _, google := range test {
        var totalTime int64
        for round := 0; round < loops; round++ {
            rand.Seed(time.Now().UnixNano())
            //fmt.Printf("start\t : %d -> %s\n", round, GetFunctionName(google))

            _, _, elapsed := Benchmark(google)
            totalTime += elapsed.Milliseconds()
            //fmt.Printf("end\t : %d -> %s(%s)\n", round, GetFunctionName(google), elapsed)

        }
        fmt.Printf("Function : %s\tTotal time : %d\tAvg time : %d\n", GetFunctionName(google), totalTime, totalTime / 10)
    }
}

從無到有開發Safari App Extension

跟開發無關的東西都寫在這個頁面上: Safari Plugin : CurrencyConverter

Github link : GitHub link

不過這篇主要是談談如何從無到有來做出一個Safari App Extension

跟以前的Safari Extension不同處

以前的Safari Extension已經被deprecated了,現在所有的Sarafi擴展都必須要是Safari App Extension。跟以前的extension比起來有這些不同:

  • 現在無法單獨發布extension了,一定得依附在一個macOS App上,即使那個App毫無功能。
  • 他擁有一個swift/obj-c的backend可以被呼叫,這對於js白癡來講是個福音,但是同時來講也限制了跨平台的可能性。
  • 本來Safari Extension的JS callback權限被大幅縮水,比方說現在要開啟new tab的話,由於安全性考量被迫一定得從backend來開啟。
  • 可以用interface builder來製作popup... 不過popup其實功能很有限,僅止於「被toolbar button喚出」而已,沒辦法做出如按下右鍵就出現一個popup這種神奇操作。
  • Contextual Menu的validation全部改成從backend來決定,也就是你右鍵選單要不要出現,要出現什麼文字,全部都要從backend來做。

總之,就是大幅縮減JS中safariextension library的權限,而把決大部分決定權丟往backend。不過老實講我覺得backend用swift是比javascript好寫多了啦....(汗)不過這大概也會造成Safari Plugin很難紅起來,而且裡面限制很多,後面會慢慢提到。

接下來的所有開發,都會以Currency Converter為例子。我不會講得太詳細,我只會提一下做App Extension必要的步驟以及要注意的東西。

官方文件

我必須要說官方文件少得很可憐,請參考這裏。不過官方文件有詳細說明整個extension的生命流程以及containing app/extension app在整個流程中的角色,請務必稍微看一下。

Containing App基本上跟Extension App並直接共用資源,必須要用App Group的方式才能共享,不過這次我們並沒有直接使用這機能(但是code裡面有一些這東西的殘餘就是)

建立Safari App Extension

XCode打開,選Create new project,選擇Create new Safari App Extension App即可。細節不贅述,大家都寫過macOS或者iOS的App(是嗎?)

修改Extension裡面的Info.plist

主要是要把NSExtension=>SFSafariWebsiteAccess裡面的Level從Some改成All,這改動可以讓這個plugin在所有的website都生效。本來的設定會僅讓這個plugin在*.webkit.org網域內啟動而已,其他網域都會不啟動。

為了讓Toolbar Button可以popup customize view,所以要把SFSafariToolbarItem的Action改成Popover。

最後,我們需要一個顯示匯率轉換結果的contextual menu,所以我們把SFSafariContextMenu裡面的預設的Command改成自己喜歡的,我是使用CurrencyExchange來做這個Context menu的識別字。

裡面還有一些可以設定的東西,比方說如果你要更改inject進網頁的script的名字的話,SFSafariContentScript裡面可以把Script改成自己想要的名字,當然,要對應專案裡面script的名字才可以。以我的例子來講,我把這個值改成cc-extension.js。CSS也是如法泡製。

從javascript跟extension app溝通

官方文件主要是這一份,裡面有些不太直覺的東西我會特別提出來。

Javascript to Backend

主要已知的溝通有兩種 :

  1. 以Message溝通
  2. Context Menu Validation,也就是提供資訊給backend決定右鍵選單怎麼辦
  //丟message以及user info,user info可留空

  safari.extension.dispatchMessage("SomeMessage", {"target": 1234});
  //丟contextmenu的validation給backend

      safari.extension.setContextMenuEventUserInfo(event, {"selected": selected})

而這兩種分別會在swift的messageReceived 以及 validateContextMenuItem處理:

override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
    if messageName == "SomeMessage" {
        //Do something about message

    }
}

override func validateContextMenuItem(withCommand command: String, in page: SFSafariPage, userInfo: [String : Any]? = nil, validationHandler: @escaping (Bool, String?) -> Void) {
    NSLog("validateContextMenuItem : Command: \(command), userInfo: \(String(describing: userInfo)), validationHandler: \(String(describing: validationHandler))")
    //處理 Command為CurrencyExchange的context menu

    if command == "CurrencyExchange" {
        //Do something about this command

        //這邊有個很弔詭的地方,false代表「可以顯示」並且可以更改context menu顯示出來的字元,而true反而代表「不顯示」...這反直覺的地方當初卡很久

        validationHandler(false, "Change Text")
    }
}

特別注意的就是validationHandler(false, "some text"),false是代表顯示該context menu,true代表不顯示,這很違反直覺..... 其他關於這函數請從我提供的連結去看document

Backend to Javascript

這個在backend也是用message + metadata的方式傳送到Javascript的

page.dispatchMessageToScript(withName: "complexMessage", userInfo: ["myKey": "myValue"])

而注意,javascript中他接收的是messageevent,而不是以上一個例子來講的complexMessage這個event name,所以要寫成這樣

safari.self.addEventListener("message", handleMessage);
function handleMessage(event) {
    console.log(event.name);
    console.log(event.message);
}

我猜很多人會寫成下面這樣然後卡上一陣子...

safari.self.addEventListener("complexMessage", handleMessage);

SafariExtensionHandler的生命週期

這個的生命週期極短,而且是每個page都會產生一次,所以不要把任何變數放在這裡面!我自己是建議把UserDefaults來當作global context來存變數,比方說

UserDefaults.standard.set(lastCurrencyExchangeStr as String, forKey: "lastResult")

如果不喜歡這樣的話,也可以自己做一個singleton class來存。

如果是在SafariExtehsionHandler裡面宣告一個member variable的話,你會發現這東西只要跳出function就消失了。這問題也是會卡不少人一陣子的。

其他的眉眉角角

Sandbox

Sandbox預設是打開的,記得要allow outgoing connection,在Signing & Capabilities底下,不然API會無法收到資料,不然乾脆直接關掉Sandbox(如果要上app store,會因此被reject喔)

Non-secure API

從不知道哪個版本開始,如果是非SSL API Call都會被擋下來,而我所使用的fixer.io免費版本只有http,沒辦法支援https。

所以要在plist裡面明文指定這個app可以用http來做API call。

  1. 在plist裡面建立一個App Transport Security Settings的dictionary
  2. 建立一個NSAllowsArbitraryLoads的key,value給true

不過如果你要上架你的app,你必須跟apple解釋為什麼你要允許不安全的http連線。如果沒要上架的話,那這樣就可以了。

CoreData跟CloudKit CoreData

都能用,甚至可以跟Containing App共用(by App Groups),不過得做些修改。這個plugin沒使用這個功能,不過這邊有詳細的做法可以參考。簡單的說,雖然XCode template是把它寫在App Delegate下,但是實際上CoreData並不需要App Delegate才能使用,所以別花時間去找Safari App Extension要怎麼拿App Delegate了,要用Core Data的話直接用就是了。

我對產生Toolbar Icon感受到了強烈的困難

對,我也是。Apple似乎無法吃PDF以外的格式,一整個莫名其妙,明明其他的Icon是可以吃SVG/PNG的。

我這邊最後解決了這問題,解法大概是這樣

  • FlatIcon 選Icon,Download SVG格式
  • 去線上找SVG to PDF,我是找到了CloudConvert啦,確認是能用的。
  • 放進Finder,記得拖入Project
  • plist.info改toolbar的Image檔名即可

要注意的幾點:

  • PDF只能是黑白,彩色的話顯示起來就只會是一個....剪影
  • 記得要把PDF拖入Project,不然他不會被Copy bundle resource觸發,會直接當的你莫名其妙。就把檔案從Finder拖進XCode左邊的檔案列表即可,記得Target要選對。

Context Menu(右鍵選單)能不能有Sub Menu?

不行,不用找方法了。

LD error的小經驗談

某天(其實就是今天)我在編譯一個自己自娛娛人的專案的時候,本來可以編過的專案卻跳出了Linker error :

[ 60%] Linking CXX executable bin/AnalyzerTest
Undefined symbols for architecture x86_64:
  "CSelection::nodeAt(unsigned int)", referenced from:
      Crawler::GetMaxIndex() in libAnalyzerLibs.a(PttCrawler.cpp.o)
      Crawler::GetArticleInIndex(int) in libAnalyzerLibs.a(PttCrawler.cpp.o)
      Crawler::ParseArticle(ArticleInfo&) in libAnalyzerLibs.a(PttCrawler.cpp.o)
  "CDocument::parse(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >)", referenced from:
      Crawler::GetMaxIndex() in libAnalyzerLibs.a(PttCrawler.cpp.o)
      Crawler::GetArticleInIndex(int) in libAnalyzerLibs.a(PttCrawler.cpp.o)
      Crawler::ParseArticle(ArticleInfo&) in libAnalyzerLibs.a(PttCrawler.cpp.o)
ld: symbol(s) not found for architecture x86_64

這個專案我前幾天才開始弄,換台電腦的話需要在目標電腦上安裝一些dependency才能跑,所以這訊息其實我並不以為意,直覺來講就是哪個package看看需要裝,cmake可能需要調整一下確保library可以load進來就可以了。

但是這次不同,不管我怎麼設定library load 怎麼用nm去查,看起來symbol都在(不過事後發現我只瞄過去「東西有在」卻沒仔細對「東西一不一樣」),所以花了很多時間去確認library到底有沒有真的被load進執行檔...

直到我仔細看了一下nm的結果

➜  lib git:(master) ✗ nm libAnalyzerLibs.a | grep nodeAt
                 U __ZN10CSelection6nodeAtEj
➜  lib git:(master) ✗ nm libgq.dylib| grep nodeAt
000000000000d120 T __ZN10CSelection6nodeAtEm

誒!?好向有點細微的差異??? demangle看看

➜  lib git:(master) ✗ nm libAnalyzerLibs.a --demangle| grep nodeAt
                 U CSelection::nodeAt(unsigned int)
➜  lib git:(master) ✗ nm libgq.dylib --demangle| grep nodeAt
000000000000d120 T CSelection::nodeAt(unsigned long)

靠杯啊!!! 一個unsigned int一個unsigned long!? 也就是說我拿到的include檔編出來的placeholder(就是上面的"U" 代表這個symbol要從別的地方--也就是下面的libgq.dylab拿)居然symbol跟binary(也就是下面的"T")裡面的不一樣!? 待我調出原始檔看看 :

我的CSelection::nodeAt()

public:

    CSelection find(std::string aSelector);

    CNode nodeAt(unsigned int i);

現在編出來的library原始檔的CSelection::nodeAt()

public:

    CSelection find(std::string aSelector);

    CNode nodeAt(size_t i);

就在這短短的幾天內剛剛好這個3rd party就把header改過且標準化了...

這故事告訴我們

  1. Version Freeze很重要,不要像哥一樣耍帥什麼都拿latest來編
  2. 可能的話盡可能寫script去build from source且拿該source的header來編,不要自己留一份版本不明的3rd party header在專案裡面用
  3. 學到不少東西(虛脫)

這個專案也被原作者放置好一陣子了,正好我最近有點時間就看了一下就順便出了個1.5.0版出來。

Github Release網址
目前已經更新到2.0.0版了,增加了Smart Paste算是殺手級(?)功能吧

其實比較勤在更新的是Welly,但是這個專案我沒辦法讓他順利的在XCode9 (with macOS 10.14)編譯起來,一堆ARC問題,我想作者之所以很久沒更新大概也是因為這個原因吧。相對於Welly,Nally其實也是一樣編譯不起來,但是至少我稍微修一下還修得動...那就用Nally改吧。不然其實Welly應該是比Nally功能完整很多的。

大概就多加了兩個我比較常用的功能,以下擷取自README.md。我相信如果Welly的作者更新了自己的電腦或者作業系統,Welly應該也有不少東西要克服才能Build起來吧,所以我想應該短期間內他也不會有更新了。

1.5.0 (修改版自Rayer)

  • Release date : 2019.03.10
  • 增加了Google image search的功能。當選擇了一個以.jpg或者.jpeg為結尾的網址的時候,右鍵選單將會增加Search by Google Image.
  • 增加了貼上Tinyurl的功能。當拷貝一個網址時,且Telnet視窗內沒有選擇任何文字時,將會有Paste by TinyURL的選項。
  • 由於現在的XCode已經不支援10.6的Deployment Target,很遺憾的目前Deployment Target必須要至少macOS 10.12以上才能執行
  • 同樣原因,重新編譯後原因不明有一個小Bug : 當打開了複數個BBS,滑鼠游標無法點選上面的BBS Tag,包含關閉視窗的X。請用快速鍵cmd + <- / cmd + -> / cmd + w來執行選擇左邊分頁/右邊分頁/關閉分頁的動作。

常見的錯誤 : return str.c_str()

這個錯誤好像通常不太會炸... 不過由於是未定義行為,所以炸不炸鍋會看運氣。

最經典的例子是繼承std::exception

class myexception : std::exception {
    myexception(...) : std::exception() {}
    const char *what() const noexcept override {
        return .....; 
    }
};

老實講我不知道為什麼exception當初要這樣設計,舊有的C並沒有exception的概念,而既然是從C++才開始引入,卻不是用string or stream卻使用超舊世代問題滿滿的const char*來當回傳值,這滿奇怪的。不過讓我們看看各種嘗試 :

丟出literial string (C-style String)

基本上這兩者編譯起來,是完全一樣的 :

const char* myexception::what() const noexcept {
    return "Hello World!";
}

//or

const char* myexception::what() const noexcept {
    const char* ret = "Hello World!";
    return ret;
}

先看看第二個,看起來很像傳回了一個Dangling Pointer,但是事實上ret的位置在絕大多數的compiler都會指向.rodata或者.data,所以雖然沒有保證,但是這個指標生命週期並不是僅止於what()

第一個其實跟第二個完全一樣,只是ret變成匿名物件而已。

同樣是const char*, 看看std::string的場合?

通常來講exception會需要動態產生what(),在log裡面通常就是印出what()。所以動態產生what()內容的話,首選當然是std::string。這範例寫法頗爛,不過不影響,正常產生格式化字串我會推boost::format()

const char* myexception::what() const noexcept {
    std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

這種寫法是最多人犯的錯誤(但是奇怪的是,爆炸率似乎不高?),同樣是const char*,這樣寫是有問題的。std::string為了相容性的原因提供了c_str(),主要是為了讓以前能吃c-style string的東西能吃得進去,為了避免外界沒透過std::string就改內容造成錯誤,所以回傳const char*

但是ret本身就是一個local variable, 一般來說回傳ret有沒有問題通常是看函數簽名回傳的是value還是reference,後者的話穩死不用說,大家都是專家,前者的話則會啟動copy constructor。先不討論回傳值最佳化Copy Elision,又稱為RVO或者std::move

//沒事,會啟動copy constructor(不討論ROV)
std::string test() {
    std::string ret = {"Hello World!"};
    return ret;
}

//有事,回傳一個local variable的reference
std::string& test() {
    std::string ret = {"Hello World!"};
    return ret;
}

既然大家都知道local變數的生命週期僅止於function scope,那這個local的內部指標當然生命週期也僅止於這個local。回到上一個例子

const char* myexception::what() const noexcept {
    std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

附帶一提,有想過為什麼std::string不提供隱式轉換成const char*嗎?這是其中一個原因。不過大家都知道,他有提供const char*隱式轉換成std::string的轉換operator。

那改用static?

const char* myexception::what() const noexcept {
    static std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

可以,但是代價很大。這會造成thread unsafe以外,所有的exception都共享一個ret。當你需要exception包exception的時候,或者multi-thread的時候,這會造成一些讓人抱頭痛哭的問題。

所以....正確答案是類別變數?

const char* myexception::what() const noexcept {
    this->m_ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

很不幸這東西無法通過編譯,std::exception的what()被後綴const保護著,而繼承者無法鬆掉這層限制。後綴const基本上會阻止你的類別變數被修改跟讀寫,而且會造成一些滿難懂的錯誤訊息。

所以我想要客製化what()的回傳該怎麼辦?

答案是,其實what()呼叫時機已經是很後面了,所有該有的資訊都有了,所以你不該在裡面做non const的行為。你應該在exception建構子裡面就把這件事情做好,拿我的專案為例子:

namespace Iris {
    class SqlException : public DPLException {
        std::string sql;
    public:
        SqlException(const std::string &reason, const std::string &sql_command = "",
                     const std::string &driver_output = "")  noexcept;
        const char *what() const noexcept override;
        ~SqlException() override = default;
    };
}


const char *Iris::SqlException::what() const noexcept {
    return what_output.c_str();
}


Iris::SqlException::SqlException(const std::string &reason, const std::string &sql_command,
                                 const std::string &driver_output) noexcept : DPLException(reason, driver_output) {
    this->sql = sql_command;
    boost::format output =
            boost::format(
                    "\nSQL Exception raised\nOS message : %1%\nsql_command : %2%\nreason : %3%\ndriver_output : %4%\n")
            % exception::what() //Don't use what(), it will cause loop. Dont use DPLException::what() too, will include duplicated message
            % sql
            % reason
            % driver_output;
    what_output = output.str();
}

通常來講,一開始就這樣做會少走一點冤枉路。

在CMake裡面使用Google Test

之前想在CMake專案裡面使用,似乎很少有人提到如何把Google Test跟CMake很好的整合起來,所以寫了一篇簡單的心得來做個紀錄。

首先取得Google Test

我個人的習慣是把目錄分成這幾個目錄,我拿我現在正在寫的一個小專案來做例子:

.
├── CMakeLists.txt
├── README.md
├── deps
│   └── googletest
├── main.cpp
├── src
│   └── IHost.h
└── test
    └── twitter_basic.cpp

其中deps專門用來放3rd party dependency。我個人是不太建議使用系統安裝來裝這種那麼小的東西,而且這會造成別人使用這個project的麻煩。

我自己是習慣使用git submodule來裝這種git專案就能拿到的東西,所以取得方法會像是這樣 :

➜  Octo git:(master)cd dep
➜  dep git:(master) ✗ git submodule add git@github.com:google/googletest.git
Cloning into '/Users/rayer/Develop/Octo/dep/googletest'...
remote: Counting objects: 7670, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 7670 (delta 3), reused 7 (delta 2), pack-reused 7654
Receiving objects: 100% (7670/7670), 2.61 MiB | 356.00 KiB/s, done.
Resolving deltas: 100% (5686/5686), done.

這樣deps底下就會有googletest這個子目錄了。其他人clone你的專案的時候,記得在README.md提醒一下對方要先git submodule init來取得所有的submodule。

當然取得google test的source code方法不只一種,你可以直接在deps裡面下git clone,或者直接download source code硬解到底下,結果應該差不多。

CMake裡面的設定

Google Test完整支援CMake,所以整個構造會很簡單。其中下面的test/twitter_basic.cpp就是你寫你TEST TEST_F TEST_P的位置,請自己建立一個檔案。

# 可加可不加,在這個範例不影響
enable_testing()

# 把google test整個cmake managed dir全部加進來
add_subdirectory(deps/googletest)

# 宣告include目錄有google test,通常順便連googlemock一起加入
include_directories(deps/googletest/googletest/include)
include_directories(deps/googletest/googlemock/include)

# 測試的主體。main宣告google test已經幫你宣告好了,直接用就是,就是gtest_main.cc
add_executable(twitter_test deps/googletest/googletest/src/gtest_main.cc test/twitter_basic.cpp)
# Link gtest library
target_link_libraries(twitter_test gtest)

這樣基本上就能跑了,看起來會像這樣:

Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from SANITY
[ RUN      ] SANITY.sanity
[       OK ] SANITY.sanity (0 ms)
[----------] 1 test from SANITY (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

我在寫Daemonizer(註一)的時候,碰到了一個很奇怪的語法錯誤

ifstream f(this->pid_file_name());
string content(istreambuf_iterator<char>(f), istreambuf_iterator<char>());
pid_t pid = stoi(content);

他會跑出error,聲稱content是一個function所以無法當做stoi的引數。然而,要是這種傳法就能work

string content("123");
// OR
string content = string(istreambuf_iterator<char>(f), istreambuf_iterator<char>());

這錯誤的主要成因是,第一個例子中的string content(istreambuf_iterator<char>(f), istreambuf_iterator<char>());被錯誤的當成了function prototype(話說,哪個use case會要在函數本體內宣告另外一個函數的prototype?)。這種情況叫做Most vexing parse

C++11提供了一個統一初始化方法,也就是說,你用一個大括號把它包起來,他就一定會被認為是個物件初始化,而不是一個function prototype。C++11以前的話,只能再多一組括號把它包起來。

//C++11
string content{istreambuf_iterator<char>(f), istreambuf_iterator<char>()};
//C++11以前的方法
string content( (istreambuf_iterator<char>(f), istreambuf_iterator<char>()) );

真是個討人厭的慣例啊(抓頭)....

(註一) 一個類似daemon的linux application,但是他是code層級,你只要繼承他,他就會幫你搞定所有Daemonize的事情

最近在玩AngularJS跟Google Application Engine,
PyCharm預設的組合就是Flask + Jinja2
配著Flask教學加上AngularJS教學,三分鐘就踢到了鐵板

<div ng-app="">
        <p>Name : <input type="text" ng-model="name"></p>
        <h1>Hello {{name}}</h1>
</div>

這個簡單到不能再簡單的東西,在網頁上永遠是錯誤的結果
本來對話筐輸入會馬上呈現在Hello後面,但是始終Hello後面都是一片空白
(正確結果請參照 這裡 )

本著實事求是的精神,打開safari收到的html仔細端詳了一下,原來他變成

<div ng-app="">
        <p>Name : <input type="text" ng-model="name"></p>
        <h1>Hello </h1>
</div>

搞個半天原來是{{ }}同時也是Jinja2的data binder的識別符號
Jinja2在render這個html的時候(render_template() )順手把沒bind到的東西移除了

當然直接拿掉Jinja2的renderer也是個方法
(就把每個endpoint最後的return render_template('template.html')
改成直接輸出template.html內容就可以了)
不過這種CGI式的renderer還是在某些場合挺方便的,不想這樣說宰就宰

後來查個半天,把jinja2的data binder識別符號換掉就好
不過網路上找到的解法是建議順便把其他兩組一起替換掉

jinja_options = app.jinja_options.copy()

jinja_options.update(dict(
        block_start_string='<%',
        block_end_string='%>',
        variable_start_string='%%',
        variable_end_string='%%',
        comment_start_string='<#',
        comment_end_string='#>'
))
app.jinja_options = jinja_options

暫時解決了這個問題了... 三分鐘就撞牆,花30分找解法 =口=

Markdown一個最麻煩的地方就是缺乏文字色彩的原生支援。之前查了網頁,10個裡面有8個說沒有,兩個說用css。但是後來我想起來Markdown本身就有原生支援html語法,那就簡單了...

我們要把<font color="red">文字</font><font color="blue">變成</font><font color="green">彩色</font>

我們要把文字變成彩色

恩,其實滿直覺的,怎麼一開始沒想到這招囧...