forked from 1b-agent/opencadstudio-upstream
feat(plugin-api): entity-class display declarations on the manifest (API v10)
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.
This commit is contained in:
1 parent
859070403c
commit
60eb7b086e
9 files changed
+502
-13
No files matched your search
@@ -29,7 +29,7 @@ use crate::ipc::protocol::{
|
||||
};
|
||||
use crate::ipc::transport::{recv, send};
|
||||
use crate::ipc::v4::client::{RunnerFrame, V4Client};
|
||||
use crate::ribbon::owned::OwnedRibbonGroup;
|
||||
use crate::ribbon::owned::{OwnedEntityClasses, OwnedPluginManifest, OwnedRibbonGroup};
|
||||
|
||||
/// Entry point for the plugin runner child process.
|
||||
///
|
||||
@@ -59,7 +59,7 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box<dyn std::err
|
||||
if version >= 5 {
|
||||
invoke_on_load(&mut *plugin, &client, &interactive)?;
|
||||
}
|
||||
run_v4(&mut *plugin, &interactive, client)
|
||||
run_v4(&mut *plugin, &interactive, client, version)
|
||||
} else {
|
||||
let client = IpcClient::connect(socket_name)?;
|
||||
eprintln!("[runner] connected to host");
|
||||
@@ -106,6 +106,7 @@ fn run_v4(
|
||||
plugin: &mut dyn BuiltinPlugin,
|
||||
interactive: &InteractiveRegistry,
|
||||
client: V4Client,
|
||||
api_version: u32,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
// Drain any host notifications and dispatch them to the plugin's
|
||||
@@ -119,9 +120,14 @@ fn run_v4(
|
||||
|
||||
match client.recv_runner_frame_timeout(std::time::Duration::from_millis(50)) {
|
||||
Ok(RunnerFrame::Request { id, payload }) => {
|
||||
if let Some(resp) =
|
||||
handle_host_request_v4(&mut *plugin, interactive, &client, id, payload)
|
||||
{
|
||||
if let Some(resp) = handle_host_request_v4(
|
||||
&mut *plugin,
|
||||
interactive,
|
||||
&client,
|
||||
id,
|
||||
api_version,
|
||||
payload,
|
||||
) {
|
||||
client.send_response(id, resp)?;
|
||||
}
|
||||
}
|
||||
@@ -277,12 +283,37 @@ fn handle_host_request_v4(
|
||||
interactive: &InteractiveRegistry,
|
||||
client: &V4Client,
|
||||
id: u64,
|
||||
api_version: u32,
|
||||
req: HostRequest,
|
||||
) -> Option<HostResponse> {
|
||||
match req {
|
||||
HostRequest::GetManifest => {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.manifest())) {
|
||||
Ok(m) => Some(HostResponse::Manifest(m.into())),
|
||||
Ok(m) => {
|
||||
let mut manifest = OwnedPluginManifest::from(m);
|
||||
// API v10: fold the plugin's entity-class declarations into
|
||||
// the registered manifest. The call is guarded by the ABI
|
||||
// version the cdylib was actually built against (the
|
||||
// exported symbol, not the manifest's claim): an older
|
||||
// cdylib's vtable predates the slot, so touching it would
|
||||
// be undefined behavior. Its entities simply keep their
|
||||
// native type names.
|
||||
if api_version >= 10 {
|
||||
let classes =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
plugin.entity_classes()
|
||||
}));
|
||||
manifest.entity_classes = match classes {
|
||||
Ok(classes) => classes.map(OwnedEntityClasses::from),
|
||||
Err(_) => {
|
||||
return Some(HostResponse::Error(
|
||||
"plugin entity_classes() panicked".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(HostResponse::Manifest(manifest))
|
||||
}
|
||||
Err(_) => Some(HostResponse::Error(
|
||||
"plugin manifest() panicked".to_string(),
|
||||
)),
|
||||
@@ -568,7 +599,7 @@ mod tests {
|
||||
code: "1+1".to_string(),
|
||||
tab_index: 0,
|
||||
};
|
||||
let resp = handle_host_request_v4(&mut plugin, &interactive, &client, 99, req);
|
||||
let resp = handle_host_request_v4(&mut plugin, &interactive, &client, 99, 4, req);
|
||||
match resp {
|
||||
Some(HostResponse::Error(msg)) => {
|
||||
assert!(msg.contains("does not support code execution"));
|
||||
@@ -577,4 +608,124 @@ mod tests {
|
||||
}
|
||||
drop(host_stream);
|
||||
}
|
||||
|
||||
static V10_MANIFEST: PluginManifest = PluginManifest {
|
||||
id: "test.classed",
|
||||
name: "Test Classed",
|
||||
version: "0.1.0",
|
||||
description: "test",
|
||||
api_version: ApiVersion { major: 10 },
|
||||
ribbon_order: 0,
|
||||
xdata_apps: &["CVIFC"],
|
||||
command_prefixes: &[],
|
||||
};
|
||||
|
||||
static TEST_ENTITY_CLASSES: crate::manifest::EntityClasses = crate::manifest::EntityClasses {
|
||||
app_id: "CVIFC",
|
||||
class_prefix: "CVIFC-CLASS:",
|
||||
labels: &[
|
||||
("LINE", "Linear Element"),
|
||||
("CIRCULARARC", "Circular Arc Element"),
|
||||
],
|
||||
};
|
||||
|
||||
struct ClassedPlugin;
|
||||
impl BuiltinPlugin for ClassedPlugin {
|
||||
fn manifest(&self) -> &'static PluginManifest {
|
||||
&V10_MANIFEST
|
||||
}
|
||||
fn ribbon(&self) -> Box<dyn CadModule> {
|
||||
Box::new(EmptyModule)
|
||||
}
|
||||
fn dispatch(&self, _host: &mut dyn HostApi, _cmd: &str) -> bool {
|
||||
false
|
||||
}
|
||||
fn entity_classes(&self) -> Option<&'static crate::manifest::EntityClasses> {
|
||||
Some(&TEST_ENTITY_CLASSES)
|
||||
}
|
||||
}
|
||||
|
||||
fn manifest_response_of(
|
||||
plugin: &mut dyn BuiltinPlugin,
|
||||
api_version: u32,
|
||||
) -> Option<HostResponse> {
|
||||
let (host_stream, runner_stream) = connect_pair();
|
||||
let client = V4Client::from_stream_for_test(runner_stream);
|
||||
let interactive: InteractiveRegistry = Rc::new(RefCell::new(HashMap::new()));
|
||||
let resp = handle_host_request_v4(
|
||||
plugin,
|
||||
&interactive,
|
||||
&client,
|
||||
99,
|
||||
api_version,
|
||||
HostRequest::GetManifest,
|
||||
);
|
||||
drop(host_stream);
|
||||
resp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_manifest_folds_entity_classes_for_v10_plugin() {
|
||||
let mut plugin = ClassedPlugin;
|
||||
match manifest_response_of(&mut plugin, 10) {
|
||||
Some(HostResponse::Manifest(manifest)) => {
|
||||
let classes = manifest.entity_classes.expect("declaration folded in");
|
||||
assert_eq!(classes.app_id, "CVIFC");
|
||||
assert_eq!(classes.class_prefix, "CVIFC-CLASS:");
|
||||
assert_eq!(
|
||||
classes.labels.get("LINE").map(String::as_str),
|
||||
Some("Linear Element")
|
||||
);
|
||||
assert_eq!(
|
||||
classes.labels.get("CIRCULARARC").map(String::as_str),
|
||||
Some("Circular Arc Element")
|
||||
);
|
||||
}
|
||||
other => panic!("expected manifest response, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_manifest_leaves_entity_classes_default_below_v10() {
|
||||
// A cdylib whose exported ABI symbol predates v10 must not have the
|
||||
// trait slot called, even if the in-process plugin object happens to
|
||||
// implement it: the declaration stays `None` (native display types).
|
||||
let mut plugin = ClassedPlugin;
|
||||
match manifest_response_of(&mut plugin, 9) {
|
||||
Some(HostResponse::Manifest(manifest)) => {
|
||||
assert!(
|
||||
manifest.entity_classes.is_none(),
|
||||
"entity classes must not be folded in for pre-v10 plugins"
|
||||
);
|
||||
}
|
||||
other => panic!("expected manifest response, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_manifest_reports_entity_classes_panic() {
|
||||
struct PanickingClassesPlugin;
|
||||
impl BuiltinPlugin for PanickingClassesPlugin {
|
||||
fn manifest(&self) -> &'static PluginManifest {
|
||||
&V10_MANIFEST
|
||||
}
|
||||
fn ribbon(&self) -> Box<dyn CadModule> {
|
||||
Box::new(EmptyModule)
|
||||
}
|
||||
fn dispatch(&self, _host: &mut dyn HostApi, _cmd: &str) -> bool {
|
||||
false
|
||||
}
|
||||
fn entity_classes(&self) -> Option<&'static crate::manifest::EntityClasses> {
|
||||
panic!("boom")
|
||||
}
|
||||
}
|
||||
|
||||
let mut plugin = PanickingClassesPlugin;
|
||||
match manifest_response_of(&mut plugin, 10) {
|
||||
Some(HostResponse::Error(msg)) => {
|
||||
assert!(msg.contains("entity_classes() panicked"), "got: {msg}");
|
||||
}
|
||||
other => panic!("expected error response, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user