一時興起研究一下 node.js 呼叫 python code 的方式,當然,都在 linux server 可以直接用 child_process 直接呼叫 python 去運行,例如 nodejs.org/api/child_process.html 的範例
```const { spawn } = require('node:child_process');const ls = spawn('ls', ['-lh', '/usr']);ls.stdout.on('data', (data) => {console.log(`stdout: ${data}`);});ls.stderr.on('data', (data) => {console.error(`stderr: ${data}`);});ls.on('close', (code) => {console.log(`child process exited with code ${code}`);});```
然而,有沒有可能在 node.js 內,直接做 python 直譯的過程等方式?當個樂子,找了一下,還真的有,這樣搞的優勢是降低環境部署的變因,當然,效率上不見得是好辦法,但可以讓不同語言的開發者進行融合(誤),目前看到兩種整合方式:
- Pyodide, pyodide.org
- Brython, brython.info
其中 Brython 屬於設計在 Web Browser 下運行(需要 DOM 資源),而 Pyodide 則不需要。分別筆記一下用法。
首先是要執行的 python code 內有 python 的 re 跟 json 模組的使用:
```% cat script.pyimport reimport jsondef runTest(inputData):output = {}flags = 0pattern = r'''(?x)(?:\.get\("n"\)\)&&\(b=|(?:b=String\.fromCharCode\(110\)|(?P<str_idx>[a-zA-Z0-9_$.]+)&&\(b="nn"\[\+(?P=str_idx)\]),c=a\.get\(b\)\)&&\(c=|\b(?P<var>[a-zA-Z0-9_$]+)=)(?P<nfunc>[a-zA-Z0-9_$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z]\)(?(var),[a-zA-Z0-9_$]+\.set\("n"\,(?P=var)\),(?P=nfunc)\.length)'''try:result = re.search(pattern, inputData, flags)if result:output["status"] = Trueoutput["data"] = result.groupdict()except Exception as e:output["error"] = str(e)return json.dumps(output, indent=4)runTest(data)```
Pyodide 用法:
```% nvm use v20Now using node v20.10.0 (npm v10.2.3)% npm install pyodide% cat package.json{"dependencies": {"pyodide": "^0.26.2"}}% cat run.jsconst fs = require('fs').promises;const { loadPyodide } = require("pyodide");async function main() {const fileContent = await fs.readFile('mydata.bin', 'utf8');let pyodide = await loadPyodide();pyodide.globals.set("data", pyodide.toPy(fileContent));const pythonCode = await fs.readFile('script.py', 'utf8');let result = pyodide.runPython(pythonCode);console.log(result);}main();% echo "Hello World" > mydata.bin% node run.js{}```
上述使用過程算直觀,但偷懶把要傳遞的資料設定在全域變數,在用 node.js 環境接住 python 運算的結果,看來這個效果是很 OK ,有正常運行得到期待的結果。
接著研究 Brython 用法,他設計上需要 Browser 環境做事:
% head -n 9 brython.js// brython.js brython.info// version [3, 11, 0, 'final', 0]// implementation [3, 11, 3, 'dev', 0]// version compiled from commented, indented source files at// github.com/brython-dev/brythonvar __BRYTHON__=__BRYTHON__ ||{}try{eval("async function* f(){}")}catch(err){console.warn("Your browser is not fully supported. If you are using "+"Microsoft Edge, please upgrade to the latest version")}
在 node.js 需要 jsdom 模擬一些環境,而下載 brython.js 和 brython_stdlib.js 則參考官網文件,透過 pip install brython 工具出來使用,所以這邊的流程會多了 python 工具的安裝,且現況用 python 3.12 會顯示有些問題,就先定在 3.11 版。此外 brython.js 運行環境,也是可以用最新版 node.js v22 ,但是會看到 [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. 訊息,所以先退到 node.js v20 避免額外的訊息
連續動作:
```% python3.11 -m venv venv% source venv/bin/activate(venv) % pip install brythonCollecting brythonUsing cached brython-3.11.3-py3-none-any.whl.metadata (1.0 kB)Using cached brython-3.11.3-py3-none-any.whl (1.6 MB)Installing collected packages: brythonSuccessfully installed brython(venv) % brython-cli installInstalling Brython 3.11.3done(venv) % lsREADME.txt brython_stdlib.js index.html venvbrython.js demo.html unicode.txt```
接著回到 node.js 主場:
```% nvm use v20Now using node v20.10.0 (npm v10.2.3)% cat package.json{"dependencies": {"jsdom": "^24.1.1"}}% cat run-via-dom.jsconst { JSDOM } = require('jsdom');const fs = require('fs');const path = require('path');const dom = new JSDOM(`<!DOCTYPE html><html><head></head><body><script></script></body></html>`, {runScripts: "dangerously",resources: "usable"});const brythonJsPath = path.join(__dirname, 'brython.js');const brythonStdlibJsPath = path.join(__dirname, 'brython_stdlib.js');const brythonJs = fs.readFileSync(brythonJsPath, 'utf8');const brythonStdlibJs = fs.readFileSync(brythonStdlibJsPath, 'utf8');try {dom.window.eval(brythonJs);dom.window.eval(brythonStdlibJs);} catch (error) {console.error('Error executing brython.js:', error);}const scriptPath = path.join(__dirname, 'script.py');const pythonScript = fs.readFileSync(scriptPath, 'utf8');const dataPath = path.join(__dirname, 'mydata.bin');const binaryData = fs.readFileSync(dataPath, 'utf8');const base64Data = Buffer.from(binaryData).toString('base64');const scriptElement = dom.window.document.createElement('script');scriptElement.type = 'text/python';scriptElement.textContent = `import base64data = base64.b64decode("""${base64Data}""")${pythonScript}from browser import documentdocument.output = runTest(data)`dom.window.document.body.appendChild(scriptElement);try {dom.window.brython({debug: 1, pythonpath: ['.']})console.log(dom.window.document.output);} catch (error) {console.error('Error executing dom.window.brython:', error);}console.log('Python script execution completed.');% echo "Hello World" > mydata.bin% node run-via-dom.js{"status": false, "data": {}, "error": "not the same type for string and pattern"}Python script execution completed.```
很可惜的,剛好要實驗複雜的 python regular expression,在 brython.js + node.js v20 + jsdom 環境上失敗了,甚至小改 index.html 搭配 python3 -m http.server 用 Chrome browser 執行(給予他完整的 Chrome 瀏覽器環境)還是有一樣的錯誤訊息,這邊就暫時推論失敗了,而上述的範例已經包括從 node.js 傳資料到 python code ,以及運行完如何把回傳資料傳到 node.js 使用,眼尖的人,應該會發現在 brython 用法內,使用了 `document.output = runTest(data)` ,其實是多呼叫了一次 runTest(data),因為原先 `${pythonScript}` 也有做,但沒在細追怎樣接運算結果,剛好不合預期就放棄研究。
最後,就是 WebAssembly 領域(一開始寫這篇筆記就是要研究 WebAssembly ,不小心走偏),把某一種 python code 轉成 wasm 格式,接著用 wasmer 運行,或是在其他語言(如 node.js)運行 wasm code。
先透過 MacPorts 安裝 wasmer:
% port search wasmerwasmer @4.3.5 (lang, devel)The leading WebAssembly Runtime supporting WASI and Emscripten% sudo port install wasmer
接著試著用 py2wasm 把 script-main.py 轉成 script-main.wasm,其中 py2wasm 官網有提到目前僅支援 python3.11:
% python3.11 -m venv venv% source venv/bin/activate(venv) % pip install py2wasm(venv) % py2wasm script-main.py -o script-main.wasm
程式碼:
```% cat script-main.pyimport reimport jsondef runTest(inputData):output = { "status": False, "data": {}, "error": None}flags = 0pattern = r'''(?x)(?:\.get\("n"\)\)&&\(b=|(?:b=String\.fromCharCode\(110\)|(?P<str_idx>[a-zA-Z0-9_$.]+)&&\(b="nn"\[\+(?P=str_idx)\]),c=a\.get\(b\)\)&&\(c=|\b(?P<var>[a-zA-Z0-9_$]+)=)(?P<nfunc>[a-zA-Z0-9_$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z]\)(?(var),[a-zA-Z0-9_$]+\.set\("n"\,(?P=var)\),(?P=nfunc)\.length)'''try:result = re.search(pattern, inputData, flags)if result:output["status"] = Trueoutput["data"] = result.groupdict()except Exception as e:output["error"] = str(e)return json.dumps(output, indent=4)if __name__ == "__main__":import sysif len(sys.argv) < 2:print("Usage: python script-main.py <inputData>")else:print(runTest(sys.argv[1]))% python3 script-main.pyUsage: python script-main.py <inputData>% python3 script-main.py "Hello World"{"status": false,"data": {},"error": null}```
wasmer 實測:
```% wasmer run script-main.wasmUsage: python script-main.py <inputData>% wasmer run script-main.wasm "Hello World"{"status": false,"data": {},"error": null}```
接著讓 Node.JS 來運行,這邊就來煩 ChatGPT 並小改一下,有了一個比較堪用的版本:
```% cat run.jsconst fs = require('fs');const { WASI } = require('wasi');const path = require('path');const { TextDecoder } = require('util');const runWasm = async (inputData) => {const wasmPath = path.resolve('./script-main.wasm');const wasmBinary = fs.readFileSync(wasmPath);// Setup a WASI instanceconst wasi = new WASI({args: inputData ? ['script-main.wasm', inputData] : ['script-main.wasm'],env: {},version: 'preview1'});// Create a memory buffer for the stdoutconst memory = new WebAssembly.Memory({ initial: 1 });// Compile and instantiate the WebAssembly moduleconst { instance } = await WebAssembly.instantiate(wasmBinary, {wasi_snapshot_preview1: wasi.wasiImport,env: { memory }});// Start the WASI instancewasi.start(instance);// Read and decode the stdout dataconst stdout = new Uint8Array(memory.buffer);const decoder = new TextDecoder('utf8');const output = decoder.decode(stdout);console.log(output.trim());};// Get the inputData from the command line argumentsrunWasm(process.argv[2] || null).catch(console.error);% nvm use v20Now using node v20.10.0 (npm v10.2.3)% node run.js(node:22046) ExperimentalWarning: WASI is an experimental feature and might change at any time(Use `node --trace-warnings ...` to show where the warning was created)Usage: python script-main.py <inputData>% node run.js "Hello World"(node:22050) ExperimentalWarning: WASI is an experimental feature and might change at any time(Use `node --trace-warnings ...` to show where the warning was created){"status": false,"data": {},"error": null}```
回過頭來,故事起源是想善用一些 open source 甚至不同程式語言的整合架構,因此稍微研究一些跨語言的整合,很可惜的,最佳的路線應當還是各自跑在各自的 runtime 環境,以上就當趣味筆記一下。
沒有留言:
張貼留言