aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-04 05:09:06 +0900
committernsfisis <nsfisis@gmail.com>2026-08-04 05:43:34 +0900
commitb1ec6db345f1b2fe67ccf39fd0d2b77d85cb18c5 (patch)
tree47df5e2da9485836c5d176890e48627b145ec854 /crates
parent80631930a6ca7b64d96a89de61c8b63f302161a8 (diff)
downloadphp-shirabe-b1ec6db345f1b2fe67ccf39fd0d2b77d85cb18c5.tar.gz
php-shirabe-b1ec6db345f1b2fe67ccf39fd0d2b77d85cb18c5.tar.zst
php-shirabe-b1ec6db345f1b2fe67ccf39fd0d2b77d85cb18c5.zip
feat(plugin): expose the composer object graph to plugins over RPC
Widen the R table beyond Composer/IO with InstallationManager, RepositoryManager, repository and package entities, interned by pointer identity through a shared register_entity. Script events answer getComposer/getIO, Composer hands out its graph getters, repositories list their packages as per-variant stubs, package getters cover the extension-installer surface (getRequires only while empty: the Link snapshot encoding does not exist yet), and getInstallPath resolves its package argument back through the R table. Everything else stays an explicit error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs33
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs315
2 files changed, 316 insertions, 32 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index 80131e25..0bfdd7c7 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -1727,8 +1727,37 @@ pub(crate) fn dispatch_event_method(
"isDevMode is only available on script events".to_string(),
)),
},
- // TODO(plugin): getComposer/getIO/stopPropagation and the rest need full proxying
- // of the object graph an event exposes, which does not exist yet.
+ "getComposer" => match event.as_any().downcast_ref::<ScriptEvent>() {
+ Some(script_event) => {
+ let composer = script_event.get_composer().upgrade().ok_or_else(|| {
+ runtime_throw("the Composer instance of this event is gone".to_string())
+ })?;
+ let rhandle = crate::plugin::php_plugin_proxy::register_composer_entity(&composer);
+ Ok(crate::plugin::php_plugin_proxy::rust_handle_value(
+ rhandle,
+ "Composer\\Composer",
+ ))
+ }
+ None => Err(runtime_throw(
+ "getComposer is only available on script events".to_string(),
+ )),
+ },
+ "getIO" => match event.as_any().downcast_ref::<ScriptEvent>() {
+ Some(script_event) => {
+ let io = script_event.get_io();
+ let class = crate::plugin::php_plugin_proxy::io_stub_class(&io)
+ .map_err(|error| runtime_throw(error.to_string()))?;
+ let rhandle = crate::plugin::php_plugin_proxy::register_io_entity(&io);
+ Ok(crate::plugin::php_plugin_proxy::rust_handle_value(
+ rhandle, class,
+ ))
+ }
+ None => Err(runtime_throw(
+ "getIO is only available on script events".to_string(),
+ )),
+ },
+ // TODO(plugin): stopPropagation and the rest need full proxying of the object graph
+ // an event exposes, which does not exist yet.
other => Err(runtime_throw(format!(
"the Event method `{other}` is not available over RPC yet"
))),
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 143af8bb..70061542 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -9,19 +9,46 @@ use crate::autoload::ClassLoader;
use crate::composer::ComposerHandle;
use crate::event_dispatcher::event_dispatcher::dispatch_event_method;
use crate::event_dispatcher::{EventInterface, EventSubscriberInterface, SubscribedEventEntry};
+use crate::installer::InstallationManagerInterface;
use crate::io::IOInterface;
+use crate::package::handle::AnyPackage;
+use crate::package::{DisplayMode, PackageInterfaceHandle};
use crate::plugin::plugin_interface::PluginInterface;
+use crate::repository::{
+ InstalledArrayRepository, InstalledFilesystemRepository, RepositoryInterfaceHandle,
+ RepositoryManagerInterface,
+};
use indexmap::IndexMap;
use shirabe_php_rpc::{
PhpObjHandle, PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method,
release_php_handle,
};
+use shirabe_php_shim::PhpMixed;
/// A Rust-side entity a PHP proxy stub points back to.
#[derive(Debug, Clone)]
enum RustEntity {
Composer(ComposerHandle),
Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>),
+ InstallationManager(std::rc::Rc<std::cell::RefCell<dyn InstallationManagerInterface>>),
+ RepositoryManager(std::rc::Rc<std::cell::RefCell<dyn RepositoryManagerInterface>>),
+ Repository(RepositoryInterfaceHandle),
+ Package(std::rc::Rc<std::cell::RefCell<AnyPackage>>),
+}
+
+/// The pointer identity backing R-table interning: the same shared instance must always cross
+/// the boundary as the same handle (`===` in the child).
+fn entity_ptr_id(entity: &RustEntity) -> usize {
+ match entity {
+ RustEntity::Composer(composer) => {
+ std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize
+ }
+ RustEntity::Io(io) => std::rc::Rc::as_ptr(io) as *const () as usize,
+ RustEntity::InstallationManager(im) => std::rc::Rc::as_ptr(im) as *const () as usize,
+ RustEntity::RepositoryManager(rm) => std::rc::Rc::as_ptr(rm) as *const () as usize,
+ RustEntity::Repository(repository) => repository.ptr_id(),
+ RustEntity::Package(package) => std::rc::Rc::as_ptr(package) as *const () as usize,
+ }
}
thread_local! {
@@ -54,45 +81,85 @@ fn ensure_release_hook_installed() {
});
}
-/// Registers the Composer instance in the R table, interned by shared-pointer identity so the
-/// same instance always crosses the boundary as the same handle (`===` in the child).
-pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 {
+/// Registers an entity in the R table, interned by shared-pointer identity within its variant.
+fn register_entity(entity: RustEntity) -> u64 {
ensure_release_hook_installed();
R_TABLE.with(|table| {
let mut table = table.borrow_mut();
- let ptr = std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize;
- for (rhandle, entity) in table.iter() {
- if let RustEntity::Composer(existing) = entity
- && std::rc::Rc::as_ptr(existing.as_rc()) as *const () as usize == ptr
- {
+ let ptr = entity_ptr_id(&entity);
+ let discriminant = std::mem::discriminant(&entity);
+ for (rhandle, existing) in table.iter() {
+ if std::mem::discriminant(existing) == discriminant && entity_ptr_id(existing) == ptr {
return *rhandle;
}
}
let rhandle = shirabe_php_rpc::alloc_rhandle();
- table.insert(rhandle, RustEntity::Composer(composer.clone()));
+ table.insert(rhandle, entity);
rhandle
})
}
-/// Registers an IO instance in the R table, interned like [`register_composer_entity`].
+/// Registers the Composer instance in the R table.
+pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 {
+ register_entity(RustEntity::Composer(composer.clone()))
+}
+
+/// Registers an IO instance in the R table.
pub(crate) fn register_io_entity(io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) -> u64 {
- ensure_release_hook_installed();
- R_TABLE.with(|table| {
- let mut table = table.borrow_mut();
- let ptr = std::rc::Rc::as_ptr(io) as *const () as usize;
- for (rhandle, entity) in table.iter() {
- if let RustEntity::Io(existing) = entity
- && std::rc::Rc::as_ptr(existing) as *const () as usize == ptr
- {
- return *rhandle;
- }
- }
- let rhandle = shirabe_php_rpc::alloc_rhandle();
- table.insert(rhandle, RustEntity::Io(io.clone()));
- rhandle
+ register_entity(RustEntity::Io(io.clone()))
+}
+
+/// A `RustHandle` wire descriptor for a freshly registered (or re-interned) entity.
+pub(crate) fn rust_handle_value(rhandle: u64, class: &str) -> PluginValue {
+ PluginValue::RustHandle(RustObjHandle {
+ rhandle,
+ class: class.to_string(),
+ epoch: 0,
+ snapshot: None,
})
}
+/// The proxy stub class matching a package's concrete variant.
+fn package_stub_class(
+ package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>,
+) -> Result<&'static str, PhpThrow> {
+ match &*package.borrow() {
+ AnyPackage::Package(_) => Ok("Composer\\Package\\Package"),
+ AnyPackage::CompletePackage(_) => Ok("Composer\\Package\\CompletePackage"),
+ AnyPackage::RootPackage(_) => Ok("Composer\\Package\\RootPackage"),
+ // TODO(plugin): alias packages need proxy stubs of their own before they can cross.
+ AnyPackage::AliasPackage(_)
+ | AnyPackage::CompleteAliasPackage(_)
+ | AnyPackage::RootAliasPackage(_) => Err(runtime_throw(
+ "alias packages are not available over RPC yet".to_string(),
+ )),
+ }
+}
+
+/// The proxy stub class matching a repository's concrete type.
+fn repository_stub_class(repository: &RepositoryInterfaceHandle) -> Result<&'static str, PhpThrow> {
+ if repository.is::<InstalledFilesystemRepository>() {
+ Ok("Composer\\Repository\\InstalledFilesystemRepository")
+ } else if repository.is::<InstalledArrayRepository>() {
+ Ok("Composer\\Repository\\InstalledArrayRepository")
+ } else {
+ // TODO(plugin): the remaining repository classes get stubs on demand, driven by
+ // explicit errors from real plugins.
+ Err(runtime_throw(
+ "no proxy stub class is available for this repository over RPC yet".to_string(),
+ ))
+ }
+}
+
+/// Registers a package and returns its wire descriptor.
+pub(crate) fn package_handle_value(
+ package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>,
+) -> Result<PluginValue, PhpThrow> {
+ let class = package_stub_class(package)?;
+ let rhandle = register_entity(RustEntity::Package(package.clone()));
+ Ok(rust_handle_value(rhandle, class))
+}
+
/// The PHP class name (= proxy stub class) of a Rust IO instance, for the `__class` field of
/// its handle descriptor.
pub(crate) fn io_stub_class(
@@ -192,18 +259,206 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned());
match entity {
Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args),
- Some(RustEntity::Composer(_)) => {
- // TODO(plugin): the Composer object graph (getConfig, getRepositoryManager,
- // ...) becomes reachable over RPC later.
- Err(runtime_throw(format!(
- "the Composer method `{method_name}` is not available over RPC yet"
- )))
+ Some(RustEntity::Composer(composer)) => {
+ dispatch_composer_method(&composer, method_name)
+ }
+ Some(RustEntity::InstallationManager(im)) => {
+ dispatch_installation_manager_method(&im, method_name, &args)
+ }
+ Some(RustEntity::RepositoryManager(rm)) => {
+ dispatch_repository_manager_method(&rm, method_name)
+ }
+ Some(RustEntity::Repository(repository)) => {
+ dispatch_repository_method(&repository, method_name)
+ }
+ Some(RustEntity::Package(package)) => {
+ dispatch_package_method(&package, method_name, &args)
}
None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
}
}
}
+fn dispatch_composer_method(
+ composer: &ComposerHandle,
+ method_name: &str,
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getRepositoryManager" => {
+ let rm = composer.borrow().get_repository_manager();
+ let rhandle = register_entity(RustEntity::RepositoryManager(rm));
+ Ok(rust_handle_value(
+ rhandle,
+ "Composer\\Repository\\RepositoryManager",
+ ))
+ }
+ "getInstallationManager" => {
+ let im = composer.borrow().get_installation_manager();
+ let rhandle = register_entity(RustEntity::InstallationManager(im));
+ Ok(rust_handle_value(
+ rhandle,
+ "Composer\\Installer\\InstallationManager",
+ ))
+ }
+ "getPackage" => {
+ let package = composer.borrow().get_package().as_rc().clone();
+ package_handle_value(&package)
+ }
+ // TODO(plugin): the remaining Composer object graph (getConfig, getLocker, ...)
+ // becomes reachable over RPC on demand, driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the Composer method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+fn dispatch_repository_manager_method(
+ rm: &std::rc::Rc<std::cell::RefCell<dyn RepositoryManagerInterface>>,
+ method_name: &str,
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getLocalRepository" => {
+ let local = rm.borrow().get_local_repository();
+ let class = repository_stub_class(&local)?;
+ let rhandle = register_entity(RustEntity::Repository(local));
+ Ok(rust_handle_value(rhandle, class))
+ }
+ // TODO(plugin): the remaining RepositoryManager surface is widened on demand, driven
+ // by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the RepositoryManager method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+fn dispatch_repository_method(
+ repository: &RepositoryInterfaceHandle,
+ method_name: &str,
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getPackages" => {
+ let packages = repository.borrow_mut().get_packages().map_err(|error| {
+ // TODO(plugin): the original exception class is collapsed to RuntimeException
+ // on this side of the boundary.
+ runtime_throw(format!("getPackages failed over RPC: {error}"))
+ })?;
+ let mut items = Vec::with_capacity(packages.len());
+ for package in packages {
+ items.push(package_handle_value(package.as_rc())?);
+ }
+ Ok(PluginValue::List(items))
+ }
+ // TODO(plugin): the remaining RepositoryInterface surface is widened on demand,
+ // driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the repository method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+fn dispatch_package_method(
+ package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ let package = package.borrow();
+ let package = package.as_package_interface();
+ match method_name {
+ "getName" => Ok(PluginValue::string(package.get_name().to_string())),
+ "getType" => Ok(PluginValue::string(package.get_type())),
+ "getPrettyVersion" => Ok(PluginValue::string(
+ package.get_pretty_version().to_string(),
+ )),
+ "getExtra" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
+ package.get_extra(),
+ ))),
+ "getFullPrettyVersion" => {
+ let truncate = match args.first() {
+ None => true,
+ Some(PluginValue::Bool(truncate)) => *truncate,
+ other => {
+ return Err(runtime_throw(format!(
+ "getFullPrettyVersion expects a bool truncate flag, got {other:?}"
+ )));
+ }
+ };
+ let display_mode = match args.get(1) {
+ None | Some(PluginValue::Int(0)) => DisplayMode::SourceRefIfDev,
+ Some(PluginValue::Int(1)) => DisplayMode::SourceRef,
+ Some(PluginValue::Int(2)) => DisplayMode::DistRef,
+ other => {
+ return Err(runtime_throw(format!(
+ "getFullPrettyVersion expects a display mode of 0..=2, got {other:?}"
+ )));
+ }
+ };
+ Ok(PluginValue::string(
+ package.get_full_pretty_version(truncate, display_mode),
+ ))
+ }
+ "getRequires" => {
+ let requires = package.get_requires();
+ if requires.is_empty() {
+ // An empty PHP array crosses the wire as a list.
+ Ok(PluginValue::List(Vec::new()))
+ } else {
+ // TODO(plugin): Link is a rust-snapshot value whose constraint field must
+ // materialize as a real composer/semver object in the child; the snapshot
+ // encoding does not exist yet.
+ Err(runtime_throw(
+ "encoding Link values over RPC is not implemented yet".to_string(),
+ ))
+ }
+ }
+ // TODO(plugin): the remaining PackageInterface surface (setters included) is widened
+ // on demand, driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the package method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+fn dispatch_installation_manager_method(
+ im: &std::rc::Rc<std::cell::RefCell<dyn InstallationManagerInterface>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getInstallPath" => {
+ let package = match args.first() {
+ Some(PluginValue::RustHandle(handle)) => {
+ let entity = R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned());
+ match entity {
+ Some(RustEntity::Package(package)) => {
+ PackageInterfaceHandle::from_rc_unchecked(package)
+ }
+ _ => {
+ return Err(runtime_throw(format!(
+ "getInstallPath expects a package handle, got Rust handle {}",
+ handle.rhandle
+ )));
+ }
+ }
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "getInstallPath expects a package argument, got {other:?}"
+ )));
+ }
+ };
+ Ok(match im.borrow().get_install_path(package) {
+ Some(path) => PluginValue::string(path),
+ None => PluginValue::Null,
+ })
+ }
+ // TODO(plugin): the remaining InstallationManager surface is widened on demand,
+ // driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the InstallationManager method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
fn dispatch_io_method(
io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
method_name: &str,