MacArm 為 Qt 配置 WebAssembly 環境

需求

我正在配置 Qt wasm環境, 但是 emsdk 不支持 apple m1 issue 769
上一篇博文 我配置好 wasm 後發現 還是無法配合 qt 使用.

環境:
MacOS Bigsur M1, Qt 5.15.2

單單配置好wasm, 當前的配置還是不能工作, 比如說, 我現在用 clang 編譯一個 Qt 的 Example:

clang 可以跑起來

可以看到, 項目本身沒問題, 能跑起來;

我接著用剛剛配置的 em++ 編譯一遍:

得到一堆報錯

分析上面的報錯, 有一大堆 permission denied 或者 file not found. 結合 Docker 容器的特性, 我們就知道錯誤的根源了. 上面的配置, emcc 的路徑只和當前的路徑對應, 而在 qt 編譯過程中會引用很多不在當前目錄下的文件, 容器找不到, 就只能報錯.

那麼怎麼解決呢? 我們當然可以手動給他映射上去, 但是每一次都這麼幹, 碰到複雜的項目煩也要煩死. 而且如果像第一種沒有 WebAssembly Kit 的情況, qt 自己也不支持, 那麼要做的東西要海了去了. 所以說, 總是要有其他方法的.

這個方法還是 Docker.

解決方案

Qt 官網文檔: using-docker-test-qt-webassembly
maukalinow/qtwasm_builder : Docker Hub

以上文檔的意思是叫我們自己 Build 一個 Docker 鏡像. 但是顯然已經有人做過了, 本著不要重複發明輪子的定律, 我們可以直接將網友做好的鏡像 Pull 過來: (自行根據 Qt 版本選擇)

docker pull maukalinow/qtwasm_builder:5.15_latest

接著, 我們編譯一下剛才的項目測試一下:

docker run --rm -v ~/Desktop/anaclock/bin:/project/build -v ~/Desktop/anaclock:/project/source maukalinow/qtwasm_builder:5.15_latest
cd /Users/justin/Desktop/anaclock/bin
python3 -m http.server

接下來打開 這個地址: http://localhost:8000/analogclock.html

可以看到, 我們的 qt 應用已經在瀏覽器上面原生運行了.

結果非常成功

為 Qt 安裝 emsdk 到現在為止已經成功.

優化

加快項目生成速度

編譯的 Log

觀察以上編譯記錄我們可以發現, 絕大部分的時間都浪費在 generating system library 上了, 而這些庫無疑都是可以復用的. 所以接下來的事情, 就比較簡單了. 在命令後面加一條, 把緩存文件夾映射到宿主機上, 不就成了嗎?

docker run --rm -v ~/Desktop/anaclock/bin:/project/build -v ~/Desktop/anaclock:/project/source -v ~/.emsdk_cache:/root/dev/emsdk/upstream/emscripten/cache maukalinow/qtwasm_builder:5.15_latest

連著運行這個命令兩遍, 可以看出, 第二遍明顯就比第一遍快了 N 多倍.

第二遍編譯的 Log

簡化輸入命令

正如之前提到, 這麼長的命令並不利於記憶, 我們可以寫一個腳本讓他變短很多.

所以我就寫了一個腳本, 你可以在下面的鏈接下載到.

之後只要在目錄下輸入 qtwasm , 構建就自動完成了

Qt 添加 Mysql 驅動

需求

Qt 自帶的 SQLITE 驅動, 只支持本地的數據庫 . 而能支持在線數據庫的 QMYSQL 需要自己編譯.
本文的環境使用的是 MacOS Bigsur, Qt版本 5.14.2. ( Qt 不能為從 Brew 安裝的版本! )

解決方案

前往 官網 下載 SQLServer, 安裝到本地.
我們不需要他開機啟動, 安裝他只是為了獲得他提供的 Lib, 所以進設置關掉開機啟動.

乘著下載的時間, 我們設置一下 PATH

export QTDIR=/opt/Qt/5.14.2/clang_64
export LD_LIBRARY_PATH=$QTDIR/lib:$LD_LIBRARY_PATH
export PATH=$QTDIR/bin:$PATH
export PATH="/usr/local/mysql/bin:$PATH"

可以試一試運行 mysql, 如果能跑起來, qt 就應該可以監測到.

下載、安裝完成後, 定位到 Qt 安裝的目錄:

cd /opt/Qt/5.14.2/src/qtbase/src/plugins/sqldrivers

(我安裝在/Opt目錄裡面, 你的可能在其他地方.)

然後運行如下指令:

$ qmake -- MYSQL_PREFIX=/usr/local/mysql

Info: creating stash file /opt/Qt/5.14.2/Src/qtbase/src/plugins/sqldrivers/.qmake.stash

Running configuration tests...
Checking for DB2 (IBM)... no
Checking for InterBase... no
Checking for MySQL... yes
Checking for OCI (Oracle)... no
Checking for ODBC... no
Checking for PostgreSQL... no
Checking for SQLite (version 2)... no
Checking for TDS (Sybase)... no
Done running configuration tests.

Configure summary:

Qt Sql Drivers:
  DB2 (IBM) .............................. no
  InterBase .............................. no
  MySql .................................. yes
  OCI (Oracle) ........................... no
  ODBC ................................... no
  PostgreSQL ............................. no
  SQLite2 ................................ no
  SQLite ................................. yes
    Using system provided SQLite ......... no
  TDS (Sybase) ........................... no

Qt is now configured for building. Just run 'make'.
Once everything is built, you must run 'make install'.
Qt will be installed into '/opt/Qt/5.14.2/clang_64'.

Prior to reconfiguration, make sure you remove any leftovers from
the previous build.

如果你像我一樣看到 Mysql 對應的是yes, 那麼就可以進入下一步了.

make sub-mysql

如果沒有報錯的話, 可以接著運行:

make install

現在 再次編譯程序, 應該已經可以正常工作了.

如果需要打印當前 Qt 支持的驅動, 可以參考以下程序:

#include "mainwindow.h"
#include <QApplication>
#include <QtSql>

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	qDebug() << "Available drivers:";
	QStringList drivers = QSqlDatabase::drivers();
	foreach(QString driver, drivers)
		qDebug() << "\t" << driver;
	return a.exec();
}
安裝完畢後以上程序輸出

踩坑

  • 如果你需要在其他系統上正常運作程序, 你可能需要將 lib 文件拷貝過去, lib 文件位於 /opt/Qt/5.14.2/src/qtbase/src/plugins/sqldrivers/plugins/sqldrivers
  • 如果你用 qt5 的話, 強烈建議使用最新版. 比較舊的如 5.9 編譯時會報錯.
  • 如果你在沒有配置完成的情況下運行了一次 qmake , 然後後面怎樣 make 都提示 no, 應該是沒有把第一次 make 出來的東西清理乾淨, 首先

    make distclean #刪除 make 製造出來的所有文件

    然後參考 Qt 的 repo 把目錄裡多出來的東西清理一下 (注意你的版本要保持一致).
  • 如果不小心誤刪了文件其實也是沒有關係的, clone 一下 qt 的 repo 然後把對應版本的代碼考到你的 qt 安裝目錄重新安裝就可以了.

Qt IDE 從文件夾導入源代碼

需求

– 現有一個下載好的 Qt 項目
– 項目裡有很多子文件夾, 沒有 Pro 文件
– 想要導入這個項目, 生成 Pro 文件

解決方案

在源代碼根目錄下運行以下指令,生成 .pro 文件

qmake -project

打開 Qt Creator,點擊導入項目,這個時候應該會出現 Configure Project 的界面 (如果沒有,自己點擊左側的 Project 按鈕配置)

點擊 “Import build from…”, 選擇包含源代碼根目錄的文件夾(包含.pro文件的文件夾的上級目錄),點擊 Import

點擊 “Configure Project”, 等待…

項目導入完成.

C語言多線程編程實戰

我沒有專門學習過多線程,非常可能有更簡單的方法,我說了一大堆廢話只是繞了遠路。不過這次嘗試中應該還是有很多自己的感悟的,所以如果你是大神,覺得很滑稽,這個家夥寫得都是什麽垃圾啊,笑一笑就好啦 : ) 如果你是和我一樣的小白,歡迎共勉共同進步。

想要實現一個Text UI (我對命令行情有獨鐘,因為我做不出圖形界面) ,要控制光標同時繪制多個區域以及時響應。那麽這個就很明顯用到多線程了,奈何我對多線程一竅不通,於是…

  • 這是我理想中的窗體:
  • 實際畫出來的效果是這樣的:

這一坨坨條形碼,讓我頓時感到世界對自己充滿了惡意…

第一次排錯

這個其實很明顯。控制台的標準輸出就一個,多個線程控制著光標滿屏亂跑,A線程抱著光標鉆到草叢里還沒幹事請呢就被B線程橫刀奪愛,最後搞出來的東西自然是誰都不像。

我於是用了一個隊列,思路是這樣的:

  • 新建一個隊列q,函數queLock()、函數queUnlock()、Bool類型變量queueLock
  • 當A函數想要執行敏感操作(比如修改同一個變量)的時候,就去使用queLock()。這個函數會給它添加一個標識到隊列尾部。然後不斷檢查隊列頭和queueLock,如果queueLock變成false且隊頭輪到這個標識符了,就瞬間取出這個標識符然後鎖上queueLock
  • 當敏感操作完畢後,用queUnlock取消queueLock的鎖。
  • 這就相當於queLock()暫時鎖住了這個線程。雖然降低了部分效率,但是還是可以體現多線程的優勢的。

我的代碼是這樣的。

bool queueLock=false;
int ident=0;
queue<int> q;
inline void queLock(){
    int id=++ident;
    q.push(id);
    while(1){
        if(!queueLock&&q.front()==id){
            queueLock=true;
            q.pop();
            return;
        }
    }
}
inline void queUnlock(){
    queueLock=false; //inline會在編譯的時候直接插入代碼,因此無需擔心調用費時。
}

這應該解決了吧?

然鵝…

基本沒有變化!

圖樣圖森破啊!

第二次排錯

經過研究,我終於发現了問題所在。如果你正在看(且你不是我),你估計早就发現問題了。

int id=++ident;

這個完美無缺的函數第一行就出現了問題。

這個操作先給ident+1,然後將此時的ident賦值給id。這個連在一起寫可能看不出來,那麽我分開。

//上面的代碼等價於:
ident++;
int id;
id=ident;

這就很明顯了吧。另外一個線程完全有可能在前兩行或者後兩行中間橫插一腿。

所以這個id放在這里就是個花瓶。等於把問題從stdout的訪問沖突轉嫁到了ident的訪問沖突上。確實訪問效率更高了,但是沒有從根本上解決問題。

解決方案其實也很簡單。關鍵在於ident訪問沖突,這個ident是幹什麽的啊?用來區別線程的。區別線程我幹嘛要專門弄一個標識符,這不是畫蛇添足嗎??

直接上改正過的代碼了。

bool queueLock=false;
queue<HANDLE> q;
inline void queLock(HANDLE hThread){ //排隊+加鎖
    q.push(hThread);
    while(1){
        if(!queueLock&&q.front()==hThread){ 
            queueLock=true;
            q.pop();
            CloseHandle(hThread); 
            return;
        }
    }
}
inline void queUnlock(){
    queueLock=false;
}

經過這次調整,效果很明顯。发生問題的幾率降低了50%,錯亂也含蓄了很多,從大塊錯亂變成了標題移位。

還是不給力啊!百分之五十算個毛線??標題移位算個毛線???

第三次排錯

經過又一翻的 苦(拔) 思(禿) 冥(頭) 想(发) ,終於OK了。

仔細看下第二次的代碼,先留出一炷香的時間思考那里有問題。



想出來了嗎?

這個if語句看起來與世無爭,屬於誰都不會去考慮的類型,在一般的程序中是絕對沒有任何問題的。但是這是多線程編程,即使代碼寫的再緊湊,每條語句中間還是有延遲的。

上面的if語句可以拆解成下面的語句:

//與上面的語句等價
if(!queueLock){  //第一個判斷 
    if(q.front()==hThread){ //第二個判斷
            queueLock=true;
            q.pop();
            CloseHandle(hThread); 
            return;
    }
 }
inline void queUnlock(){
    queueLock=false;
}

那麽,如果甲線程在執行第一個判斷的時候,丙線程剛剛執行完畢,把queueLock給取消掉了。這個時候甲和乙齊頭並進,都完成了第一重判斷。這個時候甲線程率先完成第二重判斷並pop掉了自己,於是乙再次完成第二重判斷,和甲線程一起進入了多線程狀態。
(這里的乙可以換成任意一個非甲線程,丙丁戊己庚辛 隨便那個都可以。在這個程序里面肯定有一堆線程等著要出這個頭。)

解決方案就是,把if中的兩個條件調換一下順序。理解了BUG存在的原因,那麽怎麽去掉他就非常容易了。

代碼我就不貼了,直接貼圖我方便,你看著也方便。

那麽這樣運行出來之後就穩定的得到了本文的第一張圖,多次測試沒有變過,應該是成功了。

現在可以來驗證一下我的猜想,到底是不是這樣。

如果我的猜想成立的的話,那麽這個BUG发生的條件是”同時存在三個及以上的線程“。我於是去掉了一個線程(框框)反覆嘗試,確實,一直都沒有发生問題。

為什麽出現錯誤時只有標題移位我還沒有搞清楚。我猜想是系統提供的輸出函數會使用自己的方法去後移坐標位置,方法應該比我的SetCursorPosition()更加基礎,也更快一點。這種速度跟我的慢速沖突了,所以系統的輸出爭先恐後的湧到前面來導致了錯亂。

總結

在這個程序里我完成的事請事實上是在特定條件下把並行改為並发,用了隊列的這個結構。隊列中其實同時存在的元素上限恒等於同時存在的進程數上限。這個是使用了隊列結構的特性。

第一個問題(第零次排錯)的发生,只是用來作為本文的開頭提出問題的。可以對問題有個直觀認識,我畢竟還沒蠢到那個程度。

第二個問題(第一次排錯所引发的)是沒有認清楚解決問題的本質。我實際上幹了轉移問題的操作,而沒有從實質解決問題。問題從進程搶光標變成了進程搶標識符。

第三個問題(第二次排錯所引发的)是沒有細節。多線程編程是很講究細節的,if判斷還是建議分開來寫,不然大大延長出現bug後拔頭发的時間。

而且多線程編程還是對調試不太友好的(特別是命令行情況下)。你不能開一個調試窗格去搞它,你一開,結果又不一樣了。你不能去增大延時去仔細看輸出順序,因為你開延時之後就沒問題了。這個有點像薛定諤的貓,開箱之後你最終只能看到貓的死活,你看不到貓是怎麽死掉的。所以處理這種問題的時候還是建議自己開思維導圖逐行的推斷,給自己大腦編個碼,也許問題就解決了。

後話

剛剛发現,手動的一遍遍穩定性測試弱爆了….

其實如果真的要測試到底有沒有問題,寫一個線程不停搶資源就是了。

void cpu_eater(void*){
    while(1){
        queLock(getHandle());
        queUnlock();
    }
}
int main(void)  
{  
    _beginthread(win_playlist,0,NULL);
    _beginthread(win_menu,0,NULL); 
    _beginthread(win_progressbar,0,NULL);
    //下面是搶資源線程
    _beginthread(cpu_eater,0,NULL);
    _beginthread(cpu_eater,0,NULL);
    _beginthread(cpu_eater,0,NULL);
    //上面是搶資源線程
    sleep(LIFETIME_DELAY);
    return 0;  
}

一旦有問題,在三個線程不停跟他搶資源的情況下肯定立即就暴露出來了。這樣可以非常方便的一遍檢出問題。我剛剛寫完才发現有此等操作,真是冤煞我的Ctrl+F5。

下面是開了三個搶資源線程的動圖。

還蠻美觀的蛤。(這就完全沒問題了)

思考題

留一道思考題。

如果你不像我這樣戰五渣就試試看這道題。

上面的代碼我通過修正判斷的順序解決了問題。但是我如果不去修正判斷順序,而是修正執行順序,即:

調換這兩行順序,能不能一樣達成目的呢?

(偷偷透露下,不行。)

那麽為什麽不行呢?

答案:image-20200407183057649

自製 Goose 愚人節小病毒

愚人節前我亂翻小衆軟件,突然發現了這個東西,於是就有了靈感:
點擊前往: Desktop-Goose 小衆軟件

這是什麽

程序全屏運行示意圖

這是一個可以瘋狂啓動上述 呆鵝 的小程序,只有按照指令敲入句子才可以將其關掉。
簡而言之,這是一個 啓動器,用來啓動和關閉 Goose 程序,僅此而已。

技術細節

整個程序是我用蹩脚的 C++ 編寫的,使用了 辣鷄 DevC++ ,因此你可以猜到這個東西跑起來是什麽樣子。
代碼比較凌亂。而且因爲我是趕出來的,直接一個文件解決問題,相信我你絕對不會想要去讀這個代碼的 🙂

Debug 模式下可以看到 Daemon

一些值得 Mark的 小技巧有:

  • 適當使用了 Windows Api, 實現了自由控制 文字顔色 和窗口 全屏
  • 用命令控制開啓和 一次性 關閉所有進程
  • 不同的 啓動參數 可以改變程序的行爲
  • 可以通過改變 預編譯 命令 來決定程序的惡心程度
  • 簡單的多綫程實現
  • 開啓了一個 守護進程 來檢查並阻止關閉程序(守護進程也無法關閉)
  • 檢測到嘗試關閉程序會有 懲罰
  • 長時間沒有輸入也有懲罰(多綫程實現)
  • 隱藏了一個 “上帝指令” 允許優雅的關閉程序

下載程序

在下載前,你需要知道:

  • 壓縮包裏只有 文本文件和圖片資源,你需要自己編譯 main.cpp
  • Goose程序需要分別下載,下載完之後放到程序的同級目錄

Goose: 跳轉至官網
程序源碼: 點擊下載

程序沒有任何的傳播性(不會自我複製),隱藏性(不會僞裝),破壞性(不會讀寫其他文件),且不會自動運行(若想達到這一點你可以讓程序自己複製到啓動),因此嚴格來説這個東西并不能被稱作病毒。請在編譯前仔細閲讀代碼,確定沒有問題后運行,一切後果 雨我無瓜.

病毒一般具有 传播性、隐蔽性、感染性、潜伏性、可激发性、表现性或破坏性, 一般具有兩種即以上就可以被稱作電腦病毒。具體請參考 維基百科 – 計算機病毒的特徵