2015年7月14日 星期二

Python, JavaScript, C++, Java 和 Unicode 以及 UTF-8 等編碼

基本觀念

Unicode 是一張表,定義每個文字對應的數字是什麼,比方說 'A' 是 65, '我'是 25105。

UTF-8, UTF-16, UTF-32, UCS-2, UCS-4 等則是 Unicode 的各種編碼。所謂的編碼,是某種定好的儲存格式 (或說 serialization/deserialization format 也許比較好懂?), 方便讓不同的應用程式之間傳遞資料。

比方說應用程式 A 寫入文字到檔案裡,應用程式 B 從檔案讀出文字,兩者要說好用的編碼格式 (或能從資料辨別使用的編碼格式), 才有辦法互通資料。舉例來說, 網路傳輸常用 HTTP 作為溝通協定, HTTP 有提供 client 或 server 指定傳輸內容編碼的方法 (Accept-Charset 和 Content-Type), 可以指明是用 UTF-8 或其它編碼。

寫程式的時候,基本上是假設外部讀來的資料是某種 Unicode 編碼 (最常用 UTF-8 ), 內部使用 Unicode 處理,要傳出去 (寫入硬碟/網路傳輸) 前再轉回 Unicode 編碼。用 Unicode 的好處是,可以正確計算字串長度, 這影響到使用 length, index, 取出 substring, 比對 substring (包含 regexp) 等字串 API。

這裡有以前寫的介紹,有多提一點相關的事。

Python

Python 2 就有定義 type unicode 和 type str。type str 可看成一般性的 binary, 以 byte 為單位。Python 2 的字串 '...' 的型別是 str, 要特別用 u'...' 才會改用 unicode。

有了 type unicode, 處理 unicode 比較輕鬆:

  1. 外部讀進來的字 (UTF-8) 存在 type str, 馬上轉成 unicode
  2. 函式的輸入輸出都用 unicode (len(), [] 的行為正確)
  3. 要輸出到螢幕、檔案、網路時再轉成 UTF-8。

參考 All About Python and UnicodeUnicode in Python: Common Pitfalls 了解細節。

另外, Python 可能用 UCS-2 或 UCS-4 實作 Unicode, 可從 sys.maxunicode 的值得知, 65535 表示用 UCS-2。因為 UCS-2 / UCS-4 每個字分別占 2 / 4 bytes, 對使用的記憶體的量有很大的影響,不同作業系統會有不同考量, 像 Mac OS X 用 UCS-2, Ubuntu 12.04 用 UCS-4。然後在 Python 3.3 後都支援 UCS-4 了 (因為有實作 PEP 0393 -- Flexible String Representation)。

例如 u'𝌆' 會被當成兩個字 [*1]:

In [1]: u'我'
Out[1]: u'\u6211'

In [2]: len(u'我')
Out[2]: 1

In [3]: u'𝌆'
Out[3]: u'\U0001d306'

In [4]: len(u'𝌆')
Out[4]: 2

Btw, Python 3 改變字串預設的型別: '...' 是 unicode, 要用 b'...' 才會用 str,用意是希望大家預設就用 Unicode 避免混亂,不過卻造成 2 升 3 的一些問題。直接引用以前碎碎唸的心得:

在 python2.7 踩到 python3 unicode 的雷,import unicode_literals 會讓 'abc' 自動變成 u'abc' ( 原本的行為是 ‘abc' == b'abc') ,難怪怎麼看都不太對勁,third-party code 寫 'abc',別的 third-party package 預期收到 raw string 卻收到 unicode string, 於是回報 TypeError: 'unicode' does not have the buffer interface

用 ipdb 除錯時覺得相當奇怪,一樣的程式碼 (如 s = 'abc'),在 ipdb 內執行得到的結果是 raw string, 但原本的程式執行時卻變 unicode ....

搜了一下才看到也有不少人反應這點: Proposal to not use unicode_literals #22 https://github.com/PythonCharmers/python-future/issues/22

JavaScript

JavaScript’s internal character encoding: UCS-2 or UTF-16?: 詳細到不行的介紹,沒耐性就直接看文中的 conclusion 吧。

結論是 JavaScript 直接用 UCS-2 表示字元, 不像 Python 分成 str 和 unicode。開發者幾乎不用操心 Unicode、UTF-8 之類的事, 反正在 JavaScript 內都是用 UCS-2 表示。缺點是超出 65535 的 Unicode 不會被正確處理:

> '我'.length
1
> '𝌆'.length
2

不過網頁還是可以正確顯示字的外觀,大概是因為瀏覽器內部用 UTF-16 處理,字的 Unicode 仍然是對的,畫字時有選對 font glyph。但是受限於 JavaScript 的語言規範,只能斷成多個字表示。

C++

C++ 沒有定義型別 Unicode, 要自己用 std::string, std::wstring 或其它型別表示。

查到幾種轉換方法

  • 用 C++11
  • 用 Boost
  • 用 icu
  • utf8-cpp

最後一個作法最簡單, include 幾個 header 檔即可,不像其它方法要改 compiler 參數或引入一大包函式庫。所以我後來是用 utf8-cpp, 待要非得用到其它方法時再來研究為啥需要其它方法。stackoverflow 裡有很多討論 (由此可見 C++ 使用 Unicde 的方法有多混亂....), 這是其中一篇

Java

Java 的基本單位 char 一樣用 UCS-2 實作, 但 String 提供另外的 API 可正確存取 Unicode。直接引用別人的結論:

Han Guokai wrote on 20th January 2012 at 18:28:

In Java5, ‘char’ means code unit (16-bit), not character (code point). One character uses one or two chars. String’s length method return the char count. And there are some additional methods based on code points, i.e. codePointCount. See: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html

比方說 Java String 提供 charAtcodePointAt。後者是正確的 Unicode 字元,而前者是 char, 遇到 UCS-2 外的字一樣會出錯。

結論

總算弄清楚過去常用的四種語言實作 Unicode 和 Unicode 編碼的方法,結果四種語言有四套作法, 真是出乎意料出外...。推薦閱讀JavaScript’s internal character encoding: UCS-2 or UTF-16? 了解 BMP, supplementary planes 和 surrogate pairs, 會比較清楚 JavaScript 和 Java 使用 UCS-2 作為字元定義遇到的問題 (為了省記憶體, 是必要的 trade-off)。

備註

1. Blogspots 無法正式顯示這個字, 可從這裡看到測試用的字。https://codepoints.net/ 可找到更多範例。

2015/07/19 更新

Scott 指正, 更新內文對 Python Unicode 實作的描述。

2015年7月11日 星期六

從舊有的子目錄建立新的 git repository

如果要搬移的目錄在 repository 的最上層,可以用 git subtree。若是要搬的目錄是中間某層目錄 (例: a/b/c 的 c), 可以用 filter-branch

筆記一下我操作成功的流程:

1. 在 git server 上建立新的 headless repository

git-host$ cd /path/to/repo && mkdir new_repo && cd new_repo && git init --bare

2. 在自己的機器上從原本的 repository filter 出要搬移的目錄。注意: 目錄內的內容會變成只剩目標目錄,我是 clone 一份新的來作。

myhost$ git clone ssh://path/to/old_repo
myhost$ cd old_repo
# Filter the master branch to path/to/folder and remove empty commits
myhost$ git filter-branch --prune-empty --subdirectory-filter path/to/folder master
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/master' was rewritten

3. 合併舊有的 git commits 到新的 git repository 並更新回 server

myhost$ cd /path/to/somewhere
myhost$ git clone ssh://path/to/new_repo
myhost$ cd new_repo
myhost$ git pull /path/to/old_repo # 取得剛才 filter 過的 old_repo
myhost$ git push --set-upstream origin master
myhost$ rm -rf /path/to/old_repo  # 移除用不到的目錄

2015年6月22日 星期一

使用 Node.js 實作 server push

前幾天才說用 Tornado 實作 server push 很簡單, 且可以直接用在上線環境裡。今天要來自打嘴巴說我要改用 Node.js 了。有興趣了解相關基本知識的人, 可以參閱之前的說明

Python 圈 event-based framework 的問題

這篇文章所述, Python 天生不是 async, 既有函式庫自然也不是 async, 使用 event-based framework (*1) 後, 要另外找配套的函式庫。比方說 http client 用慣了 requests, 直接拿到 Tornado 裡用會出事, 因為 Tornado 是 single process single thread 的架構, 一發出網路連線就會 block 住全部動作。要嘛改用 Tornado 提供的 httpclient, 不然就要找別人用 Tornado 提供的 API 包好的 requests

每套函式庫都這樣找頗麻煩的 (資料庫、第三方服務的 SDK、etc), 而且不見得找得到牢靠又有效率的版本, 比方說 Google API Python Client 就沒有 Tornado 版的, 要自己想辦法弄。以前用 gevent 時作過類似的事, 先搞定 Dropbox Python SDK, 再來搞不定 Google Drive API, 只好用 gevent monkey patch。感覺不是很可靠。

Node.js 的優勢

反觀 Node.js 天生就是 async, 既有函式庫自然也是 async。Google 也有提供 Google API Node.js Client。既有的社群也滿龐大的, 可以用的套件很多。對我來說, 這點最重要。

效能方面, 因為使用 V8 轉成 native code 執行, 表現也不錯。從這裡這裡的 benchmark 來看, 數值計算大勝 CPython, 然後和 PyPy 持平。我自己比較常用 dictionary, 作個簡單的 benchmark 測試使用 for, if 和 dictionary, 結果 Node.js 小勝 PyPy, 然後大勝 CPython。整體來說執行速度夠用了, 到是記憶體有 1.7G 的限制 (64bit OS 預設 1G), 要多留意一下。

使用 Express 作 web server 提供簡單的 API, 然後用 ab 作簡單的 benchmark, 結果效能比 Tornado 好 (或可視作差不多, 都達到標準), 一杪內湧入 1k 連線可在一秒內解決; Node.js 底層使用 libuv, 承受 C10k 以上的連線也不是問題。

此外, 配合既有寫網頁的經驗, 改用 Node.js 開發少了學習新語言的成本, 這也是一個優勢。

Server Push 實作

這裡有完整的程式碼, 我只有作最基本功能, 沒控管記憶體也沒提供 timeout 之類的額外參數。寫起來很直覺, /get 沒有資料的時候, 就產生一個 callback function 存到全域變數 g_cache 裡。待 /set 再逐一呼叫這些 callback function, 完成 long polling 的要求。

var error = {
  ok: 0,
  invalid_input: 1,
};

var g_cache = {};

app.get('/set', function (req, res) {
  var key = req.query.key;
  var value = req.query.value;
  if (key === undefined || value === undefined) {
    res.send(JSON.stringify({
      error_code: error.invalid_input
    }));
    return;
  }

  if (!(key in g_cache)) {
    g_cache[key] = {};
  }

  var entry = g_cache[key];
  entry.value = value;
  if ('callbacks' in entry) {
    var cbs = entry.callbacks;
    for (var i = 0; i < cbs.length; i++) {
      cbs[i]();
    }
    while (cbs.length > 0) {
      cbs.pop();
    }
  }
  res.send(JSON.stringify({
    error_code: error.ok
  }));
});

app.get('/get', function (req, res) {
  var key = req.query.key;
  if (key === undefined) {
    res.send(JSON.stringify({
      error_code: error.invalid_input
    }));
    return;
  }

  if (!(key in g_cache)) {
    g_cache[key] = {};
  }

  var entry = g_cache[key];
  if ('value' in entry) {
    res.send(JSON.stringify({
      error_code: error.ok,
      value: entry.value
    }));
  } else {
    if (!('callbacks' in entry)) {
      entry.callbacks = [];
    }
    entry.callbacks.push(function() {
      res.send(JSON.stringify({
        error_code: error.ok,
        value: entry.value
      }));
    });
  }
});

Node.js 雜項備忘

備註

1. 我指使用 epoll 之類作法的架構。

2015-06-22 更新

Scott 提到 Go 也是不錯的選擇。搜了一下, 還真多人從 Node.js 跑到 Go。另一方面抽查了幾個 third-party 服務, 看起來 Node.js 的支援度比 Go 廣。對我來說, 現階段還是用 Node.js 比較划算 (學習成本和風險考量)。一兩年後再來評估看看是否值得改用 Go。

2015年6月19日 星期五

server push by long polling

程式碼

沒興趣讀落落長心得的人, 這裡有用 Tornado 實作的程式測試碼

心得

server push 是指從 server 主動送訊息給 client, 這裡有圖解一些達成 server push 的方式。其中我個人偏好 long polling 的作法,運作方式就和字面一樣: client 先發出一個連線, server 不要立即回應。等 server 需要主動通知 client 時, 再回傳資料。

這個作法的好處是:

  • 各種 client 都適用, 只要能用 http 連線即可, 不是瀏覽器也OK, 現在各個平台都有好用的 http 函式庫。
  • 不用擔心 client 網路環境問題。client 能主動建立往 server 的連線, 反過來就很難說了。

另一方面, long polling 的缺點是 server 會有許多閒置的連線占資源。像 apache 這類每個 client 用獨立的 thread 處理連線的作法, 就不適合處理 long polling。假設一個 thread 占據 10MB 空間, 1k 個不作事的連線, 就占掉 10G 記憶體了。另外大量 thread 之間的 context switch 也是可觀的時間負擔。

但是改用 epoll 之類的 API 寫成 event-driven 的架構, 可用 single process single thread 的方式同時處理大量連線, 就沒有浪費記憶體和 context switch 的時間負擔。現在有不少現成的工具使用 epoll 包成 green thread, 使用上和 multi-thread 一樣容易上手 (而且還不用擔心 race condition, 因為實際上只有一個 native thread),降低實作門檻。其中 Tornado 是 Python 寫成的 web framework 並內建可上線用的 web server。我作了簡單的 benchmark, 的確可以快速反應一秒內同時擁入的一千個連線。用 Tornado 省去不少工夫 (若不想用 web server 而是自己從頭作 server, 也可用 gevent)。

實作 long polling 的另一個問題是: 如何在察覺資料更新時, 能立即通知 client? 想像 client 透過連線 S 連往 server, server 先不作回應。接著 server 在其它地方取得新資料, 再透過 S 回傳資料。處理連線 S 的程式要怎麼暫停? 之後要怎麼返回執行? 這個作法可以 scale 嗎?

對於 single process 程式來說, 直覺且低成本的作法是: 在 thread A 讀資料, 發覺沒資料時改用 condition 變數 (搭配 lock) 停住程式, 然後在 thread B 收到新資料時, 用同一個 condition 變數通知 thread A 可以繼續讀資料。對應到 Tornnado 的寫法是用 locks.Condition:

condition = locks.Condition()

@gen.coroutine
def waiter():
    print("I'll wait right here")
    yield condition.wait()  # Yield a Future.
    print("I'm done waiting")

@gen.coroutine
def notifier():
    print("About to notify")
    condition.notify()
    print("Done notifying")

@gen.coroutine
def runner():
    # Yield two Futures; wait for waiter() and notifier() to finish.
    yield [waiter(), notifier()]

io_loop.run_sync(runner)

有了 locks.Condition, 要實作一個完整可上線使用的 long polling 只是小事一椿, 下面的程式碼實作兩個 API /get 和 /set: 提供基本的 "get value by key" 和 "set value for key"。/get 沒資料時會先停住不回應, 等到 /set 取得資料後再立即返回。這只是示意用程式, 沒防錯也沒控管記憶體的用量:

class Entry(object):
   ...

g_cache = {}

class GetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()

        value = entry.get_value()
        if value is not None:
            self.write(json.dumps({
                'error_code': ERROR_OK,
                'value': value,
            }))
            return

        # Wait the data to be ready.
        timeout = self.get_argument("timeout", DEFAULT_TIMEOUT)
        now = tornado.ioloop.IOLoop.current().time()
        timeout = now + float(timeout)
        condition = entry.get_condition()
        try:
            yield condition.wait(timeout=timeout)  # Yield a Future.
        except Exception, e:
            logging.exception('condition timeout? timeout=%s' % str(timeout))
            self.write(json.dumps({'error_code': ERROR_TIMEOUT}))
            return

        value = entry.get_value()
        self.write(json.dumps({
            'error_code': ERROR_OK,
            'value': value,
        }))


class SetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        value = self.get_argument("value", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()
        entry.set_value(value)
        condition = entry.get_condition()
        condition.notify_all()
        self.write(json.dumps({'error_code': ERROR_OK}))

application = tornado.web.Application([
    (r"/get", GetHandler),
    (r"/set", SetHandler),
])

完整的程式見開頭的連結。

apache ab 使用 post 傳資料

用法:

$ cat post.txt
key=value&key2=value2

$ ab -n 1000 -c 1000 -p post.txt -v4 -T 'application/x-www-form-urlencoded' http://.../

注意使用 -p 指定 post data 時,同時必須用 -T 指定傳送 content type 為'application/x-www-form-urlencoded。反應不如預期時可用 -v 輸出收到的資料,協助除錯。

2015年6月1日 星期一

Chrome NaCL 開發心得小記

目前開發到一半, 先加減記一下。之後有多什麼心得再來補充。

為什麼要用 NaCL

自然是方便重用既有的 C/C++ 程式啦。

雜項

  • NaCL 無法在本機檔案中執行 (file://...)。web pages、Chrome Web App、 Chrome Extension 才能用 NaCL。若要在本機試 web pages 的話, 需要跑 local web server。NaCL SDK 內有附一個 local web server, Getting Started 的範例用 make serve 執行 local web server。建議先玩 Getting Started 裡面的兩個範例, 了解 NaCL 的架構。
  • 有支援 POSIX 和 ptherad, 還算堪用, 不過 port Linux 的程式時會遇到很多 Linux 才有但 POSIX 沒有的程式碼 (網路、時間、pthread 各式各樣都有)。
  • 分成 PNaCL (編成 bitcode 執行檔, 使用前再轉成執行檔) 和 NaCL (native binary)。若需要放到 Chrome Web Store, 後者比較不方便, 得編多份不同平台的 binary
  • 得用 NaCL SDK 附的工具, 我試過用 nm 和 ar 都無效, 改用 NaCL SDK 附的 llvm-nm 和 llvm-ar 才有正常結果。
  • 若想使用 stdio 的函式 (如 FILE 相關函式), 要另外使用 nacl_io library。需要在 Makefile 補上 -I$(NACL_SDK_ROOT)/include -I$(NACL_SDK_ROOT)/include/pnacl-lnacl_io

除錯

參考資料: 官方文件

1. 基本的除錯方式是透過 stderr 導到執行 Chrome 的 terminal (預設行為), 或寫到 DevTools 的 console (透過 /dev/console1 ~ 3)。

但是導到 DevTools console 的輸出會被限制行寬, 超出的字會被截掉。還有由於 stderr 是 unbuffered, 輸出會亂掉, 若想將 stderr 導到 /dev/console1, C/C++ 程式內記得改設 stderr 為 line buffer ( setlinebuf(stderr) )

2. 開發 extension 時, 即使沒需求, 也可考慮加個 background page, 這樣可以長駐在 Chrome 內, 方便寫 log 到 background page 的 console, 用 DevTools 觀察 console 的 log 除錯。

3. 可以用 NaCL SDK 附的 nacl-gdb attch 到使用 NaCL 的 process, 但是手續頗麻煩的。但是不用 gdb, native code 出錯時更麻煩。

參考資料: Running nacl-gdb

我一開始在 Mac OS X + Pepper 42 試失敗, 後來在 Linux + Pepper 43 試成功了。不確定和平台或版本是否有關, 先前有遇到 Mac OS X + Pepper 42 的 bug (使用 nacl_io library 後, 輸出到 stderr 的資料沒有顯示在執行 Chrome 的 terminal 上), 但在 Linux + Pepper 43 沒事。

以開發 Chrome extension 為例:

  • 執行 Chrome 要加參數 --enable-nacl-debug --no-sandbox
  • 編譯時用 -O0 -g
  • 需要設一些 gdb 參數, 我包成兩個 script, 放在 native code 專案的根目錄:
$ cat run_gdb.sh 
#!/bin/bash

if [ "$NACL_SDK_ROOT" = "" ]; then
        echo "Please set the env variable NACL_SDK_ROOT"
        exit 1
fi

echo -e "\033[1;33m"
echo "Please ensure that Chrome is run with --enable-nacl --enable-nacl-debug --no-sandbox"
echo "Ref: https://developer.chrome.com/native-client/devguide/devcycle/debugging"
echo -e "\033[m"

cd $(dirname $0)
ROOT=$(pwd)
cd ../../
${NACL_SDK_ROOT}/toolchain/linux_x86_glibc/bin/i686-nacl-gdb -x sync/nacl/nacl.gdb
$ cat nacl.gdb 
target remote localhost:4014
remote get nexe my.nexe
file my.nexe
remote get irt my.irt
nacl-irt my.irt

先重新載入 extension, 再執行 run_gdb.sh, 成功後會出現 gdb 的 prompt。注意, 執行 Chrome 有加 --enable-nacl-debug 時, 就一定要跑 nacl-gdb, 因為 Chrome 會停住 NaCL process, 等著被 gdb attach。

在 Ubuntu 14.04 上編 stable chromium 43

參考資料

步驟

1. 從這裡找到最近的 stable version VERSION

2. 取得程式和安裝相依套件

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH=$PATH:/path/to/depot_tools
$ fetch --nohooks chromium
$ fetch --nohooks blink
$ cd src
$ ./build/install-build-deps.sh
$ git checkout VERSION  # 可和 git tag 對照一下
$ git show VERSION # 取得 commit 編號 COMMIT
$ (edit ../.gclient) # 設 "safesync_url" 為 COMMIT
$ gclient sync --with_branch_heads  # 沒有 --with_branch_heads 會出錯

3. 產生編譯所需的檔案和編譯

$ cd /path/to/chromium/src
$ gclient runhooks
$ ./build/gyp_chromium
$ ninja -C out/Debug chrome

在 Fedora 下裝 id-utils

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