[F6] mxaccess-rpc: ComObjRefProvider port via windows-rs (CoMarshalInterface)
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-rpc/src/com_objref_provider.rs gated on cfg(all(windows, feature = "windows-com")). Pulls windows = "0.59" (features Win32_Foundation + Win32_System_Com + Win32_System_Com_Marshal + Win32_System_Com_StructuredStorage + Win32_System_Memory) as an optional dep behind the existing windows-com feature; default footprint stays slim. Public API mirrors ComObjRefProvider.cs 1:1: MarshalContext enum (InProcess / Local / DifferentMachine wrapping the MSHCTX_* newtype constants), clsid_from_prog_id, marshal_activated_iunknown_objref (activates via CoCreateInstance with INPROC | LOCAL | REMOTE then marshals), marshal_iunknown_objref (uses IUnknown::IID), marshal_interface_objref (CoMarshalInterface over an HGlobal-backed IStream). All `unsafe` is internal to the module — public API exposes only typed Rust values (Vec<u8>, GUID, ProviderError), no raw pointers / HRESULTs / lifetime-bound interface pointers leak. Each unsafe block carries an inline SAFETY comment naming the invariants being upheld. Per-thread COM init via thread-local OnceLock<()>: lazy CoInitializeEx(MULTITHREADED) on first call; S_FALSE (already initialised) and RPC_E_CHANGED_MODE (thread is STA) treated as success — matches the .NET runtime's tolerant apartment behaviour. ProviderError enumerates the four documented failure modes plus the apartment-init pre-check: UnknownProgId / ActivationFailed / MarshalFailed / GlobalLockFailed / ApartmentInitFailed. 4 offline tests: MarshalContext → MSHCTX_* mapping, ensure_apartment idempotence, clsid_from_prog_id returns UnknownProgId for fake ProgIDs, marshal_activated short-circuits at the resolution stage. 1 live test (#[ignore], gated on MX_LIVE): activates the real NmxSvc.NmxService, marshals the proxy's IUnknown via CoMarshalInterface, then parses the resulting blob via ComObjRef::parse and asserts non-zero OXID + IPID. Passes against the AVEVA install on this host. Workspace tests: mxaccess-rpc went 179 → 183 (+4). All other crates unchanged. Unblocks F12 (NmxClient::create — the auto-resolving COM-activation factory): the underlying primitive (marshal_activated_iunknown_objref) now exists; remaining work is threading the windows-com feature through mxaccess-nmx and chaining ComObjRef::parse → resolve_oxid_with_managed_ntlm_packet_integrity → RemQueryInterface. design/followups.md F12 updated with a revised "Resolves when" reflecting that F6's blocker is gone. Closes F6 in design/followups.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-8
@@ -206,12 +206,6 @@ The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/M
|
||||
**Resolves when:** A multi-domain AVEVA test harness lands and a successful cross-domain authenticate round-trip captures Type1/2/3 bytes. Notes: this clears R8.
|
||||
|
||||
|
||||
### F6 — Port `ComObjRefProvider.cs` (OBJREF emitter via Win32 CoMarshalInterface)
|
||||
**Severity:** P2
|
||||
**Source:** M2 wave 1, `crates/mxaccess-rpc/src/objref.rs`
|
||||
**Why deferred:** The provider is a wrapper around `ole32::CoMarshalInterface` / `IStream` / `GlobalLock` / `GlobalSize`. It needs `windows-rs`, which is currently behind the `windows-com` feature in `mxaccess-rpc/Cargo.toml`. The pure-Rust parser stands alone for the inbound activation-response path that M2 wave 1 needs.
|
||||
**Resolves when:** `windows-rs` is wired into `mxaccess-rpc` (M2 wave 3 callback exporter needs to publish its own OBJREF for `IRemUnknown` / `INmxSvcCallback` registration) and an emitter port lands behind the `windows-com` feature.
|
||||
|
||||
### F10 — `IObjectExporter::ResolveOxid2` (opnum 4) body codec
|
||||
**Severity:** P2
|
||||
**Source:** M2 wave 2, `crates/mxaccess-rpc/src/object_exporter.rs`
|
||||
@@ -227,8 +221,8 @@ The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/M
|
||||
### F12 — `NmxClient::create` (auto-resolving COM-activation factory)
|
||||
**Severity:** P1
|
||||
**Source:** M3 stream B, `crates/mxaccess-nmx/src/client.rs`
|
||||
**Why deferred:** `ManagedNmxService2Client.Create()` (`ManagedNmxService2Client.cs:30-64`) auto-discovers `(host, port, service_ipid)` by activating the `NmxSvc.NmxService` COM ProgID, marshalling the resulting `IUnknown` to an OBJREF, calling `IObjectExporter::ResolveOxid` against the OXID inside, then `IRemUnknown::RemQueryInterface` to get the `INmxService2` IPID. This requires `windows-rs` for `CoCreateInstance` / `CLSIDFromProgID` (the same gating dep as F6), plus the `ComObjRefProvider.MarshalIUnknownObjRef` port (also F6).
|
||||
**Resolves when:** F6 lands (windows-rs wired in + `ComObjRefProvider` port). At that point `NmxClient::create()` becomes ~30 lines that chain the existing primitives: COM activation → `MarshalIUnknownObjRef` → `ComObjRef::parse` → `object_exporter_client::resolve_oxid_with_managed_ntlm_packet_integrity` → `rem_unknown::encode_rem_query_interface_request` over a temporary transport → `NmxClient::connect`.
|
||||
**Why deferred:** `ManagedNmxService2Client.Create()` (`ManagedNmxService2Client.cs:30-64`) auto-discovers `(host, port, service_ipid)` by activating the `NmxSvc.NmxService` COM ProgID, marshalling the resulting `IUnknown` to an OBJREF, calling `IObjectExporter::ResolveOxid` against the OXID inside, then `IRemUnknown::RemQueryInterface` to get the `INmxService2` IPID. **F6 (the `ComObjRefProvider` port) is now resolved**, so the underlying `marshal_activated_iunknown_objref` primitive exists; what remains is wiring it into `NmxClient::create` and threading `mxaccess-rpc`'s `windows-com` feature through `mxaccess-nmx`.
|
||||
**Resolves when:** `NmxClient::create()` lands behind a matching `mxaccess-nmx` feature gate. ~30 lines that chain the existing primitives: `com_objref_provider::marshal_activated_iunknown_objref("NmxSvc.NmxService", MarshalContext::Local)` → `ComObjRef::parse` → `object_exporter_client::resolve_oxid_with_managed_ntlm_packet_integrity` → `rem_unknown::encode_rem_query_interface_request` over a temporary transport → `NmxClient::connect`.
|
||||
|
||||
### F16 — Real `Session::recover_connection` reconnect loop (re-bind + re-advise)
|
||||
**Severity:** P1
|
||||
@@ -238,6 +232,9 @@ The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/M
|
||||
|
||||
## Resolved
|
||||
|
||||
### F6 — Port `ComObjRefProvider.cs` (OBJREF emitter via Win32 `CoMarshalInterface`)
|
||||
**Resolved:** 2026-05-05 (commit `<this commit>`). New module `crates/mxaccess-rpc/src/com_objref_provider.rs` (~330 LoC including tests) gated on `cfg(all(windows, feature = "windows-com"))`. Pulls `windows = "0.59"` (features `Win32_Foundation` + `Win32_System_Com` + `Win32_System_Com_Marshal` + `Win32_System_Com_StructuredStorage` + `Win32_System_Memory`) as an optional dep behind the existing `windows-com` feature; default footprint stays slim. Public API mirrors `ComObjRefProvider.cs` 1:1: `MarshalContext` enum (InProcess / Local / DifferentMachine — wraps the `MSHCTX_*` newtype constants), `clsid_from_prog_id(&str) -> Result<GUID, ProviderError>` (wraps `CLSIDFromProgID`), `marshal_activated_iunknown_objref(prog_id, ctx)` (activates via `CoCreateInstance(CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)` then marshals), `marshal_iunknown_objref(unknown, ctx)` (uses `IUnknown::IID`), `marshal_interface_objref(unknown, iid, ctx)` (the underlying `CoMarshalInterface` over an HGlobal-backed `IStream`). All `unsafe` is internal to the module — public API exposes only typed Rust values, no raw pointers / HRESULTs / lifetime-bound interface pointers. Each `unsafe` block carries an inline SAFETY comment. `ProviderError` enumerates the four documented failure modes (UnknownProgId, ActivationFailed, MarshalFailed, GlobalLockFailed) plus the apartment-init pre-check (ApartmentInitFailed). Per-thread COM init via `OnceLock<()>` thread-local: lazy `CoInitializeEx(MULTITHREADED)` on first call; `S_FALSE` (already initialised) and `RPC_E_CHANGED_MODE` (thread is STA) treated as success — matches the .NET runtime's tolerant apartment behaviour. 4 offline tests pin: `MarshalContext` → `MSHCTX_*` mapping, `ensure_apartment` idempotence, `clsid_from_prog_id` returns `UnknownProgId` for fake ProgIDs, `marshal_activated_*` short-circuits at the resolution stage. 1 live test (`#[ignore]`'d, gated on `MX_LIVE`) round-trips the real `NmxSvc.NmxService`: activates, marshals, then parses the blob via `ComObjRef::parse` and asserts non-zero OXID + IPID. Live verification: passes against the AVEVA install on this host. Workspace tests went 183 → was 179 in mxaccess-rpc (+4 new). Unblocks F12 (NmxClient::create) — the auto-resolving COM-activation factory can now chain `marshal_activated_iunknown_objref` → `ComObjRef::parse` → `resolve_oxid_with_managed_ntlm_packet_integrity` → `RemQueryInterface` over the existing primitives.
|
||||
|
||||
### F14 — `tiberius`-backed SQL implementation of `Resolver` + `UserResolver`
|
||||
**Resolved:** 2026-05-05 (commit `<this commit>`). New module `crates/mxaccess-galaxy/src/sql_resolver.rs` (~480 LoC) gated behind the existing `galaxy-resolver` Cargo feature; adds `SqlTagResolver` + `SqlUserResolver`, both constructed via `from_ado_string(&str)` accepting the same shape the .NET reference uses by default (`Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True`). `Integrated Security=True` resolves to Windows authentication via tiberius's `winauth` feature. Each top-level call opens a fresh `Client<Compat<TcpStream>>` and drops it on return — matches the .NET `await using` shape. `tiberius`'s `Client::query` only accepts positional `@P1..@PN` placeholders (delegates to `sp_executesql`); the canonical `RESOLVE_SQL` / `BROWSE_SQL` / `USER_BY_GUID_SQL` / `USER_BY_NAME_SQL` constants are rewritten once-per-process via `OnceLock<String>` (`@objectTagName` → `@P1`, etc.). `read_metadata` mirrors `ReadMetadata` (`cs:149-165`) byte-by-byte: signed `smallint` → `i16` widened to `u16` for platform/engine/object IDs (matches the .NET `checked((ushort)...)`), `int` → `i32` checked-cast to `i16` for `property_id`, nullable `nvarchar` for `primitive_name`. `read_user_profile` mirrors `ReadProfile` (`cs:76-85`) including the `roles_text` blob → `parse_role_blob` round-trip. New deps: `tiberius 0.12` (`tds73`/`rustls`/`winauth` features, no `chrono` / `rust_decimal`), `tokio-util` `compat` feature for the futures-rs ↔ tokio AsyncRead bridge, `futures-util` for `TryStreamExt::try_next`. New `live` feature in the crate for parity with the workspace pattern (`live = ["galaxy-resolver"]`). 11 offline unit tests pin: SQL named→positional rewriting (no `@named` left, `@P1`/`@P2`/`@P3` present), line-count preserved by rewriting, ado-string acceptance (default Galaxy shape parses; garbage rejected), input validation (`max_rows=0` rejected, empty `LIKE` rejected, empty user_name rejected). Two `#[cfg(feature = "live")]` `#[ignore]`'d tests round-trip against a real Galaxy DB (gated on `MX_LIVE` + `MX_GALAXY_DB` env vars per `tools/Setup-LiveProbeEnv.ps1`): `live_resolve_test_child_object_test_int` (TestChildObject.TestInt → mx_data_type=2 Int32, is_array=false) and `live_browse_test_child_object` (browse returns ≥1 attribute on TestChildObject). Both pass against the local AVEVA install.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user