2017年12月31日 星期日

在網頁上測試 C++11 Regex -- 初試 emscripten

最近在摸熟 C++11 導入的 Regex 函式庫中使用的 regular expression 語法。C++11 Regex 預設是採用 ECMAScript (也就是 JavaScript) 中定義的 regular expression 規格 (詳見 spec),而該語法並不完全相容於我比較熟悉的 awk/egrep 中所使用的 regular expression 語法。

最快摸熟的方式當然是實際去試試看。最先是很快地寫一個簡單的 C++ 命令列程式,反覆餵它一些資料然後觀察運作結果。幾次後發現這樣的土法煉鋼法,最好還是配合 GUI 程式比較
便於操作。正準備要動工時,腦袋中突然閃過前陣子在 SlideShare 上看到的一個簡介 emscripten 的投影片。透過 emscripten 的協助,我大可直接把原本那個 C++ 命令列程式轉譯成 asm.js 嵌入網頁中,再小改一下輸入輸出的部分,最終得到一個網頁 GUI 的版本。




簡單來說,emscripten 是利用了 LLVM 的基礎,透過LLVM 家族的 clang/clang++ 編譯器,先把 C/C++ 程式編譯成一個虛擬的中間碼 (IR, Intermediate Representation) 形式,然後 emscripten 再負責把 IR 翻譯成對應的 asm.js 碼。asm.js 是 JavaScript 的嚴格子集,也就是說 asm.js 可以被執行在任何可執行 JavaScript 的平台上,包括了網頁瀏覽器。若想了解更多細節,推薦阮一峰的這篇入門教程。以下就不廢話,直接說說我是怎麼把一個命令列 C/C++ 程式透過 emscripten 移植成網頁版本的。

首先,原本的程式大概是長這樣子:


#include <iostream>
#include <regex>
#include <string>
#include <sstream>

std::string
cpp11regex(std::string regex, std::string content)
{
 std::stringstream out;
 std::regex regexExpr(regex, std::regex_constants::ECMAScript);

 std::smatch results1;
 bool r = std::regex_match(content, results1, regexExpr);

 out << "std::regex_match" << std::endl;
 out << "    return value  : "  <<  (r ? "true" : "false")  << std::endl;
 out << "    match results : (" << results1.size() << ")" << std::endl;
 for (unsigned int idx = 0; idx < results1.size(); ++idx)
 {
  out << "        " << idx << ")" << std::endl;
  out << "            pos   : " << results1.position(idx) << std::endl;
  out << "            length: " << results1.length(idx)   << std::endl;
  out << "            str   : " << results1.str(idx)      << std::endl;
 }

 out << std::endl;

 std::smatch results2;
 r = std::regex_search(content, results2, regexExpr);

 out << "std::regex_search" << std::endl;
 out << "    return value  : "  << (r ? "true" : "false")   << std::endl;
 out << "    search results: (" << results2.size() << ")" << std::endl;
 for (unsigned int idx = 0; idx < results2.size(); ++idx)
 {
  out << "        " << idx << ")" << std::endl;
  out << "            pos   : " << results2.position(idx) << std::endl;
  out << "            length: " << results2.length(idx)   << std::endl;
  out << "            str   : " << results2.str(idx)      << std::endl;
 }

 return out.str();
}

扣除七成以上的資訊輸出相關代碼,實際上就只是收兩個字串 (regex、跟輸入文字) 然後把所有輸出資訊累積到一個 stringsteam 中,最終取出 stringstream 內的 string 返回。當然,另外還應該有一個 main 函數,但那邊純粹就只是做一些雜事,然後調用這個 cpp11regex() 函數而已。轉成網頁版後也用不上,我就不列上來了。

我的目標就是把這個 cpp11regex() 的內容透過 emscripten 的協助,翻譯為 asm.js 給網頁上的其它 JavaScript 代碼來呼叫。

第一步是要在自己的電腦上建置 emscripten 的開發環境,可以參考官網上簡短有力的環境安裝說明。我是 Windows 環境,真的就只是下載一個 zip 解開、再按網頁列的去執行一些指令就搞定了。輕鬆無痛。

裝好之後的使用方法也很簡單,直接改用 "emcc" 和 "em++" 取代慣用的 "gcc/clang" 和 "g++/clang++" 指令就好了,其它編譯器參數基本上都通用。如果你的程式沒用到特殊的 system call、沒有開 thread、不依賴硬體裝置 (Video/Audio,等等),很有可能不需任何修改直接就可通過編譯了。不過產生出的是 .js 檔案而非 binary,不指定 "-o" 參數的話,預設產出檔名為 a.out.js。另外特別提一下,第一次編譯時,emscripten 必需把 C/C++ 程式中會使用到的 所有函式庫也一併轉譯為 asm.js,這包含最基本的 libc、C++ runtime 等等。所以第一次編譯會需要比較久的時間。

編譯完成之後,接著是第二步: 該怎麼把 C/C++ 的函數 "開放" 給網頁上的其它 JavaScript 呼叫呢? emscripten 提供了一個叫做 Embind 的機制來達成這件事 (見說明網頁)。套用在我的例子上,單單只需額外引用一個 header,然後透過巨集設定一個開放函數的列表,就完工了。修改後的代碼如下 (有標 +++ 的兩處異動):


#include <iostream>
#include <regex>
#include <string>
#include <sstream>

// +++
#include <emscripten/bind.h>

std::string
cpp11regex(std::string regex, std::string content)
{
 std::stringstream out;
 std::regex regexExpr(regex, std::regex_constants::ECMAScript);

 std::smatch results1;
 bool r = std::regex_match(content, results1, regexExpr);

 out << "std::regex_match" << std::endl;
 out << "    return value  : "  <<  (r ? "true" : "false")  << std::endl;
 out << "    match results : (" << results1.size() << ")" << std::endl;
 for (unsigned int idx = 0; idx < results1.size(); ++idx)
 {
  out << "        " << idx << ")" << std::endl;
  out << "            pos   : " << results1.position(idx) << std::endl;
  out << "            length: " << results1.length(idx)   << std::endl;
  out << "            str   : " << results1.str(idx)      << std::endl;
 }

 out << std::endl;

 std::smatch results2;
 r = std::regex_search(content, results2, regexExpr);

 out << "std::regex_search" << std::endl;
 out << "    return value  : "  << (r ? "true" : "false")   << std::endl;
 out << "    search results: (" << results2.size() << ")" << std::endl;
 for (unsigned int idx = 0; idx < results2.size(); ++idx)
 {
  out << "        " << idx << ")" << std::endl;
  out << "            pos   : " << results2.position(idx) << std::endl;
  out << "            length: " << results2.length(idx)   << std::endl;
  out << "            str   : " << results2.str(idx)      << std::endl;
 }

 return out.str();
}

// +++
EMSCRIPTEN_BINDINGS(my_module) {
 emscripten::function("cpp11regex", &cpp11regex);
}

Embind 有能力可以自動推導一些基本的參數型別對應方式:

  • C/C++ 的 void 對應到 JavaScript 中的 undefined
  • C/C++ 的 bool 對應到 JavaScript 中的 true 或 false
  • C/C++ 的 std::string / std::wstring 可對應到 JavaScript 中的 string
    (std::string 另可對應為其它型別 )
  • C/C++ 的 char/short/int/long/long long/float/double、以及 unsigned 版本,全部對應到 JavaScript 中的 number
如果基本的參數型別不夠用,想要傳遞自定義的 class/struct/enum,也是有辦法但比較麻煩

最後一步就是網頁端的工作了,先聲明一下我本人並非網頁前端的專業背景,下面這些 HTML 的東西我也是邊 Google 邊拼湊出來的。內行人路過就將就一下遮遮眼吧。主要就是把我需要的文字輸入框排出來,加個按鈕,按鈕按下時會去呼叫轉譯好的 cpp11regex 函數,然後把返回的字串丟進另一個文字輸入框來呈現。


<!doctype html>
<html>
  <script src="emscripten_cpp11regex.js"></script>
  <script>
    function performMatch() {
      document.getElementById("edbox_result").value =
        Module.cpp11regex(document.getElementById("edbox_regex").value, 
          document.getElementById("edbox_input").value);
    }
  </script>
  
  <body>
    <form>
      Regex (ECMAScript):<br/>
      <textarea id="edbox_regex" name="Text1" cols="80" rows="5"></textarea><br/>
      Input Text:<br/>
      <textarea id="edbox_input" name="Text2" cols="80" rows="5"></textarea><br/>
        
      <br/>
      <input type="button" value="Match" name="btn_match" onclick="performMatch()"><br/>
      <br/>
        
      Result:<br/>
      <textarea id="edbox_result" name="Text3" cols="80" rows="30" readonly=true></textarea><br/>
    </form>
  </body>
</html>

關鍵就是 performMatch() 裡的那個 Module.cpp11regex() 函數呼叫。在 C/C++ 中丟給 Embind 的函數,在 JavaScript 中統一都是用 Module.{你的函數名} 來調用。

結果就是這樣囉:

可運作的版本有打包一份放在此,下載後解開點 run.bat 就行了。
因為大部分的瀏覽器都會阻擋 file:// 形式的網頁執行 JavaScript,所以這包內有一個 devd HTTP 服器,點 run.bat 時會把它跑起、以當前目錄內容為根內容,然後打開預設的瀏灠器檢視  http://127.0.0.1/test.html 這個頁面。


補充說明:

  • 使用 emcc / em++ 編譯時,可指定 -O (優化級別)。事實上,優化級別對譯出來的 asm.js 大小有蠻大的影響。以我的例子,不設定優化時譯出來的 asm.js 足有 2MB 之多。而設定 -O3 之後,縮減為約 500 KB。但要特別小心一點,若使用 -O2 (含) 以上的優化級別時,asm.js 的內容會改為異步載入,因此在網頁中務必要等頁面完整載入完畢後,再去使用 asm.js 的內容。否則會出現找不到 Module.XXXX 的錯誤 (XXXX 是你由 C/C++ 轉譯出、放在 asm.js 中的函數)。請參考官方說法


沒有留言:

張貼留言