forked from 1b-agent/opencadstudio-upstream
Plugins can now declare the entity classes they identify via XDATA, and the
host resolves them to display-only type labels while the declaring plugin is
loaded.
- Append `BuiltinPlugin::entity_classes()` (a guarded trailing vtable slot,
the same additive pattern as `on_load` in v5) returning a &'static
`EntityClasses { app_id, class_prefix, labels }`. The static
`PluginManifest` struct is deliberately untouched: older cdylibs share it
by pointer and its layout is frozen ABI since v1, so adding a field would
read past their allocation.
- `OwnedEntityClasses` travels on the registered manifest as
`OwnedPluginManifest.entity_classes` with `#[serde(default)]`, so manifests
serialized before the field keep decoding (older runners tolerate it
both ways).
- The runner folds the declaration into the GetManifest registration only
when the loaded cdylib's exported `ocs_plugin_api_version` symbol reports
>= 10 — an older binary's vtable predates the slot, so it is never called;
its entities simply keep their native type names.
- `PluginManager::entity_class_declarations` exposes the live set, filtered
to alive, non-disabled plugins in load order. The registered manifests are
the only source and nothing is cached: not installed, disabled,
cap-excluded (`OCS_PLUGIN_MAX_API_VERSION`), failed at load, removed, or a
dead process all contribute nothing.
- Embedded version info `api_version` follows the bump to 10;
`API_VERSION_MIN_SUPPORTED` stays 3.
Tests: runner fold (v10 folds, pre-v10 skipped, `entity_classes()` panic is
contained and reported), manager filtering (disabled / dead / undeclaring
skipped; declarations disappear once the process dies — verified with a real
killed child), bincode registration round-trip, and older-manifest serde
default.
205 lines
9.0 KiB
Rust
205 lines
9.0 KiB
Rust
//! Plugin identity, API version constants, and compatibility rules.
|
|
//!
|
|
//! This module defines the host ABI major version (`API_VERSION`), the oldest
|
|
//! major the host still loads (`API_VERSION_MIN_SUPPORTED`), and the runtime
|
|
//! `OCS_PLUGIN_MAX_API_VERSION` gate that lets operators disable newer API
|
|
//! majors without rebuilding.
|
|
//!
|
|
//! Compatibility rule: a plugin compiled against major `N` runs on any host
|
|
//! whose major is `>= N`, because new vtable entries and enum variants are
|
|
//! appended at the end. `host_accepts_plugin_version` enforces the supported
|
|
//! range after applying the runtime cap.
|
|
//!
|
|
//! See `ARCHITECTURE.md` in the crate root for the full versioning policy.
|
|
|
|
/// Host plugin API version. Bump when the host runtime surface breaks
|
|
/// compatibility. v2 added `HostApi::start_interactive`. v3 changes
|
|
/// `document()` / `document_mut()` to local cached copies for out-of-process
|
|
/// plugins and appends `document_reader` / `document_view` at the end of the
|
|
/// vtable so API v2 plugins keep working. v4 adds full-duplex notifications on
|
|
/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. v5
|
|
/// adds the `BuiltinPlugin::on_load` lifecycle callback and
|
|
/// `HostApi::document_path`. v6 appends generic host system-variable access
|
|
/// (`HostApi::system_variable` / `set_system_variable`). v7 appends atomic,
|
|
/// undoable replacement of a set of existing entities. v8 appends name-addressed
|
|
/// XRecord object CRUD under named-object-dictionary paths
|
|
/// (`HostApi::create_xrecord` / `read_xrecord` / `update_xrecord` /
|
|
/// `delete_xrecord` / `list_xrecords`). v9 appends the reference-only entity
|
|
/// designation (`HostApi::set_reference_only`): display-only geometry a plugin
|
|
/// still owns and refreshes while the built-in edit paths treat it like a
|
|
/// locked layer. v10 appends `BuiltinPlugin::entity_classes`: plugin-declared
|
|
/// XDATA class-identity display labels, folded into the registered manifest.
|
|
/// Display-only; the host consults the declaration only while the declaring
|
|
/// plugin is loaded, and entities without one keep their native type names.
|
|
pub const API_VERSION: u32 = 10;
|
|
|
|
/// Oldest plugin API major the current host still loads. This keeps previously
|
|
/// compiled cdylibs usable as long as their vtable layout is a prefix of the
|
|
/// current `HostApi` trait. V2 is no longer loaded: its ribbon wire types
|
|
/// predate the current ones, so a V2 plugin passed the gate and then failed
|
|
/// silently while registering its ribbon (#1085, #987).
|
|
pub const API_VERSION_MIN_SUPPORTED: u32 = 3;
|
|
|
|
/// Environment variable that caps the API major accepted by the host at
|
|
/// runtime. Set to `5` to disable V6 plugins, `4` to disable V5, or `3` for
|
|
/// V3-only mode.
|
|
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct ApiVersion {
|
|
pub major: u32,
|
|
}
|
|
|
|
impl ApiVersion {
|
|
pub const CURRENT: Self = Self { major: API_VERSION };
|
|
|
|
/// True when this plugin version can run on `host`. A plugin is compatible
|
|
/// with any host whose API major is the same or newer (new host methods are
|
|
/// appended at the end of the vtable, so old plugins ignore them).
|
|
pub fn is_compatible_with(&self, host: ApiVersion) -> bool {
|
|
self.major <= host.major
|
|
}
|
|
}
|
|
|
|
/// The effective maximum API major this host accepts, honoring
|
|
/// [`MAX_API_VERSION_ENV`]. This lets operators disable a new API major at
|
|
/// runtime without rebuilding.
|
|
pub fn effective_max_api_version() -> u32 {
|
|
std::env::var(MAX_API_VERSION_ENV)
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.map(|max: u32| max.clamp(API_VERSION_MIN_SUPPORTED, API_VERSION))
|
|
.unwrap_or(API_VERSION)
|
|
}
|
|
|
|
/// True when a plugin built against `plugin_major` can be loaded by this host.
|
|
/// The host supports majors from `API_VERSION_MIN_SUPPORTED` up to
|
|
/// [`effective_max_api_version`], which can be lowered at runtime via
|
|
/// `OCS_PLUGIN_MAX_API_VERSION`.
|
|
pub fn host_accepts_plugin_version(plugin_major: u32) -> bool {
|
|
plugin_major >= API_VERSION_MIN_SUPPORTED && plugin_major <= effective_max_api_version()
|
|
}
|
|
|
|
/// A plugin's declaration of the entity classes it identifies via XDATA,
|
|
/// surfaced by the host as display-only type labels (the Properties panel
|
|
/// title and the automation `display_type` field).
|
|
///
|
|
/// The split is deliberate: the *identity* is a stable class KEY stamped into
|
|
/// the entity's XDATA (one string value per entity, e.g. `"MYAPP-CLASS:LINE"`)
|
|
/// and round-trips with the drawing; the *display labels* live in this
|
|
/// declaration, so they are plugin-localizable and fixable without rewriting
|
|
/// drawings. The declaration is consulted only while the declaring plugin is
|
|
/// loaded — a plugin that is not installed, disabled, failed to load, or whose
|
|
/// process has died contributes nothing, and its entities display as their
|
|
/// bare native type.
|
|
///
|
|
/// Declared through [`crate::host::BuiltinPlugin::entity_classes`] (API v10);
|
|
/// see `docs/plugin-architecture.md` for the full convention.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct EntityClasses {
|
|
/// XDATA application name whose records carry the class-identity keys
|
|
/// (e.g. `"CVIFC"`). Should be one of the manifest's `xdata_apps` so the
|
|
/// host knows the plugin writes under this id.
|
|
pub app_id: &'static str,
|
|
/// Prefix that marks a string XDATA value as a class-identity key
|
|
/// (e.g. `"CVIFC-CLASS:"`). The remainder of the value is the class key.
|
|
pub class_prefix: &'static str,
|
|
/// Class key → display label (e.g. `"LINE"` → `"Linear Element"`), in
|
|
/// declaration order. Display text never round-trips the drawing.
|
|
pub labels: &'static [(&'static str, &'static str)],
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::test_lock::ENV_LOCK;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn current_matches_const() {
|
|
assert_eq!(ApiVersion::CURRENT.major, API_VERSION);
|
|
}
|
|
|
|
#[test]
|
|
fn same_major_is_compatible() {
|
|
assert!(ApiVersion::CURRENT.is_compatible_with(ApiVersion::CURRENT));
|
|
assert!(!ApiVersion::CURRENT.is_compatible_with(ApiVersion {
|
|
major: API_VERSION - 1,
|
|
}));
|
|
// Forward compatibility: a plugin compiled today runs on a future host
|
|
// that only appends new vtable entries.
|
|
assert!(ApiVersion::CURRENT.is_compatible_with(ApiVersion {
|
|
major: API_VERSION + 1,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn api_v2_plugin_runs_on_api_v3_host() {
|
|
assert!(ApiVersion { major: 2 }.is_compatible_with(ApiVersion { major: 3 }));
|
|
}
|
|
|
|
#[test]
|
|
fn effective_max_defaults_to_api_version() {
|
|
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
std::env::remove_var(MAX_API_VERSION_ENV);
|
|
assert_eq!(effective_max_api_version(), API_VERSION);
|
|
}
|
|
|
|
#[test]
|
|
fn effective_max_can_disable_v4() {
|
|
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
std::env::set_var(MAX_API_VERSION_ENV, "3");
|
|
assert_eq!(effective_max_api_version(), 3);
|
|
assert!(!host_accepts_plugin_version(4));
|
|
assert!(host_accepts_plugin_version(3));
|
|
assert!(!host_accepts_plugin_version(2));
|
|
std::env::remove_var(MAX_API_VERSION_ENV);
|
|
}
|
|
|
|
#[test]
|
|
fn effective_max_below_minimum_is_v3_only_mode() {
|
|
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
std::env::set_var(MAX_API_VERSION_ENV, "2");
|
|
assert_eq!(effective_max_api_version(), 3);
|
|
assert!(!host_accepts_plugin_version(4));
|
|
assert!(host_accepts_plugin_version(3));
|
|
assert!(!host_accepts_plugin_version(2));
|
|
std::env::remove_var(MAX_API_VERSION_ENV);
|
|
}
|
|
|
|
#[test]
|
|
fn effective_max_clamps_to_supported_range() {
|
|
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
std::env::set_var(MAX_API_VERSION_ENV, "99");
|
|
assert_eq!(effective_max_api_version(), API_VERSION);
|
|
std::env::set_var(MAX_API_VERSION_ENV, "1");
|
|
assert_eq!(effective_max_api_version(), API_VERSION_MIN_SUPPORTED);
|
|
std::env::remove_var(MAX_API_VERSION_ENV);
|
|
}
|
|
|
|
#[test]
|
|
fn host_accepts_supported_range() {
|
|
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
std::env::remove_var(MAX_API_VERSION_ENV);
|
|
assert!(host_accepts_plugin_version(API_VERSION_MIN_SUPPORTED));
|
|
assert!(host_accepts_plugin_version(API_VERSION));
|
|
assert!(!host_accepts_plugin_version(API_VERSION + 1));
|
|
assert!(!host_accepts_plugin_version(API_VERSION_MIN_SUPPORTED - 1));
|
|
}
|
|
}
|
|
|
|
/// Static metadata every plugin supplies at registration time.
|
|
/// Keep fields in sync with `plugin.toml` beside the package.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct PluginManifest {
|
|
pub id: &'static str,
|
|
pub name: &'static str,
|
|
pub version: &'static str,
|
|
pub description: &'static str,
|
|
pub api_version: ApiVersion,
|
|
/// Sort key for add-on ribbon tabs (lower = further left among plugins).
|
|
pub ribbon_order: i32,
|
|
pub xdata_apps: &'static [&'static str],
|
|
pub command_prefixes: &'static [&'static str],
|
|
}
|