diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-09 19:10:27 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-09 19:10:27 +0900 |
| commit | 78c23c7105914b74ae91c71496265d3b03d7e285 (patch) | |
| tree | 0f7e4b384bdbc211bb1bba2c274fd416880098d7 /crates/shirabe/src | |
| parent | 6d257691a39fb8c4191f4564ec6406d080dbf6b5 (diff) | |
| download | php-shirabe-78c23c7105914b74ae91c71496265d3b03d7e285.tar.gz php-shirabe-78c23c7105914b74ae91c71496265d3b03d7e285.tar.zst php-shirabe-78c23c7105914b74ae91c71496265d3b03d7e285.zip | |
feat(plugin): let a Rust-implemented plugin cross into the worker
PluginManager::getPluginCapability hands the plugin itself to the
capability constructor. Only a PHP-implemented plugin had an entity the
child could receive, so a Rust-implemented one bailed out; it now crosses
as a handle to an R-table entity behind Shirabe\RustPluginStub, or its
Capable flavour, since `$plugin instanceof Capable` is what decides
whether Composer asks a plugin for capabilities at all.
This is what the two capability tests of PluginInstallerTest were waiting
on: the mocked Capable plugin is Rust-side, and one of them asserts the
identity of the plugin read back out of $capability->args.
Diffstat (limited to 'crates/shirabe/src')
| -rw-r--r-- | crates/shirabe/src/event_dispatcher/event_dispatcher.rs | 7 | ||||
| -rw-r--r-- | crates/shirabe/src/plugin/capability/capability.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe/src/plugin/php_plugin_proxy.rs | 80 | ||||
| -rw-r--r-- | crates/shirabe/src/plugin/plugin_manager.rs | 34 |
4 files changed, 107 insertions, 20 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 72db0543..ca6c2adc 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -1586,6 +1586,13 @@ try {{ Ok(()) } + /// For testing only: a test that never registers a plugin package still needs the Composer + /// PHP runtime in the worker before a class of its own can implement a Composer interface + /// there. + pub fn __ensure_composer_php_runtime() -> anyhow::Result<()> { + Self::ensure_composer_php_runtime() + } + fn composer_php_runtime_autoload() -> Option<String> { if let Some(dir) = Platform::get_env("SHIRABE_COMPOSER_PHP_DIR") { let path = std::path::Path::new(&dir) diff --git a/crates/shirabe/src/plugin/capability/capability.rs b/crates/shirabe/src/plugin/capability/capability.rs index aa70ffa0..bb634b9f 100644 --- a/crates/shirabe/src/plugin/capability/capability.rs +++ b/crates/shirabe/src/plugin/capability/capability.rs @@ -11,4 +11,10 @@ pub trait Capability { fn as_command_provider(&self) -> Option<&dyn CommandProvider> { None } + + /// For testing only: recovers the PHP-backed proxy so tests can read capability properties + /// the way PHPUnit asserts `$capability->args`. + fn __as_php_capability_proxy(&self) -> Option<&crate::plugin::PhpCapabilityProxy> { + None + } } diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 715a21dd..8e24dbfe 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -54,6 +54,7 @@ enum RustEntity { std::rc::Rc<std::cell::RefCell<dyn crate::event_dispatcher::EventDispatcherInterface>>, ), Operation(std::rc::Rc<AnyOperation>), + Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>), } /// The pointer identity backing R-table interning: the same shared instance must always cross @@ -74,6 +75,7 @@ fn entity_ptr_id(entity: &RustEntity) -> usize { std::rc::Rc::as_ptr(dispatcher) as *const () as usize } RustEntity::Operation(operation) => std::rc::Rc::as_ptr(operation) as *const () as usize, + RustEntity::Plugin(plugin) => std::rc::Rc::as_ptr(plugin) as *const () as usize, } } @@ -196,6 +198,22 @@ pub(crate) fn package_handle_value( rust_handle_value(rhandle, class) } +/// Registers a Rust-implemented plugin and returns its wire descriptor. A PHP-implemented +/// plugin never takes this route: its entity lives in the child's P table already. +pub fn plugin_handle_value( + plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, +) -> PluginValue { + // TODO(plugin): a Rust-implemented plugin that is also an EventSubscriberInterface crosses + // as one of these two classes, so `instanceof EventSubscriberInterface` is false in the + // child; Composer's own subscriber dispatch runs on the Rust side and never asks. + let class = match plugin.borrow().as_capable() { + Some(_) => "Shirabe\\RustCapablePluginStub", + None => "Shirabe\\RustPluginStub", + }; + let rhandle = register_entity(RustEntity::Plugin(plugin.clone())); + rust_handle_value(rhandle, class) +} + /// Registers a solver operation and returns its wire descriptor. pub(crate) fn operation_handle_value(operation: &std::rc::Rc<AnyOperation>) -> PluginValue { let class = operation_stub_class(operation); @@ -363,6 +381,7 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { Some(RustEntity::Operation(operation)) => { dispatch_operation_method(&operation, method_name, &args) } + Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name), None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), } } @@ -539,7 +558,8 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> { | RustEntity::RepositoryManager(_) | RustEntity::Repository(_) | RustEntity::EventDispatcher(_) - | RustEntity::Operation(_) => { + | RustEntity::Operation(_) + | RustEntity::Plugin(_) => { return Err(runtime_throw( "cloning this Rust-side entity over RPC is not supported".to_string(), )); @@ -592,6 +612,33 @@ fn dispatch_property_access( ))) } +fn dispatch_plugin_method( + plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, + method_name: &str, +) -> Result<PluginValue, PhpThrow> { + match method_name { + "getCapabilities" => { + let plugin = plugin.borrow(); + let capable = plugin.as_capable().ok_or_else(|| { + runtime_throw(format!( + "plugin {} does not implement Capable", + plugin.get_class_name() + )) + })?; + let capabilities = capable + .get_capabilities() + .map_err(|error| runtime_throw(format!("getCapabilities failed: {error:#}")))?; + Ok(PluginValue::from_php_mixed(&PhpMixed::Array(capabilities))) + } + // TODO(plugin): the lifecycle methods would have to turn the `$composer`/`$io` stubs the + // child passes back into the Rust handles they stand for; nothing calls them, because + // Composer activates a Rust-implemented plugin on the Rust side. + other => Err(runtime_throw(format!( + "the plugin method `{other}` is not available over RPC yet" + ))), + } +} + fn dispatch_composer_method( composer: &ComposerHandle, method_name: &str, @@ -2582,9 +2629,38 @@ impl PhpCapabilityProxy { pub(crate) fn new(handle: PhpObjHandle) -> Self { Self { handle } } + + /// For testing only: the entity descriptor, whose class and interface list answer the + /// `assertInstanceOf` checks PHPUnit makes on a capability. + pub fn __handle(&self) -> &PhpObjHandle { + &self.handle + } + + /// For testing only: reads a public property of the capability entity in the child. Unlike + /// `PhpPluginProxy::__get_property` the value keeps its wire form, so a test can assert the + /// object identity behind a handle instead of only the plain data around it. + pub fn __get_property(&self, name: &str) -> anyhow::Result<PluginValue> { + let outcome = shirabe_php_rpc::call_function( + "__shirabe_get_property", + vec![ + PluginValue::PhpHandle(self.handle.clone()), + PluginValue::string(name), + ], + )?; + match outcome { + Ok(value) => Ok(value), + Err(throw) => { + Err(shirabe_php_shim::RuntimeException::with_code(throw.message, throw.code).into()) + } + } + } } -impl Capability for PhpCapabilityProxy {} +impl Capability for PhpCapabilityProxy { + fn __as_php_capability_proxy(&self) -> Option<&PhpCapabilityProxy> { + Some(self) + } +} impl Drop for PhpCapabilityProxy { fn drop(&mut self) { diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs index 0746a9b4..0667c6a9 100644 --- a/crates/shirabe/src/plugin/plugin_manager.rs +++ b/crates/shirabe/src/plugin/plugin_manager.rs @@ -16,6 +16,7 @@ use crate::plugin::PluginBlockedException; use crate::plugin::capability::Capability; use crate::plugin::php_plugin_proxy::{ PhpCapabilityProxy, PhpCommandProviderProxy, PhpPluginProxy, PluginRpcDispatcher, php_is_a, + plugin_handle_value, }; use crate::plugin::plugin_interface::{self, PluginInterface}; use crate::repository::InstalledRepository; @@ -1031,15 +1032,16 @@ impl PluginManager { pub fn get_plugin_capability( &self, - plugin: &dyn PluginInterface, + plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, capability_class_name: &str, ctor_args: IndexMap<String, PluginValue>, ) -> anyhow::Result<Option<Box<dyn Capability>>> { - let capability_class = - match self.get_capability_implementation_class_name(plugin, capability_class_name)? { - Some(c) => c, - None => return Ok(None), - }; + let capability_class = match self + .get_capability_implementation_class_name(&*plugin.borrow(), capability_class_name)? + { + Some(c) => c, + None => return Ok(None), + }; // PHP: if (!class_exists($capabilityClass)) let exists = unwrap_php_result(call_function_with_dispatcher( @@ -1051,23 +1053,21 @@ impl PluginManager { return Err(RuntimeException::new(format!( "Cannot instantiate Capability, as class {} from plugin {} does not exist.", capability_class, - plugin.get_class_name() + plugin.borrow().get_class_name() )) .into()); } // PHP: $ctorArgs['plugin'] = $plugin; the capability constructor receives the plugin - // instance itself, so a plugin with no PHP-side entity cannot be represented. - let plugin_value = match plugin.__as_php_plugin_proxy() { + // instance itself. A PHP-implemented plugin already has an entity in the child's P + // table; a Rust-implemented one crosses as a handle to its R-table entity. + let plugin_value = match plugin.borrow().__as_php_plugin_proxy() { Some(proxy) => PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle { phandle: proxy.phandle, class: proxy.class.clone(), implements: proxy.implements.clone(), }), - None => anyhow::bail!( - "cannot instantiate capability {capability_class}: plugin {} has no PHP-side entity to pass as $ctorArgs['plugin']", - plugin.get_class_name() - ), + None => plugin_handle_value(plugin), }; let mut ctor_args = ctor_args; ctor_args.insert("plugin".to_string(), plugin_value); @@ -1121,11 +1121,9 @@ impl PluginManager { ) -> anyhow::Result<Vec<Box<dyn Capability>>> { let mut capabilities: Vec<Box<dyn Capability>> = vec![]; for plugin in self.get_plugins() { - if let Some(capability) = self.get_plugin_capability( - &*plugin.borrow(), - capability_class_name, - ctor_args.clone(), - )? { + if let Some(capability) = + self.get_plugin_capability(plugin, capability_class_name, ctor_args.clone())? + { capabilities.push(capability); } } |
