Skip to main content

7 posts tagged with "javascript"

View All Tags

· 13 min read
Wei Ji
info

範式沒有不見,範式只是變成其他形狀了。

緣由​

最近因為工作的關係需要學習使用 Python,不過作為一個有三年前端開發經驗的工程師,加上沒吃過 Python 也看過 Python 走路(?),因此學習的角度難免脫離一般沒有背景的人入門 Python 的方式,與其說是從頭學習一門程式語言,不如說是遷移我在 Javascript 上累積的經驗。

W3School 的教學安排我覺得就已經很清楚了:

不過對於我這樣一個有開發經驗的人而言,語法反而不是最重要的。

執行環境​

info

Javascript::NVM → Python::pyenv

Javascript::NPM → Python::pip

Javascript::Yarn → Python::Poetry

Javascript​

相較於其他程式語言,Javascript 的生態系除了它那容易令人誤解的名稱之外,還有規範 (Specification) 與實做 (Implementation) 分離的奇怪現象。

Javascript 本身是一團抽象規範,真正能夠執行的是實做,而開發者主要需要區分的實做分別是網頁瀏覽器 (Web Browser) 與 Node.js。

其中 Node.js 有著非常多的版本1,而這個版本也是開發者需要留意的對象,當程式碼與直譯器版本不批配時就會出現問題。

因此 Javascript 開發者通常不會直接安裝 Node.js,而是透過 NVM (Node Version Manager) 去決定與切換當前的 Node.js 版本。

接著是 Javascript 作為現代語言如何處理模組 (Module),每個專案資料夾都有各自儲存模組的路徑,並且為了減少複數個專案之間下載相同模組的時間,會在使用者的 home 留下快取。

主流的工具有 npm、 Yarn、 pnpm...,技術層面各有差異,不過在 Javascript 專案中擔任的角色是相同的。

值得一提的是,在 Javascript 的生態系中,「套件管理器」就不只一種,因此 Node.js 考慮引入 「套件管理器管理器」:Corepack。

Python​

如同 Node.js 使用 NVM 解決 Node.js 版本眾多的問題,Python 使用 pyenv 解決:

# 安裝 pyenv
curl https://pyenv.run | bash

# 把 PATH 加入 `~/.bashrc`
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc

除此之外 Python 有其他問題需要煩惱:

除了 Python 直譯器的安裝路徑外,套件的管理是另外一個問題:

因為專案之間直接使用相同的套件來源,當仰賴的套件版本衝突時就會發生相依性地獄,於是有一系列的工具為了解決這個問題而生:

目前 Poetry 看起來是最好方案,安裝 Poetry:

curl -sSL https://install.python-poetry.org | python3 -

於是建立一個 Python 專案就變成像這樣了:

poetry new poetry-demo  # 新增專案

cd peotry-demo

poetry env use python3 # 新增虛擬環境

poetry shell # 進入虛擬環境

git init

curl https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore > .gitignore

關於 Poetry 的介紹與使用這裡有一篇文章講的蠻完整的:

Python 套件管理器——Poetry 完全入門指南

型別檢查​

info

Javascript::Typescript → Python::Mypy

Javascript::tsconfig.json → Python::mypy.ini

Javascript​

JavaScript 本身是一種動態型別/弱型別語言,這種特性給予了開發者極高的彈性,但容易造成很多基本的錯誤需要在執行期間 (runtime) 才能被發現。

Typescript 就是為了解決這個問題而誕生的。Typescript 的另外一個特性是「Javascript 的超集 (superset)」,不過在 ES6 已經普及的今天 (2024 年),這個特性已經不是最有價值的部份了。

透過 Typescript 定義的型別語法,加上 Linter (語法檢查器)與 IDE Hint (編輯器提示)等機制,可以在開發階段就避免或挑出低級錯誤而減少在執行期間除錯 (Debug) 的成本。

型別檢查的規則可以透過 tsconfig.json 來調整。

Python​

Python 的動態型別特性是筆者一直以來對 Python 保有排斥心裡的原因之一,因此解決型別問題是筆者重返 Python 最高優先解決的問題。

值得慶幸的是 Python 確實陸續將型別標記之類的功能引入自己的標準之中,諸如:PEP3107、PEP484、PEP526、PEP561...。2

型別註釋能夠提供團隊成員對於變數型別的了解,不過仍不能真正解決型別誤用的情況,畢竟 Python 的型別註釋也只是註釋,並無法偵測誤用/錯用型別的情況。3:

雖然 Python 支援型別標記,卻仍然缺少型別檢查的實做,為了解決這個問題確實有不少型別檢查的工具:

Mypy,就決定是你了!

# Install type check tool
poetry add mypy

# Run type check
mypy .

如同 Typescript 的 tsconfig.json 一般,Mypy 可以透過 mypy.ini 來設定型別檢查的強度與規則。

更多關於型別的寫法見官方文件。

VSCode (Visual Studio Code)​

Javascript​

VSCode 因為自己本身就是 Typescript 寫的,所以對 Javascript/Typescript 可以說是原生支援,不需要額外掛擴充插件就能有不錯的開發體驗,不過可能需要調整一些設定之類的,例如:顯示參照。

Python​

基本語法​

基本語法照著 w3school 的清單一個一個看過去大概清楚 Python 的基本語法了,我只會挑一些語法或名稱差異比較大的出來。

模板文字​

info

Javascript

`My name is John, I am ${age}`

→ Python

f"My name is John, I am {age}"

在 Javascript 被稱作 Template literals 的東西,在 Python 則是透過 f-strings 達到類似的效果。

匿名函式​

info

Javascript::Arrow Function → Python::Lambda Function

解構賦值​

info

Javascript::Destructuring Assignment → Python:Unpack Javascript::Rest Parameters → Python::Arbitrary Arguments

W3 是把這個概念放在 Tuples 下講,但是我覺得既然是 List 和 Tuples 共用的特性應該分出來講,畢竟在 Javascript 這是一種叫做解構賦值 (Destructuring assignment) 的語法。

Javascript​

let a, b;
[a, b] = [10, 20];

let t = [0, 1, 2, 3, 4];
[a, b, ...c] = t;

const func = (...args) =>
console.log(args)

Python​

a, b = 10, 20

t = (0, 1, 2, 3, 4)
a, b, *c = t

def my_function(*args):
print(args)

這裡有一篇文章舉了更多 unpack 的例子4。

字典​

info

Javascript::Map → Python::Dictionary

陣列​

info

Javascript::Array → Python::List Javascript::TypedArray → Python::Array (NumPy)

和 Javascript 一樣,Python 的陣列不是「真的陣列」,而是一個可以變大變小的資料容器,並且這樣的容器在 Python 中被稱作 List。

在 Javascript 中可以透過 TypedArray 來創造真正意義上資料密集的陣列,但是在 Python 中這件事並不是內建在語言中的:

Python does not have built-in support for Arrays, but Python Lists can be used instead. To work with arrays in Python you will have to import a library, like the NumPy library.5

而是需要透過諸如 NumPy 之類的函式庫來達成。

進階語法​

模組 (Modules)​

從模組引入特定的函式或物件:

info

Javascript

import { name1, name2 } from "module"

→ Python

from module import name1, name2

引入整個模組:

info

Javascript

import * as new_name from "module"

→ Python

import module as new_name
import module

在 Node.js 中可以在資料夾加入進入點來表達「這個資料夾是模組」,Python 也有類似的機制:

info

Node.js::index.js → Python::__init__.py

Node.js 之父後悔 index.js 這個設計又是另外一個故事了6。

全域匯入​

info

Javascript

import "module"

→ Python

from module import *

Javascript 在模組機制還不發達的時期,引入函式庫的方式往往就是一團丟到全域變數去的東西,而這種方式很容易造成函式庫之間的命名衝突,對於這段歷史有興趣的同學可以了解一下一個名為 UMD (Universal Module Definition) 的工具,它身上有著 Javascript 模組大亂鬥的歷史。

在 Python 中有著類似匯入方式:

from random import *

print(randint(0, 5)) # It's random.randint

不過正如早期的 Javacsript 全域模組的問題一樣,這種語法在有成熟模組匯入機制的現在應該避免使用。

這裡有一篇講解更多關於 Python 匯入模組的文章:

Python 的 Import 陷阱

內建模組​

在 Node.js 中,「內建模組」的概念並不陌生,fs, path, os 都是 Node.js 的內建模組,這樣的設計可以避免過度膨脹原生的語法,而讓一些實做以模組的形式存在。

Footnotes​

  1. Node.js — Node.js Releases. Retrieved 2024-06-09, from https://nodejs.org/en/about/previous-releases ↩

  2. 新的型態提示PEP. (林信良). Retrieved 2024-06-09, from https://www.ithome.com.tw/voice/140338 ↩

  3. 使用 Python typing 模組對你的同事好一點. (Amo Chen). Retrieved 2024-06-09, from https://myapollo.com.tw/blog/python-typing-module/ ↩

  4. Unpack a tuple and list in Python. (nkmk). Retrieved 2024-06-09, from https://note.nkmk.me/en/python-tuple-list-unpack/ ↩

  5. Python Arrays. (w3school). Retrieved 2024-06-09, from https://www.w3schools.com/python/python_arrays.asp ↩

  6. Node.js 開發之父:「十個Node.js 的設計錯誤」- 以及其終極解決辦法. (David Ng). Retrieved 2024-06-09, from https://m.oursky.com/f0db0afb496e ↩

· 2 min read
Wei Ji

行程間通訊 (IPC, Inter Process Communication) 是兩個行程(或以上)或執行緒間傳送資料或訊號的一些技術或方法。 如果有在 Node.js 玩過 child_process 模組的 spawn 或是使用過 Web API 的 Worker 的話,應該對如何在 Javascript 中使用複數行程 (process) 並不陌生。

區塊之間的通訊方式一覽​

在前一篇文章提到,在 Electron 中 Render 和 Main 彼此之間是獨立的,必須透過 Preload 和 API 來實現兩者之間的溝通。參考了官方的範例程式碼以及一篇文章1之後,整理成一個圖表:

後記​

這篇文章原本在 2023-04-05 就已經寫好草稿了,沒想到完全忘記這篇還沒歸檔,然後圖表的 SVG 也已經刪掉了。


創用 CC 授權條款
Wei Ji 以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

Footnotes​

  1. [ Day 9 ] - 動物聊天室(二) - IPC 與訊息交換 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天. Retrieved 2023-08-08, from https://ithelp.ithome.com.tw/articles/10235110 ↩

· 3 min read
Wei Ji
note

本篇學習筆記以 Javascript 在應用上的實作為切入點,關於 WebRTC 背後的技術並不是本文的重點。

看了幾篇教學仍然對 WebRTC Web API 的使用不是很理解,因為這些文章不是都著墨在 WebRTC 協定本身然後程式碼的實作就簡略帶過;就是跟使用 WebSocket 處理 SDP (Session Description Protocol) 的部份一起講,造成程式碼充滿了各種事件 callback,讓程式碼變得比較不好理解。

後來看到了一個範例程式,單純針對 RTCPeerConnection 如何建立連線,從而省略實務上會透過其他機制完成 SDP,讓程式碼變得稍微好理解一點。

經過重構、Reivew 之後終於看懂使用 Javascript 建立 WebRTC 連線的流程,我把它整理成一張圖:

大致有三個重點:

  1. 兩邊的 Connection 都分別要從自己跟對方獲得 SessionDescription。
  2. 成對的 ICE (Interactive Connectivity Establishment) 處理程式 (handler)。
  3. addTrack() 與 .on("track") 用來發送/接收串流。

另外我練習使用 WebRTC 的程式試著用不同的界面分離職責:

  • ITransmittable: 發送端應用領域對傳輸領域的界面,專住於「對外串流」的職責。
  • IReceivable: 接收端應用領域對傳輸領域的界面,,專注於「接收串流」的職責。
  • IRTCOfferable: 發送端對連線建立領域的界面,專注於「發送方的 WebRTC 構成要件」的職責上。
  • IRTCAnswerable: 接收端對連線建立領域的界面,專注於「接收方的 WebRTC 構成要件」的職責上。

創用 CC 授權條款
Wei Ji 以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

· 3 min read
Wei Ji

Electron in the frontend roadmap

Electron 可以說是前端軟體工程師必學的經典工具之一 (2023 年),也是我個人有興趣學習的主題之一,只是一直缺乏明確的動機或目的去使用它,直到那天 (2023-03-30) 協助友人改善他 Side Project UI,同一天另外一個朋友又剛好問我 Electron 的問題,看來這是要我學習 Electron 的天意啊(?)

稍微試了幾個專案樣板,然後讀了一點官方文件,我稍微整理出它的架構:

這裡有幾個知識點最好在學習 Electron 之前先知道:

  • Javascript 是一種程式語言,但是它的直譯器不只一種
  • 瀏覽器上的 Javascript 執行環境和 Node.js 的執行環境不同
  • 在瀏覽器上開發者可以用 Javascript 使用 Web API (window.*, CSS, DOM...等)和 ES Module (符合 ES6 規範的 Javascript 模組)
  • 在 Node.js 上開發者可以使用 Node.js 的內建模組 (fs, os, path...等)和 NPM 上提供的 CommonJS Module
  • 在瀏覽器上 Javascript 沒有權限進行一些涉及作業系統的操作,如:對檔案系統進行讀寫

然後我整理了幾個關於 Electron 的知識點整理:

  • Electron 應用程式分成兩個區塊
    • 瀏覽器(基於 Chromium)
    • Node.js
  • 兩個區塊彼此獨立
    • Main Process 不能操作 DOM
    • Render Process 不能使用 Node.js 模組讀寫檔案系統或是 spawn 子行程
  • 一個應用程式可能有複數個視窗
  • Preload Prcess 能夠同時訪問瀏覽器區塊和 Node.js 區塊
    • 我們可以把原本被隔離的兩個區塊曝露部份操作給彼此
    • 透過 Electron 提供的 IPC API

創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

· 2 min read
Wei Ji

從影片逐幀取出畫面​

這個步驟不難,使用 FFmpeg 只要一行指令就能完成:

mkdir output
ffmpeg -i bad_apple.mp4 -vf fps=5 output/%04d.png

將畫面轉換成 ASCII 純文字檔​

這個步驟也不難,把剛剛生成的畫面載入、轉換再吐出去,搞定。

import fs from 'fs';
import asciify from 'asciify-image';

fs.mkdirSync('data', { recursive: true });

var options = {
fit: 'none',
width: 50,
height: 30,
c_ratio: 1,
color: false,
}

/**
* @param {string} path
* @returns {Promise<string>}
*/
const imageToAsciiPromise = (path) => new Promise((resolve, reject) => {
asciify(path, options, (err, asciified) => {
if (err) {
reject(err)
}
resolve(asciified.replaceAll(';', ' '));
});
});

const _files = fs.readdirSync('img');

const files = _files.map((file, index) => {
const sourcePath = (`img/${file}`);
const targetPath = `data/${(index + 1).toString().padStart(4, '0')}.txt`;
return {
sourcePath, targetPath
};
});

for (const { sourcePath, targetPath } of files) {
const acsii = await imageToAsciiPromise(sourcePath);
fs.writeFileSync(targetPath, acsii);
console.log(sourcePath)
}

播放​

import * as os from "os";
import * as std from "std";

let count = 1;

const drawFrame = () => {
const timer = os.setTimeout(drawFrame, 200);
count++;

const f = std.loadFile(`data/${count.toString().padStart(4, '0')}.txt`);
if (f === null) {
os.clearTimeout(timer);
return;
}

std.out.puts('\x1B[2J');
std.out.puts(f + '\n');
}

drawFrame();

需要特別注意的是,這個執行環境是 QuickJs,os 和 std 都是該直譯器提供的模組。

· 12 min read
Wei Ji

本文以敘述的方式紀錄我在這個專案上的過程,並省略技術細節。若對實作細節有興趣,以下是以技術筆記的形式紀錄的文章:

前情提要​

最近 (2022-11-06) 從天上掉下來的一台 3DS (?)

老實說對於一般過時 N 年的消費性電子產品,我是完全沒興趣多花時間去玩它,根據以往的經驗,hack 這種東西多半吃力不討好;花費大量的時間也只能獲得相對於現代設備少得可憐的算力。 不過個人對於這種「有機械輸入的橫式手持行動裝置」有著莫名憧憬,隨手搜尋看看刷機的資料,發現資源蠻多的,教學也很詳細,就想著來刷刷看。

閃亮亮症候群再次發作​

跟朋友表示想在 3DS 上刷 Linux 之後,「工程師的成就清單」的話題就這樣被打開了,對話中除了提 「在非常見或非通用功能電腦上安裝 Linux」還提到了「在非常見的顯示器上播放 Bad Apple」。

過幾天 (2022-11-09),我便利用下班之餘試著在 3DS 上刷 Linux。刷機的第一部就是透過系統漏洞刷入 CFW (Custom firmware),讓 3DS 能夠從 SD Card 讀取 bootloader 開機,因為教學很詳細,照著步驟做沒有花太多時間就成功刷機了,當中有些教學性質的步驟也讓我看了 3DS Linux 的頁面之後就知道大概要做什麼,也成功的在 3DS 上進入 Linux 了。

到目前為止都出乎我意料之外的順利,於是我想:

我都有一台跑著 Linux 的 3DS 了,寫個程式讓它跑 Bad Apple 應該不難吧?

接著我又想:

如果能用我熟悉的 Javascript 寫程式就好了

於是「在 3DS 上跑 Linux 跑 Javascript 顯示 Bad Apple」就變成一個 side project 的目標了,接著這個 side project 就一發不可收拾了。

這個 Linux 是金魚腦​

試著新增檔案並寫入一些資料,重開機之後卻發現檔案消失了,後來才了解到整個檔案樹是放在啟動 Linux 用的映像檔裡面,那些資料夾在開機之後是掛在 Ram Disk 上 1,自然沒有儲存的功能。

解決的辦法就是把資料存在 SD 卡上,但是這個 Linux 預設並不會掛載 SD 卡,研究了一會兒才知道它在 /dev/vda1,但是每次開機都要打一次指令顯得有點麻煩(提醒,3DS 沒辦法外接鍵盤,我只能用觸控筆敲指令)。

寫個腳本讓它在開機的時候執行便理所當然的變成必須完成的任務,然而它並沒有 systemd, rc.local 這類高級的東西。/etc/init.d/rcS 承擔 start 的功能 /etc/init.d/rcK 承擔 kill(stop) 的功能,會依序執行 /etc/init.d/rcS/S(\d\d.*)。

腳本倒是容易搞定,但是我要怎麼把它包進去開機用的 img 裡面?建置 3DS Linux 的過程涉及一個 rootfs.cpio.gz 檔案,我必須把它解包之後加入我要的修改再包回去。

花了一點時間搞懂上述這些東西,才終於讓我的 Linux 開機會自動掛載 SD 卡 (2022-11-12)。

第一個挫折​

依照自己的以往經驗:「要在 Host 上跑 Javascript 就先裝 Node.js」,然而最新版的 Node.js 並沒有 Armv6 的 prebuild release,不過一直到 11 版,還是有官方版的 armv6 prebuild 可以下載。下載之後在 3DS 上執行會拋出 -sh: ./node: not found 的錯誤,原因是官方的 prebuild 是仰賴 glibc,但是 3DS Linux 使用的是 musl libc。好吧,這個問題我也不是太陌生,之前在包 Docker 接觸過 Alpine Linux,所以對這類問題有個概念。

好唄,自己 build 就是了

雖然網路上找了一些 build 參數,但是怎麼試怎麼有問題,畢竟我平常又不寫 C,對專案的編譯配置不熟悉。後來找到一個叫做 Dockcross 的工具專門用來處理交叉編譯 (Cross Compiling)。但是相對舊版的 Node.js 仰賴 Python 2.X,Dockcross 的 Image 只有 3.X,這時我才知道 Node.js 的編譯過程居然仰賴 Python,我內心 OS:「SHAME, 程式語言之恥,Node.js 你作為一個直譯語言的直譯器的編譯過程居然仰賴另外一種直譯語言」。

好加在 Dockcross 允許擴充 Docker image 以符合專案需要,然而事情並沒有這麼簡單。Node.js 的編譯過程仰賴了編譯的產出物,也就是當我指定了編譯目標為 linux-armv6-musl ,會生成一些 tool,接著在編譯出 node 以前,它會在 host 呼叫這些 tool,但是我的 host 不是 linux-armv6-musl;於是拋出錯誤,編譯中止。(#╯O皿O)╯┻━┻

接著我發現在建置 3DS Linux 過程中使用的工具:buildroot 其實提供了一個界面可以勾選 node,而它的實作方式就是先編譯一份 x86 的版本,再跑 arm 的編譯,而編譯過程中用到的 tool 就回去 call x86 的版本。經過一波三折我終於弄出了「musl-armv6 版本的 Node.js v14」,然而問題並沒有因此解決,不然段落標題就不會是「第一個挫折」了。

$ node --version
v14.18.3

$ node -h
...
Process node (pid: 153, stack limit = 0xc6135dc6)
...
[<c0111610>] (v6_coherent_kern_range) from [<c01094c0>] (arm_syscall+0x15c/0x26c)
[<c01094c0>] (arm_syscall) from [<c0100060>] (res_fast_syscall+0x0/0x58)
Exception stack (0xc15elfa8 to 0xc15elff0)

總之 Node.js 對於那個年代的 3DS 來說 too powerful 了。(2022-11-13)

作為 ECMAScript 的信徒,這個挫折讓我很不甘心。

如果能用 node.js 跑的話感覺真的滿酷的,用 C 或 shell 就感覺很一般

並且正如我朋友說的,雖然我也不是不會寫 Shell 或是 C,但是感覺這樣就太無聊了(?)既然都難得要解 Bad Apple 的成就了,當然是加點料比較有趣,而且作為網頁前端工程師,窩真的不是很想用 Shell 或是 C 寫啊 _(:3」∠)_。誰知道 Node.js 毫無反應,就只是一團 stack 直接死在 Armv6 上。

碰巧那天 (2022-11-13) 跟朋友聊天的時候有提到在 Javascript 的圈子有個叫做「包」的東西,後來趁著放假整理瀏覽器書籤的時候發現 Bun 相關的東西,稍微查一下資料才想起來除了 Node.js 之外還有 Deno 跟 Bun 兩個實作 Javascript 的專案。

Bun 因為是後起之秀,Deno 又是用 Rust 寫的,具我所知 Rust 也能拿來寫嵌入式韌體,移植到 3DS Linux 的機率比較高一點,於是我便朝著這個方向去研究。

雖然 Deno 本身有一些很酷的 feature,包含交叉編譯的功能,可以把 Javascript 編譯成目標平台的執行檔。但是它本身卻沒有 arm linux 的 prebuild。就算自己跑編譯也會在下載仰賴套件 rusty_v8 的時候失敗,加上官方關於 32 bit ARM 支援的 issue 仍然開著。這些都還不包括 musl libc 可能會遇到的問題。看來此路仍然不通。我甚至一度產生了「乾脆用 Rust 寫吧,順便學一下這個我有興趣但是保持觀望一段時間的語言」的念頭。(2022-11-18)

等等,既然 Javascript 都有諸如 Node.js, Bun, Deno 這些不同的實作,甚至使用不同的 runtime 引擎(V8, JavaScriptCore),ECMAScript 單純作為標準,它的實作應該不只這樣吧?我也應該不是第一個想在嵌入式系統跑 Javascript 的人吧?

「Any application that can be written in JavaScript, will eventually be written in JavaScript.」​

事實證明我不孤單:

一開始我嘗試了 tiny-js,因為在問答串的分數也相對高,它看起來足夠精簡,背後又是 Google。然而很快又遇到了 Musl libc 的問題,透過 musl-cross-make 這個工具沒有花多久就排除問題了,但是執行後直接跳 Segmentation fault,畢竟是個年久失修的專案,會這樣似乎也不怎麼意外。

後來我在這裡發現 quickjs 不只有中文資源,README 也有提到可以直接執行而不是只有 lib,相較於 jerryscript 的 README 沒有明講是 lib 還是可以直接跑,又有仰賴 Python 編譯,我就先選擇了 quickjs 嘗試。(對,我就歧視 Python,OHO)

它跟 Node.js 一樣,編譯過程仰賴自己編譯的 tool (qjsc),一樣要 build 兩次來解決,因為有前面的經驗,沒有花太久就把問題排除並成功在 3DS Linux 上跑起來了。 (2022-11-20)

Voila!​

Javascript 能夠跑起來,實作的部份就簡單了(?)

  1. 用 FFmpeg 對影片以 5 FPS 取樣轉成圖片(原本試過 10 FPS,無奈設備太老舊跟不上)
  2. 把圖片轉成 ASCII 文字檔
  3. 用 Javascript 讀擋然後 print 出來

因為我現在手上同時有 arm 跟 x86 版本的 qjs 直譯器,改 Javascript 程式執行 debug 的速度就很快了,不用重新編譯,只要在 dev 環境測試到沒問題直接複製到 3DS 去再做第二次測試。

(縮圖有影片連結)


創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

Footnotes​

  1. https://github.com/linux-3ds/linux/wiki ↩

· 4 min read
Wei Ji

前情提要​

我已經成功的完成了 3DS 刷機並且在它上面跑起了 Linux,作為一個網頁前端工程師,我一點也不想為了在上面跑點東西跑去寫 C,而是想把我的舒適圈帶進去,也就是我要弄一個 Javascript runtime 進去 3DS Linux。

然而 3DS Linux 算是一個相對罕見的環境:Armv6 的硬體以及使用 musl libc,主流的 Javascript runtime 並沒有提供官方版本的 pre-build release,而是需要自己想辦法編譯。

musl libc​

C 語言程式的執行其實仰賴 libc (C Run-Time Library),並且 libc 的實作不只一種,musl libc 便是其中的一種,而且被不少 Linux 發行版作為 C 標準函式庫。

交叉編譯 (Cross Compilation)​

https://preshing.com/images/gcc-cross-compiler.png

交叉編譯就是先在 host 編譯出交叉編譯器 (cross compiler),再用這個交叉編譯器去編譯你的程式,最終目的是你的程式能夠跑在 target 上。

另外一個方法就是直接在 target 上跑編譯,但是 target 有可能是性能很低的晶片,這種方法效率非常差甚至無法完成(沒有足夠的記憶體完成編譯之類的),交叉編譯就是為了解決這種問題而存在的。

QuickJs​

眾所皆知 ECMAScript 是一個語言的規範,而不是實作,因此遵守該規範的直譯器 (runtime) 其實有百百種。除了 Node.js 之外,Javascript runtime 還有後起之秀 Deno 和 Bun,而引擎 (engnine) 自然也不少,被各種瀏覽器、 Node.js 以及 Deno 使用的 V8 或是被 Bun 使用的 JavaScriptCore,除了這些之外還有諸多的實作。

QuickJS 是一個嬌小且可嵌入式的 Javascript 引擎。它支援 ES2020 規範,包含模組、非同步 Generator、Proxy、BigInt。它是以 C 語言撰寫並且沒有仰賴其他函式酷。

編譯 Cross Compiler​

已經有人準備了方便的工具 musl-cross-make 讓我們能簡單的製作交叉編譯器。

首先下載這個 repo:

git clone git@github.com:richfelker/musl-cross-make.git
cd musl-cross-make

新增 config.mak 檔案:

touch config.mak

加入以下內容:

TARGET = arm-linux-musleabihf
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os" LDFLAGS="-s"
GCC_CONFIG += --with-arch=armv6 --with-mode=arm --with-fpu=vfp

執行編譯:

make -j ${nproc}

安裝(輸出執行檔),預設會輸出到專案目錄下的 output/ 資料夾:

make install

複製到 home

touch ~/.musl-cross-make
cp output/* ~/.musl-cross-make/.

把環境變數加到 .bashrc

export MUSL_CROSS_INSTALL="$HOME/.musl-cross-make" 
export PATH="$MUSL_CROSS_INSTALL/bin:$PATH"

編譯 QuickJs​

下載 quickjs

git clone https://github.com/bellard/quickjs
cd quickjs

需要更動 Makefile 的一些內容:

新增 CROSS_PREFIX=arm-linux-musleabihf-

  CROSS_PREFIX=arm-linux-musleabihf-
HOST_CC=gcc
CC=$(CROSS_PREFIX)gcc
LIBS+=-ldl -lpthread          #找到這行
LIBS+=-ldl -lpthread -latomic #增加參數

接著跑編譯:

make -j ${nproc}

檢查編譯結果是不是符合我們的目標:

$ file qjs
qjs: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-armhf.so.1, with debug_info, not stripped

創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。