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:
| Plugin | Binary name | Solver family |
|---|---|---|
| CPU Dense | libtdse_sim_cpu_dense.so | LAPACK, LU |
| CPU Sparse | libtdse_sim_cpu_sparse.so | KLU, MKL Pardiso |
| CUDA | libtdse_sim_cuda.so | CUDA 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:
| Header | Purpose |
|---|---|
tdse/plugin/simulation.h | Formal Simulation Plugin C ABI |
tdse/plugin/manifest.hpp | Authenticated manifest parsing and validation |
tdse/plugin/metrics.h | Plugin 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"]
}
]
}
pathis resolved relative to the manifest directory.sha256is 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.sigis the detached Ed25519 signature over the exactplugin_manifest.jsonbytes. 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_dependenciesis the mandatory<libdir>-relative, SHA-256-bound private ELF closure;external_neededis the mandatory exact path-free SONAME boundary. Both arrays are present even when empty.
The authenticated load order is fail-closed:
- Read the manifest once with a 1 MiB limit and parse it.
- Require a trusted public key and valid signature in strict mode, or verify the signature whenever a key is configured in development mode.
- Validate the requested name,
kind=sim, exact current manifest ABI 1.1, SDK release, and unique matching entry. - Resolve the manifest-relative path and reject traversal, build-tree paths, non-regular files, and symlink/reparse components.
- 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. - Require a unique, reachable, acyclic private closure and an exact
external_neededboundary. Unloaded system leaves are resolved only from canonical architecture system roots and are sealed before local preloading; loader environment search paths are ignored. - 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.
- Load only the authenticated identity. Windows uses constrained
LoadLibraryExWsearch 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. - 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
| Variable | Purpose |
|---|---|
TDSE_PLUGIN_MANIFEST | Path to plugin_manifest.json. Required in production. |
TDSE_PLUGIN_STRICT_MODE | Set to 1 for production. Requires manifest and disables fallback directory probing. |
TDSE_PLUGIN_DIR | Directory containing plugin .so files. Optional; use only for controlled evaluation or non-manifest deployments. |
TDSE_PLUGIN_PUBLIC_KEY_BASE64 | Canonical 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 operationalcuda_unavailable— no CUDA runtime or driver detectedcuda_partial— only some CUDA backends availabledense_unavailable— dense CPU solver backend not availableklu_unavailable— KLU sparse solver library not availablemkl_unavailable— MKL Pardiso sparse solver not availablesparse_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_sizebefore 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 withtdse_sim_result_set_diagnostics_buffer()andtdse_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
.sofile does not exporttdse_plugin_get_info. Check withnm -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.
