01 — Layers & Contracts¶
Why layers¶
NPS is a multi-language monorepo with a single deep C++ engine behind a
frozen C ABI. The six layers below are enforced by scripts/layer-fitness.sh
(ctest nps_n1m_8_layer_fitness, registered as the layer-fitness
lefthook pre-push hook). They exist to keep UI awareness out of the
engine, prevent L0 modules from re-entrantly pulling in the session
header, and keep the C ABI the only host border.
The six layers¶
flowchart TB
L5["L5 apps<br/>src/SlicerApp, src/SimulatorApp<br/>Avalonia + MainWindow.axaml.cs"]
L4["L4 Nps.Shell<br/>src/Nps.Shell<br/>UI-agnostic shell helpers"]
L3["L3 Nps.EngineHost<br/>src/Nps.EngineHost<br/>sole C# P/Invoke surface + RAII"]
L2["L2 C ABI<br/>src/Engine/abi/nps_c_api.h<br/>extern C + PODs only"]
L1["L1 Session<br/>src/Engine/engine.h, engine.cpp<br/>nps::Engine"]
L0["L0 modules<br/>src/Engine/internal/*<br/>zip, threemf, gcode, analysis, viewport, color"]
L5 -->|ProjectReference| L4
L5 -->|ProjectReference| L3
L4 -->|ProjectReference| L3
L3 -->|P/Invoke| L2
L2 -->|delegate| L1
L1 -->|include downward| L0
L5 apps (src/SlicerApp, src/SimulatorApp)¶
- Each is a thin Avalonia host:
App.axaml,MainWindow.axaml,Program.cs,MainWindow.axaml.cs. ProjectReferencesNps.EngineHost(sole host border) andNps.Shell(shared helpers). No direct native calls.- Per-app
EngineBridge.cswas removed; both apps useNps.EngineHost.NpsAbidirectly. - Avalonia package pins +
AvaloniaUseCompiledBindingsByDefaultlive insrc/Directory.Build.props. NoReactiveUIdependency; bindings are Avalonia's compiled bindings.
L4 shell (src/Nps.Shell)¶
- Shared by both apps. Contains UI-agnostic helpers that are not engine handle holders:
FixtureLocator.cs(92 LOC) — repo-root 3MF resolver.ShellTrace.cs(64 LOC) — stderr sink installer.StatusSink.cs(64 LOC) — stdout/stderr dual-writer.NonClosingTextWriter.cs(97 LOC) — preserves the sharedConsole.Errorstream on listener disposal.ViewportController.cs(357 LOC) — ownsSafeGcodeViewportHandle, camera state, sim cursor, overlay wiring.Controls/—ActivityBarHost,PanelHost,ShellKeybindings,FindingItemview model.- Does not parse, does not construct meshes. Consumes engine-prepared
buffers via
ViewportControllerand renders.
L3 EngineHost (src/Nps.EngineHost)¶
The sole, shared C# host-border library. Consumed by both apps and the C# interop tests.
NpsAbi.cs(532 LOC) —[DllImport("NPSEngine", CallingConvention = CallingConvention.Cdecl)]declarations that mirrornps_c_api.hexactly. This is the only file in the entire C# tree that declares[DllImport("NPSEngine")]. Every other C# source usesNps.EngineHost.NpsAbi.*only.SafeEngineHandle.cs(67 LOC) andSafeGcodeViewportHandle.cs(49 LOC) —SafeHandleRAII wrappers around the opaquevoid*handles returned byCreateEngine/CreateGcodeViewport.EngineSession.cs(595 LOC) — session-shaped facade overNpsAbi. Owns aSafeEngineHandle, exposes load / gen / analyze / findings summary / detail / warp grid queries.- POD mirrors for ABI structs (
NpsGcodeMove,NpsLoadOptions,NpsMaterialProfile, ...). Sizes and layouts are guarded bystatic_asserton the C++ side and explicit field layout on the C# side, plusMarshal.SizeOfroundtrips intests/NPSEngine.Tests/EngineBridgeTests.cs(theAbiPODLayout_Frozentest).
L2 C ABI (src/Engine/abi/nps_c_api.h)¶
- Stable surface between C++ and C#. The C# host border is generated by reading this surface; never the other way around.
- Header length: 418 LOC. Exports 59 production symbols and
9 test-gated symbols (
NPS_ENABLE_TEST_HOOKS). - Across this boundary: only PODs, opaque handles,
const char*strings owned by the engine, rawfloat*/uint32_t*/uint8_t*buffers pointing into engine-owned memory. No STL. No exceptions. - Frozen POD sizes asserted at compile time on the C++ side
(
sizeof(NpsGcodeMove) == 36, etc.) and at runtime on the C# side (Marshal.SizeOfroundtrips in tests). NPS_ABI_VERSIONis bumped on any breaking change; consumers assert parity at startup viaGetAbiVersion(). The change policy lives indocs/adr/0001-abi-change-policy.md.
L1 Session (src/Engine/engine.h + engine.cpp)¶
nps::Engineis the single stateful session class, instantiated viaCreateEngineand destroyed viaDestroyEngine.- Public surface: load entry points, mesh query API, demo toolpath generator, G-code parser, G-code analyzer entry, findings accessors, warp grid accessors. Every C ABI export delegates to a method on this class.
- Owns canonical state: loaded mesh + palette + plate + unit, last parsed/generated G-code text, parsed/generated move array, analysis manager state, last error message, inferred slicer/material/printer.
- Single-threaded. Bad calls return safe defaults (counts = 0, unit =
"millimeter", geometry pointersnullptr).
L0 modules (src/Engine/internal/*)¶
- Reachable only through
engine.cppand the Catch2 test harness. - Folder layout today:
engine_zip.h+engine_zip.cpp— ZIP/OPC container reader (ZipArchiveReader).engine_threemf.h—ModelXMLParser,MeshResolver, matrix, component graph types.engine_gcode.h— G-code parser test hooks + simplified grid scoring.engine_gcode_store.h— value member owning last generated / parsed G-code + move array.engine_viewport.h—GcodeViewportclass + render data model.engine_analysis.h—AnalysisManager,IAnalysisEnginederivatives (Thermal, Structural, Motion, Hole, Retraction, Flow).engine_color.h—ColorParser(hex, filament palette, paint-code decoder).engine_mesh_store.h— value member owning loader output + cached totals.engine_value_types.h— small PODs (Vec3,Position,Color3,BedGeometry,CameraState,OverlayState).- Plus sibling headers in
src/Engine/root:engine_data.h,engine_analysis_result.h,engine_material.h(catalog + profiles).
Include DAG (compile-time layer direction)¶
The two edges that most easily rot are forbidden at compile time:
- No L0 -> session. A leaf module including
engine.hwould pull the whole session surface into every module TU. Onlyengine.cppandtests/may includeengine.h. - No Engine -> UI.
src/Engine/may not contain any Avalonia / XAML /.cs/.axaml/.csprojfiles. The engine is host-agnostic.
flowchart BT
ABI["src/Engine/abi/nps_c_api.h<br/>L2 C ABI PODs + NpsResult<br/>depends on nothing above"]
L0["src/Engine/internal/*.h<br/>L0 module headers / TUs<br/>may include: own header,<br/>sibling L0 value/data headers,<br/>and nps_c_api.h.<br/>MUST NOT include engine.h."]
L1["src/Engine/engine.h + engine.cpp<br/>L1 nps::Engine session facade.<br/>Includes the L0 module headers.<br/>The ONLY engine surface<br/>(besides tests/) permitted<br/>to include itself."]
DATA["src/Engine/engine_data.h,<br/>engine_analysis_result.h,<br/>engine_material.h<br/>value PODs reachable from both sides"]
DATA --> ABI
L1 --> L0
L0 --> ABI
L0 --> DATA
Both rules are asserted by scripts/layer-fitness.sh, registered as
ctest nps_n1m_8_layer_fitness, with adversarial coverage by
scripts/layer-fitness-tests.sh (ctest
nps_n1m_8_layer_fitness_mutation_tests).
Forbidden edges¶
| From | To | Status | Why |
|---|---|---|---|
| L5 app code | L0 module | FORBIDDEN | All engine work must cross L3 -> L2 -> L1. |
| L5 / L4 code | native ABI (DllImport) |
FORBIDDEN outside Nps.EngineHost |
Sole host border. |
| L4 Shell/Viewport | L0 module | FORBIDDEN | No direct calls; UI consumes engine results only. |
| L3 EngineHost | L1 / L0 direct | FORBIDDEN | EngineHost may only call L2 (C ABI). |
| L2 C ABI | STL types / exceptions | FORBIDDEN | Only PODs / opaque handles / C strings cross. |
| L1 Session | L4 / L5 | FORBIDDEN | Engine has no UI awareness, no Avalonia deps. |
| L0 module | another L0 via UI | FORBIDDEN | Cross-module calls happen inside L1, never via UI. |
| Any C# layer | parsing / mesh construction | FORBIDDEN | Deep engine only. |
| L2 / L1 / L0 | filesystem beyond the loaded file | FORBIDDEN outside engine | UI owns file dialogs. |
See also¶
overview.mdengine-modules.mddocs/architecture.md(full contract layer)docs/abi-inventory.md(C ABI inventory)scripts/layer-fitness.sh(enforcement)