按规划继续工作

结论:参数文件 restore/save 已通过解释器 WASM C ABI 直连 vendored LinuxCNC,native 与 host/WASM/browser 验证全部通过。
This commit is contained in:
2026-06-08 07:37:15 +08:00
parent e5697970df
commit 16585690ca
9 changed files with 223 additions and 9 deletions

View File

@@ -87,7 +87,10 @@ required LinuxCNC `_setup` state readback with the matching files in
`tests/fixtures/canon/`. It also writes selected G-code fixtures into the `tests/fixtures/canon/`. It also writes selected G-code fixtures into the
Emscripten filesystem through the SDK and runs them through LinuxCNC Emscripten filesystem through the SDK and runs them through LinuxCNC
`Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the `Interp::open()`, `Interp::read()`, and `Interp::execute()` to validate the
file execution path. file execution path. The same Node smoke writes LinuxCNC-format parameter
files into the Emscripten filesystem and validates vendored
`Interp::restore_parameters()` and `Interp::save_parameters()`, including the
saved parameter values and `.bak` backup file boundary.
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
@@ -164,7 +167,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 through the JS SDK 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/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, and match the native canonical event plus required state readback fixtures. | | `tests/wasm/node/verify_interp_wasm.sh` | Validates the initial interpreter-core WASM module can be built from vendored LinuxCNC interpreter source, loaded through the interpreter JS SDK, run the first fixture group through `Interp::execute()` and selected file fixtures through `Interp::open()`/`read()`/`execute()`, match the native canonical event plus required state readback fixtures, and run parameter-file restore/save through vendored LinuxCNC `Interp::restore_parameters()` and `Interp::save_parameters()`. |
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter, path model, session snapshot store, machine file store, and G-code text store 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, path model, session snapshot store, machine file store, and G-code text store used by the browser INI panel without moving file persistence into the WASM core. |
| `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, and G-code text round trip in a real browser runtime. | | `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, OPFS text-file round trip, generic session snapshot round trip, machine file text round trip, and G-code text round trip in a real browser runtime. |
| `tests/browser/verify_interp_browser.sh` | Validates the interpreter-core WASM module loads through the interpreter JS SDK in a real browser runtime and runs selected positive and negative canonical fixtures through vendored LinuxCNC `Interp::execute()` plus `Interp::open()`/`read()`/`execute()` via the exported C ABI. | | `tests/browser/verify_interp_browser.sh` | Validates the interpreter-core WASM module loads through the interpreter JS SDK in a real browser runtime and runs selected positive and negative canonical fixtures through vendored LinuxCNC `Interp::execute()` plus `Interp::open()`/`read()`/`execute()` via the exported C ABI. |
@@ -230,6 +233,11 @@ path additionally covers the same canonical-event fixture group, plus
`position_params` uses a dedicated file-path expectation under `position_params` uses a dedicated file-path expectation under
`tests/fixtures/canon_file/` because LinuxCNC file execution advances the `tests/fixtures/canon_file/` because LinuxCNC file execution advances the
post-execute position parameters differently than the line-by-line MDI smoke. post-execute position parameters differently than the line-by-line MDI smoke.
The Node WASM interpreter smoke also covers LinuxCNC parameter-file
restore/save behavior through the exported C ABI, including out-of-order file
rejection, missing-file success, required numeric parameter writeback, removal
of named-parameter-only lines from saved output, and the `.bak` backup produced
by vendored `save_parameters()`.
OPFS validation is limited to the JavaScript host-boundary adapter plus the INI OPFS validation is limited to the JavaScript host-boundary adapter plus the INI
browser smoke harness. Full browser coverage, full SDK coverage, and full browser smoke harness. Full browser coverage, full SDK coverage, and full
machine-session validation remain future work. machine-session validation remain future work.

View File

@@ -32,6 +32,7 @@ semantic rewrites:
| Interpreter state tags | LinuxCNC `UPDATE_TAG` callbacks are captured from the state tags packed by vendored interpreter code; standalone code does not derive modal state. | | Interpreter state tags | LinuxCNC `UPDATE_TAG` callbacks are captured from the state tags packed by vendored interpreter code; standalone code does not derive modal state. |
| Rotary indexer lock state | LinuxCNC `UNLOCK_ROTARY` and `LOCK_ROTARY` callbacks are captured from the vendored single-axis indexer path; standalone code does not implement rotary-indexing semantics. | | Rotary indexer lock state | LinuxCNC `UNLOCK_ROTARY` and `LOCK_ROTARY` callbacks are captured from the vendored single-axis indexer path; standalone code does not implement rotary-indexing semantics. |
| File flush | LinuxCNC `FINISH` is captured as a canonical test event on the vendored `%`-delimited file-reading path. | | File flush | LinuxCNC `FINISH` is captured as a canonical test event on the vendored `%`-delimited file-reading path. |
| WASM parameter-file backup | LinuxCNC `save_parameters()` calls `link()` to create the `.bak` parameter-file backup; `runtime/core/linuxcnc_wrap/linuxcnc_wasm_posix_stubs.cpp` maps that WASM/MEMFS edge to a file copy so the vendored save path can run unchanged. |
| Interpreter reset | LinuxCNC `ON_RESET` is captured as a canonical test event on the vendored interpreter reset/file-open path. | | Interpreter reset | LinuxCNC `ON_RESET` is captured as a canonical test event on the vendored interpreter reset/file-open path. |
| Interpreter init | LinuxCNC `INIT_CANON` and the canonical initialization sequence from vendored `Interp::init()` are captured as test events. | | Interpreter init | LinuxCNC `INIT_CANON` and the canonical initialization sequence from vendored `Interp::init()` are captured as test events. |
| Comment logging | LinuxCNC `LOGOPEN`, `LOG`, `LOGAPPEND`, and `LOGCLOSE` callbacks are captured as canonical test events instead of writing host log files. | | Comment logging | LinuxCNC `LOGOPEN`, `LOG`, `LOGAPPEND`, and `LOGCLOSE` callbacks are captured as canonical test events instead of writing host log files. |

View File

@@ -53,7 +53,7 @@ Current validation is intentionally mechanical:
| Dependency | LinuxCNC files that expose it | Standalone treatment | | Dependency | LinuxCNC files that expose it | Standalone treatment |
| --- | --- | --- | | --- | --- | --- |
| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, and pure-text machine-file persistence in `runtime/opfs/machine-file-store.js` | | Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; the interpreter WASM C ABI now validates parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, and pure-text machine-file persistence in `runtime/opfs/machine-file-store.js` |
| RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` | | RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` |
| NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes | | NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes |
| HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` | | HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` |
@@ -73,15 +73,17 @@ Current validation is intentionally mechanical:
- Browser/WASM C ABI and JS SDK layers are now present for the INI parser and - Browser/WASM C ABI and JS SDK layers are now present for the INI parser and
the current interpreter-core smoke scope. The interpreter SDK is a thin the current interpreter-core smoke scope. The interpreter SDK is a thin
allocation, filesystem, and C ABI wrapper over vendored LinuxCNC execution allocation, filesystem, and C ABI wrapper over vendored LinuxCNC execution
paths; it does not define G-code semantics. Full planner/session SDK paths, including parameter-file restore/save through vendored
coverage remains future work. `rs274ngc_pre.cc`; it does not define G-code or parameter semantics. Full
planner/session SDK coverage remains future work.
- 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. `runtime/opfs/path-model.js` now `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now
defines paths for INI, tool table, parameter file, G-code program, defines paths for INI, tool table, parameter file, G-code program,
preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js` preview-cache, and session-snapshot targets. `runtime/opfs/snapshot-store.js`
adds a generic JSON session snapshot envelope. adds a generic JSON session snapshot envelope.
`runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool `runtime/opfs/machine-file-store.js` adds pure-text storage for INI, tool
table, parameter file, and G-code program content; semantic loading for tool table, parameter file, and G-code program content; browser session plumbing
tables, parameter files, G-code programs, and machine-state restoration that feeds OPFS-backed parameter files into the existing WASM
remains future work. `restore_parameters()`/`save_parameters()` ABI, plus semantic loading for
tool tables and machine-state restoration, remains future work.
- Native LinuxCNC GUI code remains out of scope for implementation. - Native LinuxCNC GUI code remains out of scope for implementation.

View File

@@ -136,6 +136,36 @@ void append_events_and_state(std::ostringstream &output, const Interp &interp)
output << "post_execute.flood=" << interp._setup.flood << "\n"; output << "post_execute.flood=" << interp._setup.flood << "\n";
} }
void append_parameter_state(std::ostringstream &output, const Interp &interp)
{
output << "parameter_5161=" << interp._setup.parameters[5161] << "\n";
output << "parameter_5162=" << interp._setup.parameters[5162] << "\n";
output << "parameter_5220=" << interp._setup.parameters[5220] << "\n";
output << "parameter_5221=" << interp._setup.parameters[5221] << "\n";
output << "parameter_5399=" << interp._setup.parameters[5399] << "\n";
}
void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc)
{
if (rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
interp.error_text(rc, error_buf, sizeof(error_buf));
output << prefix << error_buf << "\n";
}
}
void apply_parameter_assignments(Interp &interp, const char *assignments)
{
std::istringstream input(assignments ? assignments : "");
int parameter = 0;
double value = 0.0;
while (input >> parameter >> value) {
if ((parameter > 0) && (parameter < interp_param_global::RS274NGC_MAX_PARAMETERS)) {
interp._setup.parameters[parameter] = value;
}
}
}
} // namespace } // namespace
extern "C" { extern "C" {
@@ -258,6 +288,35 @@ char *lcinterp_run_file(const char *path)
return lcinterp_run_file_with_ini(path, nullptr); return lcinterp_run_file_with_ini(path, nullptr);
} }
EMSCRIPTEN_KEEPALIVE
char *lcinterp_restore_parameters(const char *path)
{
Interp interp;
initialize_minimal_interp(interp);
std::ostringstream output;
const int rc = interp.restore_parameters(path);
output << "restore_parameters=" << rc << "\n";
append_error_text(output, interp, "restore_error_text=", rc);
append_parameter_state(output, interp);
return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_save_parameters(const char *path, const char *assignments)
{
Interp interp;
initialize_minimal_interp(interp);
apply_parameter_assignments(interp, assignments);
std::ostringstream output;
const int rc = interp.save_parameters(path, interp._setup.parameters);
output << "save_parameters=" << rc << "\n";
append_error_text(output, interp, "save_error_text=", rc);
append_parameter_state(output, interp);
return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
void lcinterp_free_string(char *value) void lcinterp_free_string(char *value)
{ {

View File

@@ -1,5 +1,7 @@
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <cerrno>
#include <cstdio>
#include <wordexp.h> #include <wordexp.h>
@@ -44,4 +46,59 @@ void wordfree(wordexp_t *pwordexp)
pwordexp->we_offs = 0; pwordexp->we_offs = 0;
} }
int link(const char *oldpath, const char *newpath)
{
FILE *source = std::fopen(oldpath, "rb");
if (!source) {
return -1;
}
FILE *target = std::fopen(newpath, "rb");
if (target) {
std::fclose(target);
std::fclose(source);
errno = EEXIST;
return -1;
}
target = std::fopen(newpath, "wb");
if (!target) {
const int saved_errno = errno;
std::fclose(source);
errno = saved_errno;
return -1;
}
char buffer[4096];
while (true) {
const std::size_t read = std::fread(buffer, 1, sizeof(buffer), source);
if (read > 0 && std::fwrite(buffer, 1, read, target) != read) {
const int saved_errno = errno;
std::fclose(target);
std::fclose(source);
errno = saved_errno;
return -1;
}
if (read < sizeof(buffer)) {
if (std::ferror(source)) {
const int saved_errno = errno;
std::fclose(target);
std::fclose(source);
errno = saved_errno;
return -1;
}
break;
}
}
if (std::fclose(target) != 0) {
const int saved_errno = errno;
std::fclose(source);
errno = saved_errno;
return -1;
}
std::fclose(source);
return 0;
}
} // extern "C" } // extern "C"

View File

@@ -38,4 +38,7 @@ vendored LinuxCNC RS274NGC sources and exposes:
- `runProgramWithIni(programText, iniPath)` - `runProgramWithIni(programText, iniPath)`
- `runFile(path)` - `runFile(path)`
- `runFileWithIni(path, iniPath)` - `runFileWithIni(path, iniPath)`
- `restoreParameters(path)`
- `saveParameters(path, values)`
- `writeTextFile(path, text)` - `writeTextFile(path, text)`
- `readTextFile(path)`

View File

@@ -54,6 +54,10 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
mod.FS.writeFile(path, text, { encoding: "utf8" }); mod.FS.writeFile(path, text, { encoding: "utf8" });
}, },
readTextFile(path) {
return mod.FS.readFile(path, { encoding: "utf8" });
},
runProgram(programText) { runProgram(programText) {
return callStringResult(mod, "lcinterp_run_program", programText); return callStringResult(mod, "lcinterp_run_program", programText);
}, },
@@ -69,5 +73,16 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
runFileWithIni(path, iniPath) { runFileWithIni(path, iniPath) {
return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath); return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath);
}, },
restoreParameters(path) {
return callStringResult(mod, "lcinterp_restore_parameters", path);
},
saveParameters(path, values) {
const assignments = Object.entries(values)
.map(([parameter, value]) => `${parameter} ${value}`)
.join("\n");
return callStringResult(mod, "lcinterp_save_parameters", path, assignments);
},
}; };
} }

View File

@@ -134,4 +134,73 @@ verifyExpectedOutput(
"utf8", "utf8",
).trimEnd(), ).trimEnd(),
); );
const parameterFilePath = "/work/rs274ngc.var";
interp.writeTextFile(
parameterFilePath,
[
"5161 10.5",
"5162 20.25",
"5220 1",
"5221 2.25",
"5399 44",
"<_named_param> 123",
"",
].join("\n"),
);
verifyExpectedOutput(
"restore_parameters",
interp.restoreParameters(parameterFilePath),
[
"restore_parameters=0",
"parameter_5161=10.5",
"parameter_5162=20.25",
"parameter_5220=1",
"parameter_5221=2.25",
"parameter_5399=44",
].join("\n"),
);
verifyExpectedOutput(
"restore_parameters_missing_file",
interp.restoreParameters("/work/missing.var"),
"restore_parameters=0",
);
const outOfOrderParameterFilePath = "/work/out-of-order.var";
interp.writeTextFile(outOfOrderParameterFilePath, "5220 1\n5161 2\n");
verifyExpectedOutput(
"restore_parameters_out_of_order",
interp.restoreParameters(outOfOrderParameterFilePath),
[
"restore_parameters=5",
"restore_error_text=Parameter file out of order",
].join("\n"),
);
verifyExpectedOutput(
"save_parameters",
interp.saveParameters(parameterFilePath, {
5161: 12.34,
5162: 56.78,
5220: 1.0,
5221: 9.87,
5399: 66.6,
}),
[
"save_parameters=0",
"parameter_5161=12.34",
"parameter_5162=56.78",
"parameter_5220=1",
"parameter_5221=9.87",
"parameter_5399=66.6",
].join("\n"),
);
const savedParameterFile = interp.readTextFile(parameterFilePath);
const backupParameterFile = interp.readTextFile(`${parameterFilePath}.bak`);
assert.equal(savedParameterFile.includes("5161\t12.340000"), true);
assert.equal(savedParameterFile.includes("5162\t56.780000"), true);
assert.equal(savedParameterFile.includes("5221\t9.870000"), true);
assert.equal(savedParameterFile.includes("5399\t66.600000"), true);
assert.equal(savedParameterFile.includes("_named_param"), false);
assert.equal(backupParameterFile.includes("5161 10.5"), true);
console.log("interp_wasm_node_smoke=ok"); console.log("interp_wasm_node_smoke=ok");

View File

@@ -95,5 +95,5 @@ link_wasm_module \
-s STACK_SIZE=2MB \ -s STACK_SIZE=2MB \
-s NO_EXIT_RUNTIME=1 \ -s NO_EXIT_RUNTIME=1 \
-s FORCE_FILESYSTEM=1 \ -s FORCE_FILESYSTEM=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_free_string"]' \ -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_free_string"]' \
-s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]' -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'