新增解释器核心WASM验证
结论:已建立基于LinuxCNC解释器源码的最小WASM核心构建和minimal_linear夹具验证,并接入host smoke。
This commit is contained in:
@@ -18,6 +18,12 @@ The current WASM smoke validation command is:
|
||||
wasm-port/tests/wasm/node/verify_ini_wasm.sh
|
||||
```
|
||||
|
||||
The current WASM interpreter-core smoke validation command is:
|
||||
|
||||
```bash
|
||||
wasm-port/tests/wasm/node/verify_interp_wasm.sh
|
||||
```
|
||||
|
||||
The current OPFS host-boundary validation command is:
|
||||
|
||||
```bash
|
||||
@@ -64,6 +70,13 @@ The WASM INI smoke script builds `runtime/ui/ini-panel/linuxcnc_ini.js` and
|
||||
module through `runtime/sdk/src/linuxcnc-ini.js` in Node and verifies INI
|
||||
queries against a file written to the Emscripten filesystem.
|
||||
|
||||
The WASM interpreter-core smoke script builds
|
||||
`build/wasm/core/linuxcnc_interp.js` and `linuxcnc_interp.wasm` from the same
|
||||
vendored LinuxCNC interpreter source set used by the native minimal
|
||||
interpreter harness. It loads the module in Node, runs the `minimal_linear`
|
||||
G-code fixture through `Interp::execute()`, and compares emitted canonical
|
||||
events with `tests/fixtures/canon/minimal_linear.events`.
|
||||
|
||||
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
|
||||
directory creation, text save/load, missing file behavior, invalid relative
|
||||
@@ -129,9 +142,10 @@ 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 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, run `minimal_linear.ngc` through `Interp::execute()`, and match the native canonical event fixture. |
|
||||
| `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/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. |
|
||||
| `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM interpreter-core, OPFS, and browser smoke validation with shared WASM builds. |
|
||||
|
||||
## Fixture Coverage
|
||||
|
||||
@@ -174,8 +188,9 @@ Negative fixtures currently cover:
|
||||
|
||||
## Validation Boundaries
|
||||
|
||||
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
|
||||
Current full-core validation is native-only. WASM/SDK validation covers the
|
||||
INI parser smoke harness and an initial interpreter-core canonical event smoke
|
||||
for `minimal_linear`. OPFS validation is limited to the JavaScript
|
||||
host-boundary adapter plus the INI browser smoke harness. Full browser
|
||||
coverage, full SDK coverage, and full machine-session validation remain future
|
||||
work.
|
||||
|
||||
158
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp
Normal file
158
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_wasm.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#define private public
|
||||
#include "emc/rs274ngc/rs274ngc_interp.hh"
|
||||
#undef private
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#include "canon_event_sink.hh"
|
||||
#include "linuxcnc_hal_adapter.hh"
|
||||
#include "linuxcnc_tool_adapter.hh"
|
||||
|
||||
namespace {
|
||||
|
||||
void seed_hal_values()
|
||||
{
|
||||
standalone::reset_hal_adapter();
|
||||
|
||||
hal_data_u pin_bit{};
|
||||
pin_bit.b = true;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.pin-bit", HAL_BIT,
|
||||
pin_bit);
|
||||
|
||||
hal_data_u signal_float{};
|
||||
signal_float.f = 98.25;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Signal, "standalone.signal-float",
|
||||
HAL_FLOAT, signal_float);
|
||||
|
||||
hal_data_u param_s32{};
|
||||
param_s32.s = -17;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Param, "standalone.param-s32", HAL_S32,
|
||||
param_s32);
|
||||
|
||||
hal_data_u pin_u32{};
|
||||
pin_u32.u = 123456789u;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.pin-u32", HAL_U32,
|
||||
pin_u32);
|
||||
|
||||
hal_data_u signal_s64{};
|
||||
signal_s64.ls = -9000000000LL;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Signal, "standalone.signal-s64", HAL_S64,
|
||||
signal_s64);
|
||||
|
||||
hal_data_u param_u64{};
|
||||
param_u64.lu = 9000000000ULL;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Param, "standalone.param-u64", HAL_U64,
|
||||
param_u64);
|
||||
|
||||
hal_data_u disconnected{};
|
||||
disconnected.f = 12.5;
|
||||
standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.disconnected-float",
|
||||
HAL_FLOAT, disconnected, false);
|
||||
}
|
||||
|
||||
void seed_tool_values()
|
||||
{
|
||||
standalone::reset_tool_adapter();
|
||||
|
||||
CANON_TOOL_TABLE tool = standalone::tool_entry_init();
|
||||
tool.toolno = 2;
|
||||
tool.pocketno = 2;
|
||||
tool.offset.tran.z = 1.25;
|
||||
tool.diameter = 0.25;
|
||||
standalone::set_tool_entry(2, tool);
|
||||
}
|
||||
|
||||
void initialize_minimal_interp(Interp &interp)
|
||||
{
|
||||
seed_hal_values();
|
||||
seed_tool_values();
|
||||
interp._setup.length_units = CANON_UNITS_MM;
|
||||
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
|
||||
interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE;
|
||||
interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
|
||||
interp._setup.plane = CANON_PLANE::XY;
|
||||
interp._setup.motion_mode = G_0;
|
||||
interp._setup.percent_flag = false;
|
||||
interp._setup.sequence_number = 0;
|
||||
interp._setup.parameter_occurrence = 0;
|
||||
interp._setup.num_spindles = 1;
|
||||
interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS;
|
||||
interp._setup.parameter_g73_peck_clearance = 1.0;
|
||||
interp._setup.parameter_g83_peck_clearance = 1.0;
|
||||
interp._setup.parameters[5599] = 1.0;
|
||||
interp.load_tool_table();
|
||||
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
|
||||
}
|
||||
|
||||
std::vector<std::string> split_program_lines(const char *program_text)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream input(program_text ? program_text : "");
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty()) {
|
||||
lines.push_back(line + "\n");
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
char *copy_result(const std::string &result)
|
||||
{
|
||||
char *out = static_cast<char *>(std::malloc(result.size() + 1));
|
||||
if (!out) {
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(out, result.c_str(), result.size() + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
char *lcinterp_run_program(const char *program_text)
|
||||
{
|
||||
Interp interp;
|
||||
standalone::reset_canon_events();
|
||||
initialize_minimal_interp(interp);
|
||||
|
||||
const std::vector<std::string> program = split_program_lines(program_text);
|
||||
std::ostringstream output;
|
||||
if (program.empty()) {
|
||||
output << "error=empty fixture\n";
|
||||
return copy_result(output.str());
|
||||
}
|
||||
|
||||
for (std::size_t idx = 0; idx < program.size(); ++idx) {
|
||||
interp._setup.sequence_number = static_cast<int>(idx + 1);
|
||||
const int execute_rc = interp.execute(program[idx].c_str());
|
||||
output << "execute_line_" << (idx + 1) << "=" << execute_rc << "\n";
|
||||
if (execute_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
interp.error_text(execute_rc, error_buf, sizeof(error_buf));
|
||||
output << "error_text=" << error_buf << "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &event : standalone::canon_events()) {
|
||||
output << "canon_event=" << event << "\n";
|
||||
}
|
||||
return copy_result(output.str());
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void lcinterp_free_string(char *value)
|
||||
{
|
||||
std::free(value);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,47 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <wordexp.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
int wordexp(const char *words, wordexp_t *pwordexp, int)
|
||||
{
|
||||
if (!words || !pwordexp) {
|
||||
return WRDE_BADCHAR;
|
||||
}
|
||||
|
||||
char **wordv = static_cast<char **>(std::calloc(2, sizeof(char *)));
|
||||
if (!wordv) {
|
||||
return WRDE_NOSPACE;
|
||||
}
|
||||
|
||||
wordv[0] = static_cast<char *>(std::malloc(std::strlen(words) + 1));
|
||||
if (!wordv[0]) {
|
||||
std::free(wordv);
|
||||
return WRDE_NOSPACE;
|
||||
}
|
||||
|
||||
std::strcpy(wordv[0], words);
|
||||
pwordexp->we_wordc = 1;
|
||||
pwordexp->we_wordv = wordv;
|
||||
pwordexp->we_offs = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void wordfree(wordexp_t *pwordexp)
|
||||
{
|
||||
if (!pwordexp || !pwordexp->we_wordv) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < pwordexp->we_wordc; ++index) {
|
||||
std::free(pwordexp->we_wordv[index]);
|
||||
}
|
||||
std::free(pwordexp->we_wordv);
|
||||
pwordexp->we_wordc = 0;
|
||||
pwordexp->we_wordv = nullptr;
|
||||
pwordexp->we_offs = 0;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
14
wasm-port/runtime/core/shims/boost/noncopyable.hpp
Normal file
14
wasm-port/runtime/core/shims/boost/noncopyable.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
namespace boost {
|
||||
|
||||
class noncopyable {
|
||||
protected:
|
||||
noncopyable() = default;
|
||||
~noncopyable() = default;
|
||||
|
||||
noncopyable(const noncopyable &) = delete;
|
||||
noncopyable &operator=(const noncopyable &) = delete;
|
||||
};
|
||||
|
||||
} // namespace boost
|
||||
8
wasm-port/runtime/core/shims/linuxcnc_wasm_compat.hh
Normal file
8
wasm-port/runtime/core/shims/linuxcnc_wasm_compat.hh
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <libgen.h>
|
||||
|
||||
inline char *basename(const char *path)
|
||||
{
|
||||
return ::basename(const_cast<char *>(path));
|
||||
}
|
||||
@@ -4,7 +4,9 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
"$ROOT_DIR/tools/build_ini_panel.sh"
|
||||
"$ROOT_DIR/tools/build_wasm_core.sh"
|
||||
SKIP_INI_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_ini_wasm.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh"
|
||||
"$ROOT_DIR/tests/opfs/node/verify_file_service.sh"
|
||||
SKIP_INI_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
||||
|
||||
|
||||
62
wasm-port/tests/wasm/node/verify_interp_wasm.mjs
Normal file
62
wasm-port/tests/wasm/node/verify_interp_wasm.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js";
|
||||
|
||||
function allocCString(mod, value) {
|
||||
const bytes = mod.lengthBytesUTF8(value) + 1;
|
||||
const ptr = mod._malloc(bytes);
|
||||
mod.stringToUTF8(value, ptr, bytes);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function runProgram(mod, programText) {
|
||||
const programPtr = allocCString(mod, programText);
|
||||
let resultPtr = 0;
|
||||
try {
|
||||
resultPtr = mod._lcinterp_run_program(programPtr);
|
||||
assert.notEqual(resultPtr, 0);
|
||||
return mod.UTF8ToString(resultPtr);
|
||||
} finally {
|
||||
if (resultPtr) {
|
||||
mod._lcinterp_free_string(resultPtr);
|
||||
}
|
||||
mod._free(programPtr);
|
||||
}
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = resolve(__dirname, "../../..");
|
||||
const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm");
|
||||
|
||||
const interp = await createLinuxCncInterpModule({
|
||||
wasmBinary: readFileSync(wasmPath),
|
||||
print() {},
|
||||
printErr(message) {
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const fixtureName = "minimal_linear";
|
||||
const programText = readFileSync(
|
||||
resolve(rootDir, `tests/fixtures/gcode/${fixtureName}.ngc`),
|
||||
"utf8",
|
||||
);
|
||||
const expectedEvents = readFileSync(
|
||||
resolve(rootDir, `tests/fixtures/canon/${fixtureName}.events`),
|
||||
"utf8",
|
||||
).trimEnd();
|
||||
|
||||
const output = runProgram(interp, programText);
|
||||
const actualEventSet = new Set(
|
||||
output.split("\n").filter((line) => line.startsWith("canon_event=")),
|
||||
);
|
||||
const expectedEventLines = expectedEvents.split("\n").filter(Boolean);
|
||||
|
||||
for (const expectedEvent of expectedEventLines) {
|
||||
assert.equal(actualEventSet.has(expectedEvent), true, expectedEvent);
|
||||
}
|
||||
console.log("interp_wasm_node_smoke=ok");
|
||||
9
wasm-port/tests/wasm/node/verify_interp_wasm.sh
Executable file
9
wasm-port/tests/wasm/node/verify_interp_wasm.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
|
||||
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
|
||||
"$ROOT_DIR/tools/build_wasm_core.sh"
|
||||
fi
|
||||
node "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.mjs"
|
||||
77
wasm-port/tools/build_wasm_core.sh
Executable file
77
wasm-port/tools/build_wasm_core.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
VENDOR_DIR="$ROOT_DIR/vendor/linuxcnc"
|
||||
WRAP_DIR="$ROOT_DIR/runtime/core/linuxcnc_wrap"
|
||||
SHIM_DIR="$ROOT_DIR/runtime/core/shims"
|
||||
INCLUDE_DIR="$ROOT_DIR/runtime/core/include"
|
||||
OUT_DIR="$ROOT_DIR/build/wasm/core"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
COMMON_FLAGS=(
|
||||
-std=c++20
|
||||
-O2
|
||||
-D_GNU_SOURCE
|
||||
-DM_PI=3.14159265358979323846
|
||||
-DOBJECT_FWD_DWA2002724_HPP
|
||||
-DUNIT_TEST
|
||||
-DLINUXCNC_STANDALONE_USE_RS274_PRE_STATE
|
||||
-DEMC2_HOME=\"/standalone\"
|
||||
-include "$SHIM_DIR/linuxcnc_wasm_compat.hh"
|
||||
-I"$WRAP_DIR"
|
||||
-I"$SHIM_DIR"
|
||||
-I"$INCLUDE_DIR"
|
||||
-I"$VENDOR_DIR/src"
|
||||
-I"$VENDOR_DIR/src/rtapi"
|
||||
-I"$VENDOR_DIR/src/emc"
|
||||
-I"$VENDOR_DIR/src/emc/nml_intf"
|
||||
-I"$VENDOR_DIR/src/emc/motion"
|
||||
-I"$VENDOR_DIR/src/emc/tp"
|
||||
-I"$VENDOR_DIR/src/emc/kinematics"
|
||||
-I"$VENDOR_DIR/src/emc/ini"
|
||||
-I"$VENDOR_DIR/src/libnml/posemath"
|
||||
)
|
||||
|
||||
INTERP_CORE_SOURCES=(
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/modal_state.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_array.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_internal.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_read.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_check.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_arc.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_inverse.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_execute.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_convert.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_cycles.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_g7x.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_queue.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_find.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_namedparams.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_write.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/interp_o_word.cc"
|
||||
"$VENDOR_DIR/src/emc/rs274ngc/rs274ngc_pre.cc"
|
||||
"$WRAP_DIR/linuxcnc_hal_adapter.cpp"
|
||||
"$WRAP_DIR/linuxcnc_emc_status_runtime.cpp"
|
||||
"$WRAP_DIR/linuxcnc_interp_edge_stubs.cpp"
|
||||
"$WRAP_DIR/linuxcnc_runtime_state_stubs.cpp"
|
||||
"$WRAP_DIR/linuxcnc_tool_adapter.cpp"
|
||||
"$WRAP_DIR/linuxcnc_interp_minimal_runtime.cpp"
|
||||
"$WRAP_DIR/linuxcnc_wasm_posix_stubs.cpp"
|
||||
"$VENDOR_DIR/src/emc/ini/inifile.cc"
|
||||
)
|
||||
|
||||
emcc \
|
||||
"${COMMON_FLAGS[@]}" \
|
||||
"${INTERP_CORE_SOURCES[@]}" \
|
||||
"$WRAP_DIR/linuxcnc_interp_wasm.cpp" \
|
||||
-o "$OUT_DIR/linuxcnc_interp.js" \
|
||||
-s MODULARIZE=1 \
|
||||
-s EXPORT_ES6=1 \
|
||||
-s ENVIRONMENT=web,node \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s STACK_SIZE=2MB \
|
||||
-s NO_EXIT_RUNTIME=1 \
|
||||
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_free_string"]' \
|
||||
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
|
||||
Reference in New Issue
Block a user