Time-Domain System Equivalent logoTime-Domain System EquivalentLinear dynamics, solved faster.Discuss Integration

Plugin System

Plugin loading, discovery, contracts, and extension points.

Use this chapter when you need to deploy, validate, or troubleshoot TDSE solver plugins in a packaged environment. Each plugin is a shared library (.so on Linux, .dll on Windows) that the host loads at runtime to execute transient and AC simulations.

If your PoC stays on the default CPU path and you do not need manifest, deployment, or ABI decisions yet, you can defer this chapter until deployment planning. Come here when the question becomes compatibility, routing, signing, or plugin-health evidence.

Use the surrounding boundary chapters with it:

  • use Platform Notes for the broader host, package, and qualification boundary
  • use Element Reference when the deployment risk is really netlist coverage rather than plugin loading

Architecture

Three simulation engine plugins are provided:

PluginBinary nameSolver family
CPU Denselibtdse_sim_cpu_dense.soLAPACK, LU
CPU Sparselibtdse_sim_cpu_sparse.soKLU, MKL Pardiso
CUDAlibtdse_sim_cuda.soCUDA dense, CUDA sparse

The host selects the correct plugin automatically based on the requested solver backend. Each plugin exports a standard entry point tdse_plugin_get_info() that returns ABI metadata and a function table.

Use the canonical installed headers for new integrations:

HeaderPurpose
tdse/plugin/simulation.hFormal Simulation Plugin C ABI
tdse/plugin/manifest.hppAuthenticated manifest parsing and validation
tdse/plugin/metrics.hPlugin metrics data and C++ snapshot reader

The corresponding top-level headers remain compatibility forwarding includes. They add no declarations, symbols, ownership rules, or compatibility promise of their own.

ABI Version

The current Simulation Plugin ABI is 1.1. ABI 1.0 was the first formal version; earlier repository drafts were not published customer ABIs and do not define a compatibility lane.

Within a published major line, a host may accept an older published minor only when the package includes executable compatibility evidence for it. A newer plugin minor is rejected by an older host, and a major mismatch is rejected. At the initial v1.0 publication, host and plugin therefore match exactly.

The ABI contract and compatibility policy are documented in the SDK headers and the Plugin ABI Evolution Guide shipped with the evaluation package.

Isolation And Trust Model

The supported production plugin mode is trusted in-process execution. The plugin shared library is loaded into the TDSE host process and calls through the public C ABI. This keeps latency and data movement low, but it is not a sandbox.

Use in-process plugins only for:

  • TDSE-shipped plugins
  • explicitly trusted partner plugins
  • customer plugins that have passed the same manifest, ABI, stress, and release evidence gates as TDSE-shipped plugins

Manifest signatures, requested identity, ABI/SDK policy, path policy, and held-file hash checks reject stale or mispackaged trusted plugins before native loading. Post-load metadata and vtable checks run before a plugin generation is published. tdse_plugin_doctor and health checks inspect plugins after this boundary. None of these controls contains arbitrary native-code behavior after the plugin is loaded: a plugin crash, process abort, memory corruption, or data race is process-fatal for the host.

The three TDSE-shipped ABI v1 plugins are a same-release implementation exception: they statically embed Circuit code, but all embedded mutable state is hidden and local to that plugin DSO. It is not shared with the host's Circuit state. Partner and customer plugins must not statically link stateful TDSE Core libraries; shared state must cross the ABI explicitly. Formal ABI v1 uses caller-owned output, diagnostics, and error-message buffers and plugin-owned opaque netlist handles, so no allocation is freed across the DLL boundary.

Untrusted native plugins are not supported by the in-process ABI. Deployments that require crash or memory-safety isolation must use a separate process, container, or VM boundary. TDSE's out-of-process plugin host remains a roadmap option until the host binary and release gates are present.

Deployment Layout

/opt/tdse/
  lib/
    plugins/
      sim/
        libtdse_sim_cpu_dense.so
        libtdse_sim_cpu_sparse.so
        libtdse_sim_cuda.so
        plugin_manifest.json
        plugin_manifest.json.sig

Plugins, their manifest, and the detached signature sidecar must live together in the plugins/sim/ directory. Do not rearrange or flatten this layout.

Plugin Manifest

The manifest (plugin_manifest.json) maps logical plugin names to on-disk binaries and carries integrity metadata:

{
  "schema_version": 1,
  "plugins": [
    {
      "name": "sim_cpu_dense",
      "kind": "sim",
      "abi_major": 1,
      "abi_minor": 1,
      "sdk_version": "1.0.0-rc1",
      "path": "libtdse_sim_cpu_dense.so",
      "sha256": "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4",
      "runtime_dependencies": [],
      "external_needed": ["libc.so.6", "libtdse.so.1"]
    }
  ]
}
  • path is resolved relative to the manifest directory.
  • sha256 is mandatory. The loader opens the selected binary once, holds its stable file identity, hashes bytes through that handle, and rejects mismatches before native loading.
  • plugin_manifest.json.sig is the detached Ed25519 signature over the exact plugin_manifest.json bytes. It is required in strict mode together with a configured trusted public key. An unsigned manifest is permitted only in explicit non-strict development mode. Whenever a public key is configured, the sidecar is verified before the plugin file is opened or mapped.
  • The manifest must be valid UTF-8 JSON, contain no duplicate object keys at any depth, and fit within 1 MiB. Field order, whitespace, and line endings are signed bytes; changing any of them requires a new sidecar.
  • Every JSON number, including an unknown extension value, must be finite and within the representable binary64 range; nonzero underflow is rejected. Manifest binders retain Decimal semantic precision when rewriting owned fields, although any rewritten manifest must be signed again.
  • runtime_dependencies is the mandatory <libdir>-relative, SHA-256-bound private ELF closure; external_needed is the mandatory exact path-free SONAME boundary. Both arrays are present even when empty.

The authenticated load order is fail-closed:

  1. Read the manifest once with a 1 MiB limit and parse it.
  2. Require a trusted public key and valid signature in strict mode, or verify the signature whenever a key is configured in development mode.
  3. Validate the requested name, kind=sim, exact current manifest ABI 1.1, SDK release, and unique matching entry.
  4. Resolve the manifest-relative path and reject traversal, build-tree paths, non-regular files, and symlink/reparse components.
  5. Secure-open the plugin and every private dependency. Linux copies all held bytes to private memfds, revalidates their sources, applies write/grow/shrink/seal seals, hashes the sealed snapshots, and validates the native ET_DYN/SONAME/NEEDED graph before any constructor.
  6. Require a unique, reachable, acyclic private closure and an exact external_needed boundary. Unloaded system leaves are resolved only from canonical architecture system roots and are sealed before local preloading; loader environment search paths are ignored.
  7. Under a process-wide SONAME+digest registry, preload the complete closure leaf-first with immediate/local binding. Same-digest concurrent users share one mapping; different digests and untrusted preloaded SONAMEs fail closed.
  8. Load only the authenticated identity. Windows uses constrained LoadLibraryExW search flags; Linux loads /proc/self/fd/<sealed-fd> with immediate, local symbol binding and binds the entry-symbol mapping back to the sealed memfd device/inode.
  9. Revalidate the loaded/current file identity and plugin-reported name/kind/ABI/SDK/vtable before publishing a generation.

The public Circuit route owns one process-global simulation-plugin host. The first successful transient or AC request activates one plugin family (dense CPU, sparse CPU, or CUDA). Later requests for the same family share that generation. A request for a different family fails with an explicit host conflict; it never unloads the healthy generation, resets its metrics, or silently switches backends. Applications that need another family must use a separate process. Quarantine may tear down a fatally violating generation, but does not turn a conflicting request into an implicit switch.

The stable externally visible result of a family conflict is TDSE_STATUS_PLUGIN_NOT_AVAILABLE. Plugin health/metrics report TDSE_PLUGIN_LOAD_FAILURE_POLICY_REJECTED, and the detailed diagnostic begins with [policy_rejected] and identifies the active-family conflict. Treat this as a host policy decision, not as ordinary backend discovery failure. Quarantine removes a fatally violating generation; a later request for the same family may reauthenticate and load a fresh generation. It never retries the failed operation, and a request made while a different healthy family is active remains rejected.

Plugin constructors, destructors, and Windows DllMain callbacks are native loader callbacks, not TDSE initialization hooks. They must not call TDSE host APIs, recursively load/unload plugins, or start a thread and wait for that thread to call TDSE. Put fallible or host-dependent initialization in an explicit plugin ABI entry point after publication. TDSE rejects same-thread recursive loads during native initialization and teardown; other load transactions are process-serialized, but blocking a loader callback on TDSE work remains an unsupported plugin lifecycle violation.

This native-loader policy supports Windows held-handle loading and Linux hosts with memfd_create, file sealing, and mounted /proc/self/fd and /proc/self/maps. Other POSIX hosts fail closed before dlopen. Linux simulation plugins use only the approved relocatable $ORIGIN RUNPATH entries from <libdir>/plugins/sim to <libdir> and packaged third-party roots. Absolute build/install paths are forbidden, so a composed installation remains valid after moving without LD_LIBRARY_PATH. Build-tree plugin artifacts carry the same owned RUNPATH to avoid empty CMake padding entries. They are development inputs for TDSE host tools and tests, which load the matching TDSE host first; standalone loading of a build-tree plugin is not a supported deployment mode.

On Windows, the first simulation-plugin operation, including an unauthenticated development-mode operation, establishes a process-wide default DLL search of System32 before any plugin constructor runs. Failure rejects the operation. This changes subsequent bare-name LoadLibrary behavior for the whole host process: application, plugin, package, current-working-directory, PATH, and user-added directories are not searched. A host must load its own private runtime components before initializing TDSE plugins and must not later loosen or replace the process search policy. Exact-path authenticated plugin loads remain supported.

A configured malformed, invalid, or non-matching manifest is authoritative and never falls through to directory discovery, including in non-strict development mode. Only development runs with no configured manifest may use the explicitly unauthenticated discovery path. Non-strict development may also use an unsigned manifest for path and hash integrity checks, but that mode does not authenticate the manifest publisher and must not be described as strict authentication.

Environment Variables

VariablePurpose
TDSE_PLUGIN_MANIFESTPath to plugin_manifest.json. Required in production.
TDSE_PLUGIN_STRICT_MODESet to 1 for production. Requires manifest and disables fallback directory probing.
TDSE_PLUGIN_DIRDirectory containing plugin .so files. Optional; use only for controlled evaluation or non-manifest deployments.
TDSE_PLUGIN_PUBLIC_KEY_BASE64Canonical RFC 4648 Base64 for the 32-byte Ed25519 public key. Required in strict mode unless compiled into the host.

Manifest Signing

The manifest uses a detached Ed25519 signature. Signing never rewrites the manifest:

# Generate a key pair
python3 tools/release/tdse_sign_manifest.py --generate-key-pair

# Sign the exact manifest bytes; writes manifest.json.sig
python3 tools/release/tdse_sign_manifest.py --sign manifest.json --private-key key.priv

# Verify the exact bytes and detached sidecar
python3 tools/release/tdse_verify_manifest_signature.py \
  --manifest manifest.json \
  --signature manifest.json.sig \
  --public-key <canonical-base64-public-key>

The signature sidecar is exactly 88 ASCII Base64 bytes with no whitespace. The trust key is canonical Base64 that decodes to exactly 32 bytes; malformed padding, nonzero trailing bits, whitespace, high bytes, wrong lengths, and all-zero keys or signatures are rejected.

Strict mode requires a public key configured via TDSE_PLUGIN_PUBLIC_KEY_BASE64 or the build-time TDSE_PLUGIN_PUBLIC_KEY, plus a valid manifest signature. Missing keys, unsigned manifests, and invalid signatures fail closed before plugin entry or file processing. Non-strict unsigned manifests are an explicit development-only integrity mode.

See the Plugin Deployment Guide shipped with the evaluation package for the detailed signing workflow and key management expectations.

Production Configuration

For a packaged deployment, start with this configuration:

export TDSE_PLUGIN_MANIFEST=/opt/tdse/lib/plugins/sim/plugin_manifest.json
export TDSE_PLUGIN_STRICT_MODE=1
export TDSE_PLUGIN_PUBLIC_KEY_BASE64=<canonical-base64-public-key>

In strict mode:

  • The host loads plugins only from the manifest.
  • Fallback directory probing and relative directory scans are rejected.
  • A configured trusted public key and valid manifest signature are required.
  • Manifest ABI must exactly match the current pre-1.0 ABI 1.1.
  • Manifest integrity (sha256) and authenticity (signature) are enforced.

This is the recommended baseline whenever you are shipping TDSE outside a local evaluation sandbox or controlled validation environment.

Health Check

Each plugin exports a health check function. Use tdse_plugin_doctor to inspect all installed plugins:

TDSE_PLUGIN_MANIFEST=/opt/tdse/lib/plugins/sim/plugin_manifest.json \
  tdse_plugin_doctor

The doctor reports for each plugin:

  • ABI version and host compatibility
  • Health status (ok, cuda_partial, cuda_unavailable)
  • Manifest entry match and sha256 verification
  • Struct size and vtable presence

Treat these health values as operational readiness signals, not performance claims. For example, ok means the plugin loaded compatibly under the current policy; it does not by itself prove throughput, WCET, or target-machine qualification.

Expected health status values:

  • ok — plugin is fully operational
  • cuda_unavailable — no CUDA runtime or driver detected
  • cuda_partial — only some CUDA backends available
  • dense_unavailable — dense CPU solver backend not available
  • klu_unavailable — KLU sparse solver library not available
  • mkl_unavailable — MKL Pardiso sparse solver not available
  • sparse_unavailable — no sparse solver backends available

PluginSDK clients should prefer the typed, caller-owned result:

tdse_plugin_health_result_t health = tdse_plugin_health_result_init();
char status[128];
tdse_plugin_health_query_status_t query =
    tdse_plugin_health_check_v2(info, &health, status, sizeof(status));

query distinguishes unsupported, callback failure, success, and invalid arguments. On success, health.state is HEALTHY, DEGRADED, UNHEALTHY, or UNKNOWN; the result also reports required/written bytes, truncation, and an optional plugin-native detail code. A null buffer with zero capacity performs a size query. All storage remains host-owned, so no allocator or lifetime crosses the plugin boundary. tdse_plugin_health_check(...) remains the ABI-compatible legacy projection and still returns zero for both unsupported and failure.

Monitoring

Plugin execution metrics are available through the host API (snapshot_metrics()). Tracked counters include:

  • Load success and failure counts, with failure category
  • Per-call-kind simulation counts (transient, AC matrix, AC probe)
  • Total steps completed and frequency points processed
  • Error count and last error code
  • Backend switch count
  • Session duration

Load failures are classified into stable categories ([file_not_found], [abi_mismatch], [hash_mismatch], [manifest_invalid], [signature_invalid], [policy_rejected]) for machine parsing from logs.

Compatibility

The plugin ABI uses struct_size and abi_version negotiation for extensible results, plus struct_size for configuration additions:

  • New fields are appended to the end of config structs.
  • The host checks struct_size before accessing new fields.
  • Defaults for missing fields are safe (zero, null, off).
  • Callers initialize results with tdse_sim_result_init() and attach their own diagnostics and error-message buffers with tdse_sim_result_set_diagnostics_buffer() and tdse_sim_result_set_error_message_buffer().
  • Plugins write only their known result prefix and preserve caller buffer pointers, capacities, and unknown tail bytes.

A minor ABI bump is compatible only when these prefix rules are preserved and an executable older-minor fixture proves a real simulation call. No unpublished draft layout is treated as an older supported binary.

For formal v1 on x64, tdse_sim_result_t is 160 bytes and the known prefix is 96 bytes. Output buffers remain explicit caller-owned function arguments. output_values_required reports the requested shape, while output_values_written counts only committed values. Any failure reports zero written values and clears the output-complete flag. The host and shipped plugins stage transient output, so validation, cancellation, callback, solver, and result-contract failures leave the caller's output bytes unchanged. Diagnostics payload type is CIRCUIT, schema version 1, and may be populated on success or failure.

Simulation Plugin ABI 1.1 appends fixed-width error codes without changing the v1 layout. Timeout, I/O, invalid state, numeric failure, busy, dependency unavailable, out-of-range, and buffer-too-small map to their corresponding public TDSE statuses. Plugin BUSY maps to TDSE_STATUS_CONCURRENT_API_USE; dependency unavailability maps to TDSE_STATUS_PLUGIN_NOT_AVAILABLE.

A throwing host progress or cancellation callback returns TDSE_STATUS_HOST_CALLBACK_FAILED, leaves output uncommitted, and does not unload the plugin. Plugin error code 16 is reserved for that recorded host provenance; a plugin that returns it without a matching host callback exception is treated as an ABI contract violation. An ABI contract violation returns TDSE_STATUS_ABI_CONTRACT_VIOLATION and quarantines only the offending loaded generation. TDSE_SIM_ERR_INTERNAL is also generation-fatal; every other defined plugin error is nonfatal.

Transient scheme numbers are exact ABI values: TRAP=0, reserved GEAR=1, and BACKWARD_EULER=2. ABI v1 implements TRAP and backward Euler (BDF1). GEAR is reserved for future variable-order Gear/BDF and returns TDSE_SIM_ERR_UNSUPPORTED before solver entry; unknown integers return TDSE_SIM_ERR_INVALID_ARG. Plugins must not silently substitute TRAP or backward Euler.

External Compatibility Contract

For packaged deployments, the practical compatibility promises are:

  • plugin load compatibility follows the major/minor ABI rule described above
  • extensible plugin-facing config, info, and diagnostics structs grow through an explicit size/version prefix and bounded writes
  • caller-owned result side buffers never transfer allocation ownership across the shared-library boundary
  • manifest integrity checks and documented health / failure classifications are stable operational behavior, not optional debugging extras
  • install-tree plugin consumption is exercised through smoke and compatibility tests, not only ad hoc local loading

That means customers should treat manifest validation, ABI compatibility, documented health states, and documented failure classes as part of the supported contract surface. Use tdse_plugin_doctor as the first field diagnostic for those signals, while its human-readable presentation may evolve.

Troubleshooting

"No simulation engine plugin loaded"

Check TDSE_PLUGIN_MANIFEST or TDSE_PLUGIN_DIR. Run tdse_plugin_doctor to see detailed diagnostic output. Common causes:

  • ABI version mismatch: the plugin was compiled against a different SDK version. The error message includes the plugin and host ABI versions.
  • Manifest sha256 mismatch: the on-disk binary does not match the manifest. Rebuild or update the manifest.
  • Missing entry point: the .so file does not export tdse_plugin_get_info. Check with nm -D.

Plugin loads but simulation fails with UNSUPPORTED

The loaded plugin does not support the requested solver backend. The host automatically loads the correct plugin for a given backend; check that the manifest includes the required plugin entry.

CUDA plugin reports cuda_unavailable

The CUDA toolkit or compatible driver is not installed, or the CUDA runtime libraries are not on the library path. Verify with tdse_plugin_doctor.