[F55 Path A] DCOM-managed INmxSvcCallback sink
Replace the hand-rolled CallbackExporter (TCP listener + custom
OBJREF) with a real `windows-rs` `#[implement]` COM class for
INmxSvcCallback, marshalled via CoMarshalInterface. NmxSvc validates
the callback OBJREF by calling IObjectExporter::ResolveOxid against
the local RPCSS at 127.0.0.1:135; hand-rolled OXIDs aren't registered
there, which is why RegisterEngine2 returned RPC_S_SERVER_UNAVAILABLE
(1722) on every live attempt. CoMarshalInterface registers the OXID
with RPCSS automatically, so the SCM-side resolution succeeds.
Mirrors MxNativeSession.CreateRegisteredService (cs:624), which is
the .NET reference's working path:
ComObjRefProvider.MarshalInterfaceObjRef(callback,
INmxSvcCallback, DifferentMachine)
Layout:
- mxaccess-callback::dcom_sink — INmxSvcCallback + DcomCallbackSink
+ create_dcom_callback_sink_objref. Forwards inbound calls into
the same CallbackEvent::CallbackInvoked { opnum, body } shape the
legacy exporter produces, so callback_router stays path-agnostic.
- Session::from_nmx_client — branched on `windows-com`. Real DCOM
sink when on; legacy CallbackExporter when off (kept for unit
tests that run against an in-process fake NMX peer).
- SessionInner.dcom_sink_holder: Option<IUnknownHolder> — keeps the
COM ref alive for the session's lifetime; shutdown_nmx drops it.
- mxaccess-rpc + mxaccess-callback: windows-rs 0.59 → 0.62. The 0.59
#[implement] macro generates code that doesn't compile under
edition 2024; 0.62 is fixed.
Live result: cargo test -p mxaccess-compat --features
live-windows-com --test lmx_write_complete_live -- --ignored
--nocapture passes end-to-end. RegisterEngine2 OK, write
round-trips, OnWriteComplete fires with the captured MxStatus shape.
Unblocks F49 step 5; F55 marked Resolved in design/followups.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,7 @@ pub enum ProviderError {
|
||||
/// which we accept. If a thread is already initialised to STA we receive
|
||||
/// `RPC_E_CHANGED_MODE` — also treated as success (the existing apartment
|
||||
/// is fine for `CoMarshalInterface`).
|
||||
fn ensure_apartment() -> Result<(), ProviderError> {
|
||||
pub fn ensure_apartment() -> Result<(), ProviderError> {
|
||||
thread_local! {
|
||||
// `OnceLock` per thread guarantees we only attempt CoInitializeEx
|
||||
// once per worker; subsequent calls are a no-op.
|
||||
@@ -270,6 +270,18 @@ pub struct IUnknownHolder {
|
||||
inner: IUnknown,
|
||||
}
|
||||
|
||||
impl IUnknownHolder {
|
||||
/// Wrap an existing `IUnknown` into a holder. Used by callers
|
||||
/// (e.g. `mxaccess-callback::dcom_sink`) that have an `IUnknown`
|
||||
/// from a `windows-rs` `#[implement]` cast and need to keep the
|
||||
/// COM ref alive for the same Path-A reasons documented at the
|
||||
/// type level.
|
||||
#[must_use]
|
||||
pub fn from_iunknown(inner: IUnknown) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for IUnknownHolder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IUnknownHolder").finish_non_exhaustive()
|
||||
|
||||
Reference in New Issue
Block a user