aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-09 19:10:27 +0900
committernsfisis <nsfisis@gmail.com>2026-08-09 19:10:27 +0900
commit78c23c7105914b74ae91c71496265d3b03d7e285 (patch)
tree0f7e4b384bdbc211bb1bba2c274fd416880098d7 /crates/shirabe/src/plugin
parent6d257691a39fb8c4191f4564ec6406d080dbf6b5 (diff)
downloadphp-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/plugin')
-rw-r--r--crates/shirabe/src/plugin/capability/capability.rs6
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs80
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs34
3 files changed, 100 insertions, 20 deletions
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);
}
}