按建议,继续完成后续工作

结论:已新增 INI WASM JS SDK 封装,把 UI 和 Node smoke test 改为通过 SDK 调用 vendored LinuxCNC inifile.cc 的导出 C ABI,并同步更新兼容性、复用和漂移文档。
This commit is contained in:
2026-06-08 00:23:48 +08:00
parent 9a9fb5d4c1
commit 0d78849d80
7 changed files with 121 additions and 103 deletions

View File

@@ -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 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 `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 module through `runtime/sdk/src/linuxcnc-ini.js` in Node and verifies INI
written to the Emscripten filesystem. queries against a file written to the Emscripten filesystem.
The OPFS host-boundary script validates the JavaScript file-service adapter 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 with a Node mock of the browser File System Access handles. It covers nested
@@ -103,7 +103,7 @@ The validation fails if:
| Harness | Purpose | | 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. | | `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 ## Fixture Coverage
@@ -147,10 +147,10 @@ Negative fixtures currently cover:
## Validation Boundaries ## Validation Boundaries
Current full-core validation is native-only. WASM validation is limited to the Current full-core validation is native-only. WASM/SDK validation is limited to
INI parser smoke harness. OPFS validation is limited to the JavaScript the INI parser smoke harness. OPFS validation is limited to the JavaScript
host-boundary adapter. Browser, SDK, and full machine-session validation host-boundary adapter. Browser-level validation, full SDK coverage, and full
remain future work. machine-session validation remain future work.
The current fixture expectations validate standalone behavior against both the The current fixture expectations validate standalone behavior against both the
vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for vendored LinuxCNC source path and an upstream `rs274` side-by-side baseline for

View File

@@ -58,7 +58,8 @@ semantic rewrites:
- No browser/full-core WASM parity tests yet. The INI parser now has a Node - No browser/full-core WASM parity tests yet. The INI parser now has a Node
WASM smoke harness against vendored LinuxCNC `inifile.cc`. 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 - OPFS validation is currently limited to a Node mock of the browser
file-service adapter; browser-level OPFS validation is not yet established. file-service adapter; browser-level OPFS validation is not yet established.
- Identity/trivial, `5axiskins`, TRT `xyzac`/`xyzbc`, delta, SCARA, PUMA, - Identity/trivial, `5axiskins`, TRT `xyzac`/`xyzbc`, delta, SCARA, PUMA,

View File

@@ -31,7 +31,7 @@ Current validation is intentionally mechanical:
| Capability | LinuxCNC source files | Port classification | Standalone boundary | Current validation | | 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 | | 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 | | 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` | | 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 - Cutter compensation positive motion and negative interpreter paths are
fixture-covered through vendored `interp_convert.cc` and `interp_queue.cc`. 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 - 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 - OPFS persistence is connected to the INI panel through the host-side
`runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code `runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code
program loading, and browser-level OPFS validation remain future work. program loading, and browser-level OPFS validation remain future work.

View File

@@ -0,0 +1,4 @@
{
"private": true,
"type": "module"
}

View File

@@ -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),
]),
);
},
};
}

View File

@@ -1,4 +1,4 @@
import createLinuxCncIniModule from "./linuxcnc_ini.js"; import { createLinuxCncIniSdk } from "../../sdk/src/linuxcnc-ini.js";
import { import {
getOpfsRoot, getOpfsRoot,
loadTextFile, loadTextFile,
@@ -24,7 +24,7 @@ const fields = {
parameterFile: document.getElementById("field-parameter-file"), parameterFile: document.getElementById("field-parameter-file"),
}; };
let moduleInstance = null; let iniSdk = null;
function setBadge(node, text, className = "badge") { function setBadge(node, text, className = "badge") {
node.className = className; node.className = className;
@@ -40,49 +40,15 @@ function setField(name, value) {
fields[name].textContent = value ?? "-"; fields[name].textContent = value ?? "-";
} }
function requireModule() { function requireIniSdk() {
if (!moduleInstance) { if (!iniSdk) {
throw new Error("WASM module is not ready yet."); throw new Error("WASM module is not ready yet.");
} }
return moduleInstance; return iniSdk;
}
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);
}
} }
function syncEditorToWasmFs() { function syncEditorToWasmFs() {
const mod = requireModule(); requireIniSdk().writeTextFile(WASM_FILE, editor.value);
try {
mod.FS.mkdir("/work");
} catch {
// already exists
}
mod.FS.writeFile(WASM_FILE, editor.value, { encoding: "utf8" });
} }
async function loadSample() { async function loadSample() {
@@ -97,7 +63,7 @@ async function loadSample() {
async function boot() { async function boot() {
try { try {
moduleInstance = await createLinuxCncIniModule(); iniSdk = await createLinuxCncIniSdk();
setBadge(wasmBadge, "WASM: ready"); setBadge(wasmBadge, "WASM: ready");
} catch (error) { } catch (error) {
setBadge(wasmBadge, "WASM: failed", "badge danger"); setBadge(wasmBadge, "WASM: failed", "badge danger");
@@ -152,17 +118,18 @@ document.getElementById("sync-wasm").addEventListener("click", () => {
document.getElementById("query").addEventListener("click", async () => { document.getElementById("query").addEventListener("click", async () => {
try { try {
syncEditorToWasmFs(); 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")); for (const [name, value] of Object.entries(values)) {
setField("display", iniQuery(mod, WASM_FILE, "DISPLAY", "DISPLAY")); setField(name, value);
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"),
);
setLog("Queried LinuxCNC INI fields through the standalone WASM parser."); setLog("Queried LinuxCNC INI fields through the standalone WASM parser.");
} catch (error) { } catch (error) {

View File

@@ -3,14 +3,14 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import assert from "node:assert/strict"; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../.."); const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "runtime/ui/ini-panel/linuxcnc_ini.wasm"); const wasmPath = resolve(rootDir, "runtime/ui/ini-panel/linuxcnc_ini.wasm");
const mod = await createLinuxCncIniModule({ const ini = await createLinuxCncIniSdk({
wasmBinary: readFileSync(wasmPath), wasmBinary: readFileSync(wasmPath),
print() {}, print() {},
printErr(message) { printErr(message) {
@@ -18,33 +18,7 @@ const mod = await createLinuxCncIniModule({
}, },
}); });
function allocCString(value) { ini.writeTextFile(
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(
"/work/node-smoke.ini", "/work/node-smoke.ini",
`[EMC] `[EMC]
MACHINE = wasm-node-smoke MACHINE = wasm-node-smoke
@@ -57,21 +31,25 @@ COORDINATES = X Y Z A B
KINEMATICS = xyzbc-trt-kins KINEMATICS = xyzbc-trt-kins
JOINTS = 5 JOINTS = 5
`, `,
{ encoding: "utf8" },
); );
assert.deepEqual(queryIni("/work/node-smoke.ini", "EMC", "MACHINE"), { assert.equal(
rc: 0, ini.getString("/work/node-smoke.ini", "EMC", "MACHINE"),
value: "wasm-node-smoke", "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"); console.log("ini_wasm_node_smoke=ok");