用 QuickJS 直譯 JavaScript 程式

創造出 FFmpeg、Tiny C Compiler、QEMU 的大神 Fabrice Bellard,在 2019 年又創造另一個神器了:QuickJS!一款嵌入式 JavaScript 引擎,而且符合 ECMAScript 2020 標準。

它小巧(檔案小)、輕快(佔用系統資源低),執行效率卻高得驚人,跑分數一數二!

做為可嵌入的 JavaScript 引擎,QuickJS 東西不多,很容易就能完全掌握它的所有程式功能,不失為 JavaScript 程式設計的入行工具1

但也因為這樣的關係,QuickJS 無論現在或未來,都不會有豐富的 API 可用。如果你想開發視窗應用程式、網路應用程式、電腦遊戲,那你不用期待 QuickJS 會內建相關 API、也不用找有沒有第三方模組可用,你該搜尋的資料是,怎樣將 QuickJS 嵌入到某某 GUI、WEB、GAME 框架裡面~


準備


下載 QuickJS

請至 http://bellard.org/quickjs/binary_releases/ 下載最新版!

以 Windows 10 64 位元為例,我下載的是 quickjs-win-x86_64-2021-03-27.zip。


Hello, world!

用記事本寫入如下程式,並儲存檔案為 main.js:

notepad('main.js','print(\'Hello, world!\');')

進入命令提示字元,然後用 QuickJS 資料夾裡面的 qjs.exe 執行 main.js,沒問題的話會顯示 Hello, world!:

cmd('C:\\Users\\User\\Desktop>C:\\quickjs-win-i686-2021-03-27\\qjs main.js','Hello, world!','','C:\\Users\\User\\Desktop>_')


官方說明書

請連至 http://bellard.org/quickjs/quickjs.html,裡面說明了 QuickJS 有哪些功能、語法、程式庫。由於所有資料都在一份 HTML 文件,直接另存新檔就能保存在自己電腦隨時查閱。

平板電腦或智慧手機想保存一份隨時查閱的話,建議改看 DPF 檔,因為網頁瀏覽器通常不允許開啟本機端的 HTML 檔案:http://bellard.org/quickjs/quickjs.pdf


目錄

主控台輸出入
命令列參數
讀寫檔案、呼叫外部程式
逐行讀取檔案內容
插圖 更多 std.open() 所傳回 FILE 物件的操作方法
os 模組的檔案讀寫
取得檔案訊息
載入其他 *.js 程式碼檔案
CGI
其它好用的模組功能
附錄


範例

QuickJS 符合 ECMAScript 標準,因此支援 ECMAScript 規範的物件,像是 Object、Array、String…之類,請盡量操作這些物件的方法來寫程式。

這些資料很豐富,尤其是 Mozilla 寫的聖典《JavaScript Reference》,只要看這份文件,就能使用所有物件!

本文接下來只示範不屬於 ECMAScript 標準的功能…為什麼會需要另外增加標準外的功能?因為 ECMAScript 的 API,完全是以網頁瀏覽器的使用去制訂的,所以沒有主控台輸出入、檔案讀寫、檔案與資料夾增刪…等等與作業系統相關的功能。因此,做為直譯器,QuickJS 就必須彌補 ECMAScript 的不足,自行追加能讓程式設計師與作業系統互動的功能囉~

QuickJS 的官方文件只適合資深程式設計師閱讀,新手的話,網路資料不多且雜亂無章,所以本文彙整一些範例,一看就知道該怎麼做,希望能幫助到剛接觸 QuickJS 的人。


主控台輸出入


Input: abc123
abc123

std.puts() 相當於 std.out.puts(),與 print() 不同之處在於結尾不會自動追加 '\n' 換行字元。


命令列參數


qjs main.js ABC 123
ABC123

scriptArgs[0] 是 main.js。

scriptArgs 是 QuickJS 唯一一個全域變數。2


讀寫檔案、呼叫外部程式


ABC123
ABC123


逐行讀取檔案內容

text.txt

main.js


AAA
BBB
CCC


更多 std.open() 所傳回 FILE 物件的操作方法

puts(str)
printf(fmt, ...args)
flush()
seek(offset, whence)
tell()
fileno()
read(buffer, position, length)
write(buffer, position, length)
getByte()
putByte(c)


os 模組的檔案讀寫

std 模組提供物件導向式的檔案讀寫功能,而 os 模組則提供程序導向式的檔案讀寫:

os.open(filename, flags, mode=0o666)
os.seek(fd, offset, whence)
os.write(fd, buffer, offset, length)
os.read(fd, buffer, offset, length)
os.close(fd)

不過,os.seek() 的位移量,依然是用 std 模組來存取:

std.SEEK_SET
std.SEEK_CUR
std.SEEK_END


取得檔案訊息

os.stat(path) 可以取得資料夾或檔案的訊息。

然而,這函式傳回的是 [obj, err] 陣列,所以必須先用索引值 0 取得表示訊息的物件,就能透過物件的 dev、ino、mode、nlink、uid、gid、rdev、blocks、atime、mtime、ctime 等屬性取得訊息:


載入其他 *.js 程式碼檔案

extra.js

main.js


579


CGI

自從 ECMAScript 6 引進能多行又能插值的 Backslash 字串,這語言變得很適合寫 CGI!

然而,別說 2020 年代了,不可能有人想寫 CGI,即使願意幹這種落伍的事,ECMAScript 一向都是實作在網頁瀏覽器裡,很少有好用的直譯器可以用在 CGI 上。

以嵌入到各裝置、各開發環境為訴求的 QuickJS,正好一拍即合!

當你臨時需要一個網頁伺服器,而且前端後端都想用 JavaScript 語言時,不妨拿支援 CGI 的 HTTP Server + QuickJS 來應急吧!雖然開發效率不高,難以滿足大型商務網站的需求,但寫寫動態網頁的話,會是令人愉快的組合。3

index.html

test.cgi

Web browser

msedge('http://127.0.0.1','../../images/overcast/20190711A_02.png')

msedge('http://127.0.0.1/test.cgi?name=Twideem&number=58','../../images/overcast/20190711A_03.png')

POST 的話,使用 std.in.getline() 取得資料。

想把 QuickJS 的資料帶進 HTML 給 JavaScript 使用,只要變通一下,在 HTML 用 script 標籤宣告個變數,賦值為 QuickJS 的資料即可:

print(`<script>let save = '${std.loadFile('save.txt')}'</script>`);

這樣網頁瀏覽器的 JavaScript 就能透過 save 變數使用 QuickJS 的資料:

print(`<script>
 if(save == 'Hello, world!'){
  document.write('是在哈囉');
 }
</script>`);

反過來要把 JavaScript 的資料傳給 QuickJS,也是變通一下,用 location.href 把 JavaScript 的資料放在 URL 的參數,傳給 CGI 的 QuickJS 使用:

let data = 'Hello, world!';
location.href = `save.cgi?data=${data}`;

要查看有哪些環境變數可用的話:

for(let n in std.getenviron()) print(n);

要列出所有環境變數內容的話:

for(let n in std.getenviron()) print(`${n} = ${std.getenv(n)}`);


其它好用的模組功能

std.printf(fmt, ...args)格式化輸出。
std.sprintf(fmt, ...args)格式化字串。
std.exit()結束程式。
std.gc()記憶體回收。
std.in對應 C 語言的 stdin 程式庫。
std.out對應 C 語言的 stdout 程式庫。
std.err對應 C 語言的 stderr 程式庫。
os.sleep(毫秒)暫停一段時間。
os.remove(filename)移除檔案。
os.rename(oldname, newname)修改檔案名稱。
os.mkdir(path, mode=0o777)建立資料夾
os.chdir(path)切換資料夾。
os.readdir(path)取得資料夾裡面的檔案清單,傳回 [str, err] 陣列。
os.utimes(path, atime, mtime)修改檔案時間。
os.kill(pid, sig)刪除行程。

附錄


qjs 直譯器

萬用字元

qjs 不接受萬用字元,所以無法用 *.js 一次執行所有程式檔。也無法下多個檔案給 qjs 批次執行,qjs 一次只執行一個程式檔。

這並非設計上的短缺,而是刻意這樣設計的!QuickJS 設計的理念是,每發出一次 qjs 就代表一個 process,想執行多個程式檔,就多下一次 qjs。

以 qjs 的啟動速度和執行效率,這不但沒有問題,這種做法反而表現出更優異的性能。

QuickJS 在 os 模組提供了 qjs 之間相互溝通的功能,請自行參閱官方文件。

e 參數的妙用

不用直譯 *.js 程式檔、也不用進入 interactive mode,qjs 就可以直接跑一段運算式:

qjs -e print(12+34-56*78/9) > output.txt
-439.3333333333333
像這樣搭配 DOS 模式的 > 功能,可以讓其它程式語言把 qjs.exe 當指令調用,計算結果透過純文字文件讀取進來。

不過只能跑簡單的運算式,並不能跑完整的 JavaScript 程式碼。例如要指派變數保存資料的話,不能用 let 或 var 宣告變數,而是直接寫個變數名稱,用全域物件的屬性來完成。

預設載入 std 和 os 模組

std 打包了 C 語言的 stdlib、stdin、stdout、stderr 基本功能,讓 JavaScript 能進行一些程式設計最主要的工作。

os 提供了作業系統相關的功能,像是檔案系統、執行緒和同步、計時器。

不想每次寫程式都 improt 這兩個模組的話,可以對 qjs 下 --std 參數:

qjs --std main.js
注意!參數必須下在程式碼檔名前面。


JS Bignum Extensions

Fabrice Bellard 藉由 QuickJS 為 JavaScript 導入他先前發明的 Bignum 數值功能,可以解決 ECMAScript 數值超過 999,999,999,999,999 後會自動四捨五入的問題,但依然符合 IEEE 754 標準。

官網還特地示範 QuickJS 搭配 Bignum 可以計算圓周率到 10 億位數的能力。

因此這功能千萬不要錯過!它能讓 JavaScript 在大數據與科學計算上跟上其他動態語言的腳步:http://bellard.org/quickjs/jsbignum.html