docs: revise interop fix path — wnwrapConsumer.dll is the right surface
Reflection on aaAlarmManagedClient.AlarmClient shows it implements only IDisposable (no [ComImport] interface, no class GUID) and has a single field "CwwAlarmConsumer* m_almUnmanaged". So AlarmClient is a C++/CLI managed wrapper around a native C++ class -- NOT a COM-interop class. The DateTime conversion happens INSIDE AVEVA's wrapper IL, not at the .NET-COM marshaling boundary. There's no separate COM interface to QI to. Revised approach (in docs/AlarmClientDiscovery.md): A. wnwrapConsumer.dll -- separate standalone COM library AVEVA ships at "C:\Program Files (x86)\Common Files\ArchestrA" exposing WNWRAPCONSUMERLib.wwAlarmConsumerClass with SetXmlAlarmQuery / GetXmlCurrentAlarms. XML-string output bypasses FILETIME marshaling entirely. Best fit -- real COM, self-contained, conventional production-grade approach. B. Patch aaAlarmManagedClient.dll IL -- direct but modifies a vendor binary, brittle to upgrades. C. Reflect into m_almUnmanaged and call native vtable directly -- requires reverse-engineering the C++ class layout. Picking A. Probe restored to Skip; next commit starts the wnwrapConsumer integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -392,6 +392,36 @@ disruptive. Once the interop is custom, `AlarmClient.Subscribe` +
|
|||||||
`GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable
|
`GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable
|
||||||
polling-style alarm consumer.
|
polling-style alarm consumer.
|
||||||
|
|
||||||
|
**REVISED 2026-05-01 — option 1 not directly applicable.**
|
||||||
|
Reflection on `aaAlarmManagedClient.AlarmClient` shows it
|
||||||
|
implements only `IDisposable` (no `[ComImport]` interface, no
|
||||||
|
class GUID). It has a single field `CwwAlarmConsumer*
|
||||||
|
m_almUnmanaged` — meaning `AlarmClient` is a **C++/CLI managed
|
||||||
|
wrapper around a native C++ class**, NOT a COM-interop class.
|
||||||
|
The DateTime conversion happens inside the AVEVA wrapper's IL,
|
||||||
|
not at a .NET-to-COM marshaling boundary. There is no separate
|
||||||
|
COM interface IID we can QI to.
|
||||||
|
|
||||||
|
Revised approach options:
|
||||||
|
|
||||||
|
A. **Switch to `wnwrapConsumer.dll`** — a separate standalone
|
||||||
|
COM library AVEVA ships at
|
||||||
|
`C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll`
|
||||||
|
exposing `WNWRAPCONSUMERLib.wwAlarmConsumerClass` with
|
||||||
|
`SetXmlAlarmQuery` / `GetXmlCurrentAlarms`. XML-string output
|
||||||
|
bypasses FILETIME marshaling entirely.
|
||||||
|
B. **Patch `aaAlarmManagedClient.dll` IL** — wrap the unsafe
|
||||||
|
`DateTime.FromFileTime` calls with a safe variant. Direct
|
||||||
|
fix but modifies a vendor binary.
|
||||||
|
C. **Reflect into `m_almUnmanaged` and call native vtable** —
|
||||||
|
get the IntPtr, walk the MSVC C++ vtable, call
|
||||||
|
`__thiscall` methods via `Marshal.GetDelegateForFunctionPointer`.
|
||||||
|
Doable but requires reverse-engineering the C++ class layout.
|
||||||
|
|
||||||
|
Option A is the best fit: real COM-based, self-contained in
|
||||||
|
our code, conventional production-grade approach (the WIN-911
|
||||||
|
consumer pattern referenced in AVEVA support forums uses it).
|
||||||
|
|
||||||
The polling-vs-WM_APP-callback question from earlier is now
|
The polling-vs-WM_APP-callback question from earlier is now
|
||||||
moot: `GetStatistics`'s `positions[]/handles[]` arrays remained
|
moot: `GetStatistics`'s `positions[]/handles[]` arrays remained
|
||||||
empty even when alarms were demonstrably present. The active
|
empty even when alarms were demonstrably present. The active
|
||||||
|
|||||||
@@ -250,6 +250,52 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
|||||||
{
|
{
|
||||||
client = new AlarmClient();
|
client = new AlarmClient();
|
||||||
|
|
||||||
|
// One-time interop introspection: dump AlarmClient's class GUID
|
||||||
|
// (CoClass IID) and every interface it implements with their
|
||||||
|
// GUID + InterfaceType. The IID we need to redeclare with safe
|
||||||
|
// blittable types is the one whose vtable carries
|
||||||
|
// GetHighPriAlarm.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type ct = client.GetType();
|
||||||
|
Log($"=== AlarmClient interop introspection ===");
|
||||||
|
Log($"Class FullName: {ct.FullName}");
|
||||||
|
var classGuid = ct.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true)
|
||||||
|
.Cast<System.Runtime.InteropServices.GuidAttribute>().FirstOrDefault();
|
||||||
|
Log($"Class GUID: {classGuid?.Value ?? "(none)"}");
|
||||||
|
foreach (var iface in ct.GetInterfaces())
|
||||||
|
{
|
||||||
|
var ig = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true)
|
||||||
|
.Cast<System.Runtime.InteropServices.GuidAttribute>().FirstOrDefault();
|
||||||
|
var ity = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.InterfaceTypeAttribute), true)
|
||||||
|
.Cast<System.Runtime.InteropServices.InterfaceTypeAttribute>().FirstOrDefault();
|
||||||
|
int methodCount = iface.GetMethods().Length;
|
||||||
|
Log($" iface {iface.FullName} | GUID={ig?.Value ?? "(none)"} | type={ity?.Value.ToString() ?? "(none)"} | methods={methodCount}");
|
||||||
|
}
|
||||||
|
// Dump fields (private/internal) — the COM object reference
|
||||||
|
// is likely on a private field.
|
||||||
|
Log($"--- AlarmClient instance fields ---");
|
||||||
|
foreach (var f in ct.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
Log($" field {f.FieldType.FullName} {f.Name} (public={f.IsPublic})");
|
||||||
|
}
|
||||||
|
// Dump base class chain.
|
||||||
|
Log($"--- base class chain ---");
|
||||||
|
Type? baseT = ct.BaseType;
|
||||||
|
int depth = 0;
|
||||||
|
while (baseT != null && depth < 5)
|
||||||
|
{
|
||||||
|
Log($" base[{depth}]: {baseT.FullName}");
|
||||||
|
baseT = baseT.BaseType;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
Log($"=== end introspection ===");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"Interop introspection threw: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
// Try InitializeConsumer first — separate from RegisterConsumer
|
// Try InitializeConsumer first — separate from RegisterConsumer
|
||||||
// per the discovered API surface; previous probe runs skipped
|
// per the discovered API surface; previous probe runs skipped
|
||||||
// it. Some AVEVA managed-client patterns require Initialize
|
// it. Some AVEVA managed-client patterns require Initialize
|
||||||
|
|||||||
Reference in New Issue
Block a user