[zircon][rust] Add generic Dispatcher FFI infrastructure

This introduces the generic Dispatcher and ProcessDispatcher facades
in Rust to support C++ to Rust object migrations.

It includes:
- OpaqueFacade helper in zr library.
- LockToken accessor generator in ksync guarded macro.
- Generic Dispatcher and ProcessDispatcher opaque facades and FFI in Rust.
- Generic C++ FFI implementation for Dispatcher and ProcessDispatcher.
- KernelHandle type-safe improvements (take_ref_ptr, cast).

Bug: 532573303
Test: fx test core-counter-test-package

Change-Id: I465350db906f4251714b5f711f86c57fb8296600
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/1705137
Fuchsia-Auto-Submit: Adam Barth <abarth@google.com>
Reviewed-by: Adrian Danis <adanis@google.com>
Commit-Queue: Adam Barth <abarth@google.com>
diff --git a/src/lib/ksync/macro/src/lib.rs b/src/lib/ksync/macro/src/lib.rs
index eb2b9e0..2c15f28 100644
--- a/src/lib/ksync/macro/src/lib.rs
+++ b/src/lib/ksync/macro/src/lib.rs
@@ -439,6 +439,11 @@
                 #guard_accessors
 
                 #[inline]
+                #struct_vis fn token(&self) -> &::ksync::LockToken<'a, #class_ident> {
+                    self.inner.token()
+                }
+
+                #[inline]
                 #struct_vis fn fields<'b>(&'b self) -> #fields_ident #fields_ty_generics {
                     let me = self;
                     let token = me.inner.token();
diff --git a/src/lib/zr/src/lib.rs b/src/lib/zr/src/lib.rs
index 98540f7..b345404 100644
--- a/src/lib/zr/src/lib.rs
+++ b/src/lib/zr/src/lib.rs
@@ -10,6 +10,6 @@
 mod static_assert;
 mod string;
 
-pub use opaque::Opaque;
+pub use opaque::{Opaque, OpaqueFacade};
 pub use opaque_bytes::OpaqueBytes;
 pub use string::{parse_usize, to_array};
diff --git a/src/lib/zr/src/opaque.rs b/src/lib/zr/src/opaque.rs
index 023db31..94a95f0 100644
--- a/src/lib/zr/src/opaque.rs
+++ b/src/lib/zr/src/opaque.rs
@@ -35,3 +35,15 @@
         Self::uninit()
     }
 }
+
+/// A zero-sized type representing an opaque C++ object facade.
+///
+/// This is used as a field in Rust facade structs that represent C++ objects
+/// of unknown size. It keeps the facade struct `Sized` (size 0) so it can be
+/// used in FFI (thin pointers) and with generic containers like `RefPtr`,
+/// while ensuring LLVM knows the object has interior mutability.
+#[repr(C)]
+#[derive(Default)]
+pub struct OpaqueFacade {
+    _unused: UnsafeCell<()>,
+}
diff --git a/zircon/kernel/object/BUILD.gn b/zircon/kernel/object/BUILD.gn
index 9196d29..39f7652 100644
--- a/zircon/kernel/object/BUILD.gn
+++ b/zircon/kernel/object/BUILD.gn
@@ -18,6 +18,7 @@
     "counter_dispatcher.cc",
     "diagnostics.cc",
     "dispatcher.cc",
+    "dispatcher_ffi.cc",
     "event_dispatcher.cc",
     "event_pair_dispatcher.cc",
     "exception.cc",
@@ -49,6 +50,7 @@
     "pinned_memory_token_dispatcher.cc",
     "port_dispatcher.cc",
     "process_dispatcher.cc",
+    "process_dispatcher_ffi.cc",
     "profile_dispatcher.cc",
     "resource.cc",
     "resource_dispatcher.cc",
@@ -86,6 +88,7 @@
     "//zircon/kernel/lib/kconcurrent",
     "//zircon/kernel/lib/ktl",
     "//zircon/kernel/lib/ktrace",
+    "//zircon/kernel/lib/object-constants",
     "//zircon/kernel/lib/object_cache",
     "//zircon/kernel/lib/page",
     "//zircon/kernel/lib/page_cache",
@@ -175,8 +178,19 @@
   edition = "2024"
   source_root = "object.rs"
   sources = [
+    "dispatcher.rs",
+    "dispatcher_ffi.rs",
     "handle.rs",
     "object.rs",
+    "process_dispatcher.rs",
+    "process_dispatcher_ffi.rs",
   ]
-  deps = [ "//sdk/rust/zx-types" ]
+  deps = [
+    "//sdk/rust/zx-status",
+    "//sdk/rust/zx-types",
+    "//src/lib/kalloc",
+    "//src/lib/ksync",
+    "//src/lib/zr",
+    "//zircon/system/ulib/fbl/rust:fbl",
+  ]
 }
diff --git a/zircon/kernel/object/dispatcher.rs b/zircon/kernel/object/dispatcher.rs
new file mode 100644
index 0000000..4471482
--- /dev/null
+++ b/zircon/kernel/object/dispatcher.rs
@@ -0,0 +1,79 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::dispatcher_ffi::{
+    cpp_dispatcher_get_ref_counted, cpp_dispatcher_on_zero_handles, cpp_dispatcher_recycle,
+    cpp_dispatcher_update_state, cpp_dispatcher_update_state_locked,
+};
+use core::marker::{PhantomData, PhantomPinned};
+use core::ptr::NonNull;
+use kalloc::AllocError;
+use ksync::LockToken;
+
+pub trait DispatcherOps {
+    type LockClass;
+
+    fn dispatcher(&self) -> *const Dispatcher;
+
+    fn on_zero_handles(&self) {
+        unsafe {
+            cpp_dispatcher_on_zero_handles(self.dispatcher());
+        }
+    }
+
+    fn update_state(&self, clear_mask: u32, set_mask: u32) {
+        unsafe {
+            cpp_dispatcher_update_state(self.dispatcher(), clear_mask, set_mask);
+        }
+    }
+
+    fn update_state_locked(
+        &self,
+        _token: &LockToken<'_, Self::LockClass>,
+        clear_mask: u32,
+        set_mask: u32,
+    ) {
+        unsafe {
+            cpp_dispatcher_update_state_locked(self.dispatcher(), clear_mask, set_mask);
+        }
+    }
+}
+
+#[repr(C)]
+pub struct Dispatcher {
+    _marker: PhantomData<PhantomPinned>,
+    _facade: zr::OpaqueFacade,
+}
+
+unsafe impl Send for Dispatcher {}
+unsafe impl Sync for Dispatcher {}
+
+impl DispatcherOps for Dispatcher {
+    type LockClass = ();
+
+    fn dispatcher(&self) -> *const Dispatcher {
+        self
+    }
+}
+
+impl fbl::HasRefCount for Dispatcher {
+    fn ref_count(&self) -> &fbl::RefCounted {
+        unsafe {
+            let ptr = cpp_dispatcher_get_ref_counted(self);
+            &*(ptr.cast::<fbl::RefCounted>())
+        }
+    }
+}
+
+unsafe impl fbl::Recyclable for Dispatcher {
+    unsafe fn recycle(ptr: NonNull<Self>) {
+        unsafe {
+            cpp_dispatcher_recycle(ptr.as_ptr());
+        }
+    }
+
+    fn allocate(_value: Self) -> Result<NonNull<Self>, AllocError> {
+        Err(AllocError)
+    }
+}
diff --git a/zircon/kernel/object/dispatcher_ffi.cc b/zircon/kernel/object/dispatcher_ffi.cc
new file mode 100644
index 0000000..75ad6c7
--- /dev/null
+++ b/zircon/kernel/object/dispatcher_ffi.cc
@@ -0,0 +1,28 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <object/dispatcher.h>
+
+extern "C" {
+
+void cpp_dispatcher_on_zero_handles(Dispatcher* disp) { disp->on_zero_handles(); }
+
+void cpp_dispatcher_update_state(Dispatcher* disp, zx_signals_t clear_mask, zx_signals_t set_mask) {
+  disp->UpdateState(clear_mask, set_mask);
+}
+
+void cpp_dispatcher_update_state_locked(Dispatcher* disp, zx_signals_t clear_mask,
+                                        zx_signals_t set_mask) TA_NO_THREAD_SAFETY_ANALYSIS {
+  disp->UpdateStateLocked(clear_mask, set_mask);
+}
+
+void* cpp_dispatcher_get_ref_counted(const Dispatcher* disp) {
+  return disp->get_ref_counted_base();
+}
+
+void cpp_dispatcher_recycle(Dispatcher* disp) {
+  fbl::internal::recycler<Dispatcher>::recycle(disp);
+}
+
+}  // extern "C"
diff --git a/zircon/kernel/object/dispatcher_ffi.rs b/zircon/kernel/object/dispatcher_ffi.rs
new file mode 100644
index 0000000..fe2f0af
--- /dev/null
+++ b/zircon/kernel/object/dispatcher_ffi.rs
@@ -0,0 +1,23 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::dispatcher::Dispatcher;
+
+unsafe extern "C" {
+    pub(crate) fn cpp_dispatcher_on_zero_handles(dispatcher: *const Dispatcher);
+    pub(crate) fn cpp_dispatcher_update_state(
+        dispatcher: *const Dispatcher,
+        clear_mask: u32,
+        set_mask: u32,
+    );
+    pub(crate) fn cpp_dispatcher_update_state_locked(
+        dispatcher: *const Dispatcher,
+        clear_mask: u32,
+        set_mask: u32,
+    );
+    pub(crate) fn cpp_dispatcher_get_ref_counted(
+        dispatcher: *const Dispatcher,
+    ) -> *mut core::ffi::c_void;
+    pub(crate) fn cpp_dispatcher_recycle(dispatcher: *const Dispatcher);
+}
diff --git a/zircon/kernel/object/handle.cc b/zircon/kernel/object/handle.cc
index b1b2ff8c..f5dbf1e 100644
--- a/zircon/kernel/object/handle.cc
+++ b/zircon/kernel/object/handle.cc
@@ -11,6 +11,7 @@
 
 #include <fbl/conditional_select_nospec.h>
 #include <object/dispatcher.h>
+#include <object/process_dispatcher.h>
 
 namespace {
 
diff --git a/zircon/kernel/object/handle.rs b/zircon/kernel/object/handle.rs
index 0c19561..ce418ae 100644
--- a/zircon/kernel/object/handle.rs
+++ b/zircon/kernel/object/handle.rs
@@ -2,7 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use zx_types::zx_handle_t;
+use crate::DispatcherOps;
+use fbl::{HasRefCount, Recyclable, RefPtr};
+use zx_status::Status;
+use zx_types::{zx_handle_t, zx_rights_t};
 
 /// A wrapper around a handle value received from userspace.
 #[repr(transparent)]
@@ -22,3 +25,67 @@
         self.value
     }
 }
+
+#[repr(transparent)]
+pub struct KernelHandle<T>
+where
+    T: HasRefCount + Recyclable + DispatcherOps,
+{
+    ptr: *const T,
+}
+
+impl<T> KernelHandle<T>
+where
+    T: HasRefCount + Recyclable + DispatcherOps,
+{
+    pub fn new(dispatcher: RefPtr<T>) -> Self {
+        Self { ptr: RefPtr::into_raw(dispatcher) }
+    }
+
+    /// Casts this handle to a generic Dispatcher handle.
+    pub fn cast(self) -> KernelHandle<crate::dispatcher::Dispatcher> {
+        let ptr = self.ptr.cast::<crate::dispatcher::Dispatcher>();
+        core::mem::forget(self);
+        KernelHandle { ptr }
+    }
+
+    pub fn release(mut self) -> RefPtr<T> {
+        let ref_ptr = self.take_ref_ptr().expect("KernelHandle was empty");
+        core::mem::forget(self);
+        ref_ptr
+    }
+
+    fn take_ref_ptr(&mut self) -> Option<RefPtr<T>> {
+        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null());
+        if ptr.is_null() {
+            None
+        } else {
+            // SAFETY: ptr came from RefPtr::into_raw.
+            Some(unsafe { RefPtr::from_raw(ptr) })
+        }
+    }
+
+    pub fn dispatcher(&self) -> &T {
+        assert!(!self.ptr.is_null());
+        // SAFETY: We are holding a reference to the object, which ensures that it lives as long as
+        // we do.
+        unsafe { &*self.ptr }
+    }
+
+    pub fn make_and_add_handle(self, rights: zx_rights_t) -> Result<HandleValue, Status> {
+        crate::process_dispatcher::ProcessDispatcher::with_current(|up| {
+            up.make_and_add_handle(self, rights)
+        })
+    }
+}
+
+impl<T> Drop for KernelHandle<T>
+where
+    T: HasRefCount + Recyclable + DispatcherOps,
+{
+    fn drop(&mut self) {
+        if let Some(ref_ptr) = self.take_ref_ptr() {
+            ref_ptr.on_zero_handles();
+        }
+    }
+}
diff --git a/zircon/kernel/object/include/object/dispatcher.h b/zircon/kernel/object/include/object/dispatcher.h
index c50132b..babc000 100644
--- a/zircon/kernel/object/include/object/dispatcher.h
+++ b/zircon/kernel/object/include/object/dispatcher.h
@@ -31,6 +31,17 @@
 #include <object/handle.h>
 #include <object/signal_observer.h>
 
+class Dispatcher;
+
+extern "C" {
+void cpp_dispatcher_on_zero_handles(Dispatcher* disp);
+void cpp_dispatcher_update_state(Dispatcher* disp, zx_signals_t clear_mask, zx_signals_t set_mask);
+void cpp_dispatcher_update_state_locked(Dispatcher* disp, zx_signals_t clear_mask,
+                                        zx_signals_t set_mask);
+void* cpp_dispatcher_get_ref_counted(const Dispatcher* disp);
+void cpp_dispatcher_recycle(Dispatcher* disp);
+}
+
 template <typename T>
 struct DispatchTag;
 template <typename T>
@@ -95,6 +106,12 @@
 //
 // You don't derive directly from this class; instead derive
 // from SoloDispatcher or PeeredDispatcher.
+extern "C" {
+void cpp_dispatcher_update_state(Dispatcher* disp, zx_signals_t clear_mask, zx_signals_t set_mask);
+void cpp_dispatcher_update_state_locked(Dispatcher* disp, zx_signals_t clear_mask,
+                                        zx_signals_t set_mask);
+}
+
 class Dispatcher : private fbl::RefCountedUpgradeable<Dispatcher>,
                    private fbl::Recyclable<Dispatcher> {
  public:
@@ -299,6 +316,10 @@
 
  private:
   friend class fbl::Recyclable<Dispatcher>;
+  friend void cpp_dispatcher_update_state(Dispatcher* disp, zx_signals_t clear_mask,
+                                          zx_signals_t set_mask);
+  friend void cpp_dispatcher_update_state_locked(Dispatcher* disp, zx_signals_t clear_mask,
+                                                 zx_signals_t set_mask);
   void fbl_recycle();
 
   fbl::Canary<fbl::magic("DISP")> canary_;
diff --git a/zircon/kernel/object/include/object/process_dispatcher.h b/zircon/kernel/object/include/object/process_dispatcher.h
index 216db6a..bd01c97 100644
--- a/zircon/kernel/object/include/object/process_dispatcher.h
+++ b/zircon/kernel/object/include/object/process_dispatcher.h
@@ -437,4 +437,11 @@
 
 const char* StateToString(ProcessDispatcher::State state);
 
+extern "C" {
+ProcessDispatcher* cpp_process_dispatcher_current();
+zx_status_t cpp_process_dispatcher_make_and_add_handle(ProcessDispatcher* process,
+                                                       KernelHandle<Dispatcher>* handle,
+                                                       zx_rights_t rights, zx_handle_t* out_handle);
+}
+
 #endif  // ZIRCON_KERNEL_OBJECT_INCLUDE_OBJECT_PROCESS_DISPATCHER_H_
diff --git a/zircon/kernel/object/object.rs b/zircon/kernel/object/object.rs
index 3b6225e..43724f1 100644
--- a/zircon/kernel/object/object.rs
+++ b/zircon/kernel/object/object.rs
@@ -4,6 +4,12 @@
 
 #![no_std]
 
+mod dispatcher;
+mod dispatcher_ffi;
 mod handle;
+mod process_dispatcher;
+mod process_dispatcher_ffi;
 
-pub use handle::HandleValue;
+pub use dispatcher::{Dispatcher, DispatcherOps};
+pub use handle::{HandleValue, KernelHandle};
+pub use process_dispatcher::ProcessDispatcher;
diff --git a/zircon/kernel/object/process_dispatcher.rs b/zircon/kernel/object/process_dispatcher.rs
new file mode 100644
index 0000000..69ffff8f
--- /dev/null
+++ b/zircon/kernel/object/process_dispatcher.rs
@@ -0,0 +1,52 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::handle::{HandleValue, KernelHandle};
+use crate::process_dispatcher_ffi::{
+    cpp_process_dispatcher_current, cpp_process_dispatcher_make_and_add_handle,
+};
+use core::marker::{PhantomData, PhantomPinned};
+use zx_status::Status;
+use zx_types::zx_rights_t;
+
+#[repr(C)]
+pub struct ProcessDispatcher {
+    _marker: PhantomData<PhantomPinned>,
+    _facade: zr::OpaqueFacade,
+}
+
+unsafe impl Send for ProcessDispatcher {}
+unsafe impl Sync for ProcessDispatcher {}
+
+impl ProcessDispatcher {
+    /// Executes the given function with a reference to the current process.
+    pub fn with_current<R>(f: impl FnOnce(&ProcessDispatcher) -> R) -> R {
+        // SAFETY: The current process is guaranteed to be valid for the duration of the call.
+        let proc = unsafe { &*cpp_process_dispatcher_current() };
+        f(proc)
+    }
+
+    /// Creates a handle for the given dispatcher in this process's handle table.
+    pub fn make_and_add_handle<T>(
+        &self,
+        handle: KernelHandle<T>,
+        rights: zx_rights_t,
+    ) -> Result<HandleValue, Status>
+    where
+        T: fbl::HasRefCount + fbl::Recyclable + crate::DispatcherOps,
+    {
+        let mut handle = handle.cast();
+        let mut out = HandleValue::default();
+        let status = unsafe {
+            cpp_process_dispatcher_make_and_add_handle(
+                self as *const _,
+                &mut handle,
+                rights,
+                &mut out,
+            )
+        };
+        Status::ok(status)?;
+        Ok(out)
+    }
+}
diff --git a/zircon/kernel/object/process_dispatcher_ffi.cc b/zircon/kernel/object/process_dispatcher_ffi.cc
new file mode 100644
index 0000000..f3c4a15
--- /dev/null
+++ b/zircon/kernel/object/process_dispatcher_ffi.cc
@@ -0,0 +1,19 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <object/handle.h>
+#include <object/process_dispatcher.h>
+
+extern "C" {
+
+ProcessDispatcher* cpp_process_dispatcher_current() { return ProcessDispatcher::GetCurrent(); }
+
+zx_status_t cpp_process_dispatcher_make_and_add_handle(ProcessDispatcher* process,
+                                                       KernelHandle<Dispatcher>* handle,
+                                                       zx_rights_t rights,
+                                                       zx_handle_t* out_handle) {
+  return process->MakeAndAddHandle(ktl::move(*handle), rights, out_handle);
+}
+
+}  // extern "C"
diff --git a/zircon/kernel/object/process_dispatcher_ffi.rs b/zircon/kernel/object/process_dispatcher_ffi.rs
new file mode 100644
index 0000000..e688708
--- /dev/null
+++ b/zircon/kernel/object/process_dispatcher_ffi.rs
@@ -0,0 +1,18 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::dispatcher::Dispatcher;
+use crate::handle::{HandleValue, KernelHandle};
+use crate::process_dispatcher::ProcessDispatcher;
+use zx_types::{zx_rights_t, zx_status_t};
+
+unsafe extern "C" {
+    pub(crate) fn cpp_process_dispatcher_current() -> *const ProcessDispatcher;
+    pub(crate) fn cpp_process_dispatcher_make_and_add_handle(
+        process: *const ProcessDispatcher,
+        handle: *mut KernelHandle<Dispatcher>,
+        rights: zx_rights_t,
+        out_handle: *mut HandleValue,
+    ) -> zx_status_t;
+}