From 0d78849d80f0cf9997c791665e05ea621925963b Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 8 Jun 2026 00:23:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=BB=BA=E8=AE=AE=EF=BC=8C=E7=BB=A7?= =?UTF-8?q?=E7=BB=AD=E5=AE=8C=E6=88=90=E5=90=8E=E7=BB=AD=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:已新增 INI WASM JS SDK 封装,把 UI 和 Node smoke test 改为通过 SDK 调用 vendored LinuxCNC inifile.cc 的导出 C ABI,并同步更新兼容性、复用和漂移文档。 --- wasm-port/docs/compatibility-validation.md | 14 ++-- wasm-port/docs/drift-report.md | 3 +- wasm-port/docs/source-reuse-map.md | 5 +- wasm-port/runtime/sdk/package.json | 4 ++ wasm-port/runtime/sdk/src/linuxcnc-ini.js | 67 ++++++++++++++++++ wasm-port/runtime/ui/ini-panel/app.js | 69 +++++-------------- wasm-port/tests/wasm/node/verify_ini_wasm.mjs | 62 ++++++----------- 7 files changed, 121 insertions(+), 103 deletions(-) create mode 100644 wasm-port/runtime/sdk/package.json create mode 100644 wasm-port/runtime/sdk/src/linuxcnc-ini.js diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index 4f67706..e3827cb 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -49,8 +49,8 @@ The native validation script runs these checks in order: The WASM INI smoke script builds `runtime/ui/ini-panel/linuxcnc_ini.js` and `linuxcnc_ini.wasm` from vendored LinuxCNC `inifile.cc`, then loads that -module in Node and verifies `lcini_get_string()` against an in-memory INI file -written to the Emscripten filesystem. +module through `runtime/sdk/src/linuxcnc-ini.js` in Node and verifies INI +queries against a file written to the Emscripten filesystem. The OPFS host-boundary script validates the JavaScript file-service adapter with a Node mock of the browser File System Access handles. It covers nested @@ -103,7 +103,7 @@ The validation fails if: | Harness | Purpose | | --- | --- | -| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded in Node, and queried through the exported C ABI. | +| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. | | `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter used by the browser INI panel without moving file persistence into the WASM core. | ## Fixture Coverage @@ -147,10 +147,10 @@ Negative fixtures currently cover: ## Validation Boundaries -Current full-core validation is native-only. WASM validation is limited to the -INI parser smoke harness. OPFS validation is limited to the JavaScript -host-boundary adapter. Browser, SDK, and full machine-session validation -remain future work. +Current full-core validation is native-only. WASM/SDK validation is limited to +the INI parser smoke harness. OPFS validation is limited to the JavaScript +host-boundary adapter. Browser-level validation, full SDK coverage, and full +machine-session validation remain future work. The current fixture expectations validate standalone behavior against both the vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index bdd0dd0..0acd558 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -58,7 +58,8 @@ semantic rewrites: - No browser/full-core WASM parity tests yet. The INI parser now has a Node WASM smoke harness against vendored LinuxCNC `inifile.cc`. -- No JS SDK validation yet. +- JS SDK validation is currently limited to the INI WASM wrapper around + vendored LinuxCNC `inifile.cc`. - OPFS validation is currently limited to a Node mock of the browser file-service adapter; browser-level OPFS validation is not yet established. - Identity/trivial, `5axiskins`, TRT `xyzac`/`xyzbc`, delta, SCARA, PUMA, diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index e6c8fc2..611db22 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -31,7 +31,7 @@ Current validation is intentionally mechanical: | Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation | | --- | --- | --- | --- | --- | -| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh` | +| INI parsing | `src/emc/ini/inifile.cc`, `inifile.h`, `inifile.hh` | Copy unchanged | Native file IO remains LinuxCNC-style in the vendored parser; browser OPFS integration remains outside this layer; `runtime/sdk/src/linuxcnc-ini.js` only wraps the exported WASM C ABI | Vendor byte sync, `linuxcnc_ini_probe`, `linuxcnc_inifile_source_probe`, `tests/wasm/node/verify_ini_wasm.sh` | | RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes | | Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses | | Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe` | @@ -69,7 +69,8 @@ Current validation is intentionally mechanical: - Cutter compensation positive motion and negative interpreter paths are fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. - Browser/WASM C ABI and JS SDK layers are not yet built for the full - interpreter/planner core. The INI parser has a Node WASM smoke harness. + interpreter/planner core. The INI parser has a minimal JS SDK wrapper and + Node WASM smoke harness. - OPFS persistence is connected to the INI panel through the host-side `runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code program loading, and browser-level OPFS validation remain future work. diff --git a/wasm-port/runtime/sdk/package.json b/wasm-port/runtime/sdk/package.json new file mode 100644 index 0000000..e986b24 --- /dev/null +++ b/wasm-port/runtime/sdk/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "module" +} diff --git a/wasm-port/runtime/sdk/src/linuxcnc-ini.js b/wasm-port/runtime/sdk/src/linuxcnc-ini.js new file mode 100644 index 0000000..38a75d7 --- /dev/null +++ b/wasm-port/runtime/sdk/src/linuxcnc-ini.js @@ -0,0 +1,67 @@ +import createLinuxCncIniModule from "../../ui/ini-panel/linuxcnc_ini.js"; + +function allocCString(mod, value) { + const bytes = mod.lengthBytesUTF8(value) + 1; + const ptr = mod._malloc(bytes); + mod.stringToUTF8(value, ptr, bytes); + return ptr; +} + +function ensureParentPath(mod, path) { + const parts = path.split("/").filter(Boolean); + let current = ""; + for (const part of parts.slice(0, -1)) { + current += `/${part}`; + try { + mod.FS.mkdir(current); + } catch { + // Directory already exists. + } + } +} + +export async function createLinuxCncIniSdk(moduleOptions = {}) { + const mod = await createLinuxCncIniModule(moduleOptions); + + return { + module: mod, + + writeTextFile(path, text) { + ensureParentPath(mod, path); + mod.FS.writeFile(path, text, { encoding: "utf8" }); + }, + + getString(path, section, tag) { + const pathPtr = allocCString(mod, path); + const sectionPtr = allocCString(mod, section); + const tagPtr = allocCString(mod, tag); + const outSize = 2048; + const outPtr = mod._malloc(outSize); + + try { + const rc = mod._lcini_get_string( + pathPtr, + sectionPtr, + tagPtr, + outPtr, + outSize, + ); + return rc === 0 ? mod.UTF8ToString(outPtr) : null; + } finally { + mod._free(pathPtr); + mod._free(sectionPtr); + mod._free(tagPtr); + mod._free(outPtr); + } + }, + + getFields(path, fields) { + return Object.fromEntries( + Object.entries(fields).map(([name, query]) => [ + name, + this.getString(path, query.section, query.tag), + ]), + ); + }, + }; +} diff --git a/wasm-port/runtime/ui/ini-panel/app.js b/wasm-port/runtime/ui/ini-panel/app.js index 8ef676d..3675b8e 100644 --- a/wasm-port/runtime/ui/ini-panel/app.js +++ b/wasm-port/runtime/ui/ini-panel/app.js @@ -1,4 +1,4 @@ -import createLinuxCncIniModule from "./linuxcnc_ini.js"; +import { createLinuxCncIniSdk } from "../../sdk/src/linuxcnc-ini.js"; import { getOpfsRoot, loadTextFile, @@ -24,7 +24,7 @@ const fields = { parameterFile: document.getElementById("field-parameter-file"), }; -let moduleInstance = null; +let iniSdk = null; function setBadge(node, text, className = "badge") { node.className = className; @@ -40,49 +40,15 @@ function setField(name, value) { fields[name].textContent = value ?? "-"; } -function requireModule() { - if (!moduleInstance) { +function requireIniSdk() { + if (!iniSdk) { throw new Error("WASM module is not ready yet."); } - return moduleInstance; -} - -function allocCString(mod, value) { - const bytes = mod.lengthBytesUTF8(value) + 1; - const ptr = mod._malloc(bytes); - mod.stringToUTF8(value, ptr, bytes); - return ptr; -} - -function iniQuery(mod, wasmPath, section, tag) { - const pathPtr = allocCString(mod, wasmPath); - const sectionPtr = allocCString(mod, section); - const tagPtr = allocCString(mod, tag); - const outSize = 2048; - const outPtr = mod._malloc(outSize); - - try { - const rc = mod._lcini_get_string(pathPtr, sectionPtr, tagPtr, outPtr, outSize); - if (rc !== 0) { - return null; - } - return mod.UTF8ToString(outPtr); - } finally { - mod._free(pathPtr); - mod._free(sectionPtr); - mod._free(tagPtr); - mod._free(outPtr); - } + return iniSdk; } function syncEditorToWasmFs() { - const mod = requireModule(); - try { - mod.FS.mkdir("/work"); - } catch { - // already exists - } - mod.FS.writeFile(WASM_FILE, editor.value, { encoding: "utf8" }); + requireIniSdk().writeTextFile(WASM_FILE, editor.value); } async function loadSample() { @@ -97,7 +63,7 @@ async function loadSample() { async function boot() { try { - moduleInstance = await createLinuxCncIniModule(); + iniSdk = await createLinuxCncIniSdk(); setBadge(wasmBadge, "WASM: ready"); } catch (error) { setBadge(wasmBadge, "WASM: failed", "badge danger"); @@ -152,17 +118,18 @@ document.getElementById("sync-wasm").addEventListener("click", () => { document.getElementById("query").addEventListener("click", async () => { try { syncEditorToWasmFs(); - const mod = requireModule(); + const values = requireIniSdk().getFields(WASM_FILE, { + machine: { section: "EMC", tag: "MACHINE" }, + display: { section: "DISPLAY", tag: "DISPLAY" }, + kinematics: { section: "KINS", tag: "KINEMATICS" }, + joints: { section: "KINS", tag: "JOINTS" }, + coordinates: { section: "TRAJ", tag: "COORDINATES" }, + parameterFile: { section: "RS274NGC", tag: "PARAMETER_FILE" }, + }); - setField("machine", iniQuery(mod, WASM_FILE, "EMC", "MACHINE")); - setField("display", iniQuery(mod, WASM_FILE, "DISPLAY", "DISPLAY")); - setField("kinematics", iniQuery(mod, WASM_FILE, "KINS", "KINEMATICS")); - setField("joints", iniQuery(mod, WASM_FILE, "KINS", "JOINTS")); - setField("coordinates", iniQuery(mod, WASM_FILE, "TRAJ", "COORDINATES")); - setField( - "parameterFile", - iniQuery(mod, WASM_FILE, "RS274NGC", "PARAMETER_FILE"), - ); + for (const [name, value] of Object.entries(values)) { + setField(name, value); + } setLog("Queried LinuxCNC INI fields through the standalone WASM parser."); } catch (error) { diff --git a/wasm-port/tests/wasm/node/verify_ini_wasm.mjs b/wasm-port/tests/wasm/node/verify_ini_wasm.mjs index 816803c..25f0ca5 100644 --- a/wasm-port/tests/wasm/node/verify_ini_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_ini_wasm.mjs @@ -3,14 +3,14 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import assert from "node:assert/strict"; -import createLinuxCncIniModule from "../../../runtime/ui/ini-panel/linuxcnc_ini.js"; +import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/linuxcnc-ini.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const rootDir = resolve(__dirname, "../../.."); const wasmPath = resolve(rootDir, "runtime/ui/ini-panel/linuxcnc_ini.wasm"); -const mod = await createLinuxCncIniModule({ +const ini = await createLinuxCncIniSdk({ wasmBinary: readFileSync(wasmPath), print() {}, printErr(message) { @@ -18,33 +18,7 @@ const mod = await createLinuxCncIniModule({ }, }); -function allocCString(value) { - const bytes = mod.lengthBytesUTF8(value) + 1; - const ptr = mod._malloc(bytes); - mod.stringToUTF8(value, ptr, bytes); - return ptr; -} - -function queryIni(path, section, tag) { - const pathPtr = allocCString(path); - const sectionPtr = allocCString(section); - const tagPtr = allocCString(tag); - const outSize = 256; - const outPtr = mod._malloc(outSize); - - try { - const rc = mod._lcini_get_string(pathPtr, sectionPtr, tagPtr, outPtr, outSize); - return { rc, value: rc === 0 ? mod.UTF8ToString(outPtr) : null }; - } finally { - mod._free(pathPtr); - mod._free(sectionPtr); - mod._free(tagPtr); - mod._free(outPtr); - } -} - -mod.FS.mkdir("/work"); -mod.FS.writeFile( +ini.writeTextFile( "/work/node-smoke.ini", `[EMC] MACHINE = wasm-node-smoke @@ -57,21 +31,25 @@ COORDINATES = X Y Z A B KINEMATICS = xyzbc-trt-kins JOINTS = 5 `, - { encoding: "utf8" }, ); -assert.deepEqual(queryIni("/work/node-smoke.ini", "EMC", "MACHINE"), { - rc: 0, - value: "wasm-node-smoke", +assert.equal( + ini.getString("/work/node-smoke.ini", "EMC", "MACHINE"), + "wasm-node-smoke", +); +assert.equal(ini.getString("/work/node-smoke.ini", "TRAJ", "LINEAR_UNITS"), "mm"); +assert.equal( + ini.getString("/work/node-smoke.ini", "KINS", "KINEMATICS"), + "xyzbc-trt-kins", +); +assert.equal(ini.getString("/work/node-smoke.ini", "TRAJ", "MISSING"), null); + +assert.deepEqual(ini.getFields("/work/node-smoke.ini", { + machine: { section: "EMC", tag: "MACHINE" }, + joints: { section: "KINS", tag: "JOINTS" }, +}), { + machine: "wasm-node-smoke", + joints: "5", }); -assert.deepEqual(queryIni("/work/node-smoke.ini", "TRAJ", "LINEAR_UNITS"), { - rc: 0, - value: "mm", -}); -assert.deepEqual(queryIni("/work/node-smoke.ini", "KINS", "KINEMATICS"), { - rc: 0, - value: "xyzbc-trt-kins", -}); -assert.notEqual(queryIni("/work/node-smoke.ini", "TRAJ", "MISSING").rc, 0); console.log("ini_wasm_node_smoke=ok");