aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin/php_plugin_proxy.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/plugin/php_plugin_proxy.rs')
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs360
1 files changed, 360 insertions, 0 deletions
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
new file mode 100644
index 00000000..87d67ea4
--- /dev/null
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -0,0 +1,360 @@
+//! PHP-backed `PluginInterface` adapter and the R table serving its RPC callbacks.
+//!
+//! This module has no Composer counterpart: it is the Rust half of the plugin runtime split
+//! (a real PHP child process executes the plugin code, see `docs/dev/php-rpc.md`). The plugin
+//! entity lives in the worker's P table; Rust-side entities the plugin can call back into
+//! (`$composer`, `$io`) live in the R table here.
+
+use crate::autoload::ClassLoader;
+use crate::composer::ComposerHandle;
+use crate::io::IOInterface;
+use crate::plugin::plugin_interface::PluginInterface;
+use indexmap::IndexMap;
+use shirabe_php_rpc::{
+ PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method, release_php_handle,
+};
+
+/// A Rust-side entity a PHP proxy stub points back to.
+#[derive(Debug)]
+enum RustEntity {
+ Composer(ComposerHandle),
+ Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>),
+}
+
+thread_local! {
+ /// The R table. Entries are strong references kept for the worker's lifetime.
+ /// TODO(plugin): GC (dropping entries on ReleaseRustHandle) is not implemented yet;
+ /// until then entities registered here are intentionally never released.
+ static R_TABLE: std::cell::RefCell<IndexMap<u64, RustEntity>> =
+ std::cell::RefCell::new(IndexMap::new());
+}
+
+/// 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 {
+ 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
+ {
+ return *rhandle;
+ }
+ }
+ let rhandle = shirabe_php_rpc::alloc_rhandle();
+ table.insert(rhandle, RustEntity::Composer(composer.clone()));
+ rhandle
+ })
+}
+
+/// Registers an IO instance in the R table, interned like [`register_composer_entity`].
+pub(crate) fn register_io_entity(io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) -> u64 {
+ 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
+ })
+}
+
+/// 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(
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+) -> anyhow::Result<&'static str> {
+ let borrowed = io.borrow();
+ let any = borrowed.as_any();
+ if any
+ .downcast_ref::<crate::io::buffer_io::BufferIO>()
+ .is_some()
+ {
+ Ok("Composer\\IO\\BufferIO")
+ } else if any
+ .downcast_ref::<crate::io::console_io::ConsoleIO>()
+ .is_some()
+ {
+ Ok("Composer\\IO\\ConsoleIO")
+ } else if any.downcast_ref::<crate::io::null_io::NullIO>().is_some() {
+ Ok("Composer\\IO\\NullIO")
+ } else {
+ // TODO(plugin): only the IO classes with hand-written proxy stubs can cross the
+ // boundary until a stub generator exists.
+ Err(anyhow::anyhow!(
+ "no proxy stub class is available for this IO implementation"
+ ))
+ }
+}
+
+/// Looks a class up in every registered Rust-side `ClassLoader`, in registration order — the
+/// Rust mirror of what the PHP `spl_autoload_register` stack would do in-process.
+pub(crate) fn find_file_in_registered_loaders(class: &str) -> Option<String> {
+ for (_vendor_dir, mut loader) in ClassLoader::get_registered_loaders() {
+ if let Some(file) = loader.find_file(class) {
+ return Some(file);
+ }
+ }
+ None
+}
+
+/// Serves `CallRustMethod` requests from the child while a plugin-related call is in flight:
+/// rhandle 0 is the runtime service endpoint (autoload lookups), every other rhandle resolves
+/// through the R table. Unsupported methods are explicit errors, never silent fallbacks.
+#[derive(Debug)]
+pub(crate) struct PluginRpcDispatcher;
+
+impl RustMethodDispatcher for PluginRpcDispatcher {
+ fn dispatch(
+ &mut self,
+ rhandle: u64,
+ method_name: &str,
+ args: Vec<PluginValue>,
+ _out_param_positions: &[u32],
+ ) -> Result<PluginValue, PhpThrow> {
+ if rhandle == 0 {
+ if method_name == "__shirabe_find_file" {
+ let class = match args.first() {
+ Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(),
+ other => {
+ return Err(runtime_throw(format!(
+ "__shirabe_find_file expects a class name argument, got {other:?}"
+ )));
+ }
+ };
+ return Ok(match find_file_in_registered_loaders(&class) {
+ Some(file) => PluginValue::string(file),
+ None => PluginValue::Null,
+ });
+ }
+ return Err(runtime_throw(format!(
+ "unknown runtime service method `{method_name}`"
+ )));
+ }
+
+ R_TABLE.with(|table| {
+ let table = table.borrow();
+ match table.get(&rhandle) {
+ 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"
+ )))
+ }
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ }
+ })
+ }
+}
+
+fn dispatch_io_method(
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "write" | "writeError" => {
+ let (messages, newline, verbosity) = decode_write_args(method_name, args)?;
+ let borrowed = io.borrow();
+ for message in &messages {
+ if method_name == "write" {
+ borrowed.write3(message, newline, verbosity);
+ } else {
+ borrowed.write_error3(message, newline, verbosity);
+ }
+ }
+ Ok(PluginValue::Null)
+ }
+ "isInteractive" => Ok(PluginValue::Bool(io.borrow().is_interactive())),
+ "isVerbose" => Ok(PluginValue::Bool(io.borrow().is_verbose())),
+ "isVeryVerbose" => Ok(PluginValue::Bool(io.borrow().is_very_verbose())),
+ "isDebug" => Ok(PluginValue::Bool(io.borrow().is_debug())),
+ "isDecorated" => Ok(PluginValue::Bool(io.borrow().is_decorated())),
+ // TODO(plugin): the remaining IOInterface surface (ask*, authentications, ...) is
+ // widened on demand, driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the IO method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+/// Decodes `($messages, bool $newline, int $verbosity)`: `$messages` is a string or a list of
+/// strings in PHP.
+fn decode_write_args(
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<(Vec<String>, bool, i64), PhpThrow> {
+ let messages = match args.first() {
+ Some(PluginValue::String(bytes)) => vec![String::from_utf8_lossy(bytes).into_owned()],
+ Some(PluginValue::List(items)) => {
+ let mut messages = Vec::with_capacity(items.len());
+ for item in items {
+ match item {
+ PluginValue::String(bytes) => {
+ messages.push(String::from_utf8_lossy(bytes).into_owned());
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects string messages, got {other:?}"
+ )));
+ }
+ }
+ }
+ messages
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a string or list of strings, got {other:?}"
+ )));
+ }
+ };
+ let newline = match args.get(1) {
+ Some(PluginValue::Bool(b)) => *b,
+ None => true,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a bool newline flag, got {other:?}"
+ )));
+ }
+ };
+ let verbosity = match args.get(2) {
+ Some(PluginValue::Int(v)) => *v,
+ None => crate::io::NORMAL,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects an int verbosity, got {other:?}"
+ )));
+ }
+ };
+ Ok((messages, newline, verbosity))
+}
+
+fn runtime_throw(message: String) -> PhpThrow {
+ PhpThrow {
+ exception_class: "RuntimeException".to_string(),
+ message,
+ code: 0,
+ }
+}
+
+/// `PluginInterface` adapter for a plugin entity living in the PHP child process: every
+/// lifecycle call is forwarded as a `CallPhpMethod` RPC.
+#[derive(Debug)]
+pub struct PhpPluginProxy {
+ pub(crate) phandle: u64,
+ pub(crate) class: String,
+}
+
+impl PhpPluginProxy {
+ pub fn new(phandle: u64, class: String) -> Self {
+ Self { phandle, class }
+ }
+
+ fn forward_lifecycle_call(
+ &self,
+ method: &str,
+ composer: &ComposerHandle,
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ let composer_rhandle = register_composer_entity(composer);
+ let io_rhandle = register_io_entity(io);
+ let io_class = io_stub_class(io)?;
+ let args = vec![
+ PluginValue::RustHandle(RustObjHandle {
+ rhandle: composer_rhandle,
+ class: "Composer\\Composer".to_string(),
+ epoch: 0,
+ snapshot: None,
+ }),
+ PluginValue::RustHandle(RustObjHandle {
+ rhandle: io_rhandle,
+ class: io_class.to_string(),
+ epoch: 0,
+ snapshot: None,
+ }),
+ ];
+ let outcome = call_php_method(self.phandle, method, args, Some(&mut PluginRpcDispatcher))?;
+ match outcome {
+ Ok(_) => Ok(()),
+ // TODO(plugin): the original exception class is collapsed to RuntimeException on
+ // this side of the boundary.
+ Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ })),
+ }
+ }
+
+ /// For testing only: reads a public property of the plugin entity in the child, mirroring
+ /// PHPUnit assertions like `$plugins[0]->version`.
+ pub fn __get_property(&self, name: &str) -> anyhow::Result<shirabe_php_shim::PhpMixed> {
+ let outcome = shirabe_php_rpc::call_function(
+ "__shirabe_get_property",
+ vec![
+ PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle {
+ phandle: self.phandle,
+ class: self.class.clone(),
+ implements: Vec::new(),
+ }),
+ PluginValue::string(name),
+ ],
+ )?;
+ match outcome {
+ Ok(value) => Ok(value.to_php_mixed()?),
+ Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ })),
+ }
+ }
+}
+
+impl PluginInterface for PhpPluginProxy {
+ fn activate(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("activate", &composer, &io)
+ }
+
+ fn deactivate(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("deactivate", &composer, &io)
+ }
+
+ fn uninstall(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("uninstall", &composer, &io)
+ }
+
+ fn get_class_name(&self) -> String {
+ self.class.clone()
+ }
+
+ fn as_php_plugin_proxy(&self) -> Option<&PhpPluginProxy> {
+ Some(self)
+ }
+}
+
+impl Drop for PhpPluginProxy {
+ fn drop(&mut self) {
+ // A dead worker has nothing left to release.
+ let _ = release_php_handle(self.phandle);
+ }
+}