# MXAccess reverse-engineering analysis ## Executive summary The primary `ArchestrA.MXAccess.dll` is not a protocol implementation. It is a .NET primary interop assembly generated from `LMXPROXYLib`. All useful methods on `LMXProxyServerClass` are `extern` COM calls. The actual implementation is native, 32-bit, and registered as an in-process COM server: ```text net48 x86 caller -> ArchestrA.MXAccess.dll -> COM CLSID {C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC} -> C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll -> C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.dll -> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll -> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe -> runtime engines over NMX ``` That is why the current OPC UA interface is constrained to .NET Framework and x86. The binding itself is not forcing .NET 4.8; the 32-bit COM/native stack is. The cleanest modern replacement is not "port the interop assembly"; it is either: 1. keep a 32-bit sidecar process and expose a modern IPC API to .NET 10/Rust, or 2. implement the lower LMX/NMX client protocol directly from the native type libraries plus runtime traces. Option 2 is possible but not complete from static decompilation alone because the proprietary message bodies are assembled inside native code, not in `ArchestrA.MXAccess.dll`. ## Evidence ### Managed DLL `ArchestrA.MXAccess.dll`: - File path: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll` - Assembly: `ArchestrA.MxAccess, Version=3.2.0.0, PublicKeyToken=23106a86e706d0ae` - File description/product: `Assembly imported from type library 'LMXPROXYLib'.` - Decompilation contains only COM-imported interfaces, structs, enums, delegates, and event sink helpers. - `LMXProxyServerClass` methods are all `runtime managed internalcall`, meaning the CLR dispatches them through COM; there is no managed implementation body to port. ### COM registration `LMXProxy.LMXProxyServer` is registered only in the 32-bit COM registry view: ```text HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC} (default) = LMXProxyServer Class InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll ThreadingModel = Apartment ProgID = LMXProxy.LMXProxyServer.1 VersionIndependentProgID = LMXProxy.LMXProxyServer TypeLib = {C36ECF28-3EF3-4528-9843-81D7FDC86328} ``` Type libraries: ```text {77E896EC-B3E3-4DE4-B96F-F32570164211}\3.2 LMXProxy 3.2 Type Library C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess32.tlb {C36ECF28-3EF3-4528-9843-81D7FDC86328}\1.0 LMXProxy 1.0 Type Library C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll {C36ECF28-3EF3-4528-9843-81D7FDC86328}\2.0 LMXProxy 2.0 Type Library C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess20.tlb ``` ### Native binaries | Binary | Role inferred from registration/imports/strings | Architecture | | --- | --- | --- | | `LmxProxy.dll` | Public MXAccess COM wrapper. Implements `LMXProxy.LMXProxyServer`. Imports licensing wrapper and COM/OLE Automation APIs. | PE32 x86 | | `Lmx.dll` | Main local message exchange implementation. Exposes many COM objects including `IDataClient`, `IDataConsumer`, `Nmx`, bootstrap, runtime object, and data variant surfaces. | PE32 x86 | | `NmxAdptr.dll` | In-process adapter over NMX service. Imports Winsock and exposes `WonderWare.Nmx.CNmxAdapter`. | PE32 x86 | | `NmxSvc.exe` | Out-of-process NMX service. Registered as `NmxSvc.NmxService`; listens on TCP/UDP port `5026` on this machine. | PE32 x86 | | `NmxSvcps.dll` | MIDL proxy/stub for NMX service COM interfaces. | PE32 x86 | `LmxProxy.dll` exports only standard COM DLL exports: ```text DllCanUnloadNow DllGetClassObject DllRegisterServer DllUnregisterServer ``` This means there is no flat C ABI to P/Invoke from modern .NET or Rust. `LmxProxy.dll` imports `LicAPINativeWrapper.dll`, including: ```text CreateClientConnection AddLicenseRequestInfo AcquireLicense GetLicenseAcquisitionError ReleaseLicense GetDeviceIdentity ``` Any native replacement has to account for AVEVA licensing behavior, either by using a supported client surface or by keeping a licensed sidecar. ## Runtime model inferred from MXAccess The current high-level API is handle based: 1. `Register(clientName)` returns an `hLMXServerHandle`. 2. `AddItem(handle, fullReference)` resolves a Galaxy/runtime attribute and returns an item handle. 3. `Advise` or `AdviseSupervisory` subscribes to updates. 4. Updates arrive through COM connection-point events: `OnDataChange`, `OnWriteComplete`, and `OperationComplete`. 5. `Write`/`Write2` submit writes and completion is reported asynchronously. 6. `UnAdvise`, `RemoveItem`, and `Unregister` clean up. The existing OPC UA host uses the canonical pattern correctly: - All COM work runs on one STA thread. - Reads are implemented as transient subscribe -> first `OnDataChange` -> unsubscribe because MXAccess exposes no direct synchronous read. - Writes are submitted with `Write` and then matched with `OnWriteComplete`. - Subscriptions use `AdviseSupervisory`. ## Lower LMX data-client surface `Lmx.dll` exposes a richer COM interface named `IDataClient`. It is still x86 COM, but it is closer to the underlying data model than `LMXProxyServer`: ```csharp void Initialize(string namespaceName); IntPtr Connect(string endPointUri, ulong timeout, ref _IUserToken userToken, out uint clientId); IArchestrAResult[] Connect2(...); long CreateSubscription2(...); IArchestrAResult[] RegisterItems2(IItemIdentity2[] items, ...); IArchestrAResult[] AddMonitoredItems2(long subscriptionId, IMonitoredItem2[] items, ...); IArchestrAResult[] Publish2(long subscriptionId, out DataChangeUpdate[] updates, out uint resultCount); IArchestrAResult[] Read2(IItemIdentity2[] items, out DataChangeUpdate[] updates, out uint updatesCount); IArchestrAResult[] Write2(WriteRequest2[] writes, ...); ``` Important structs: ```csharp struct IItemIdentity2 { ushort ReferenceType; ushort type; string ContextName; string Name; ulong id; } struct IMonitoredItem2 { byte Active; ushort ReferenceType; ushort type; string ContextName; string Name; ulong id; ulong SampleInterval; ulong TimeDeadband; } struct DataChangeUpdate { ulong ItemId; int StatusCode; uint HighDateTime; uint LowDateTime; int quality; IDataVariant value; } struct IDataVariant { ushort type; int length; byte[] Payload; } ``` This surface suggests AVEVA has a newer OPC UA-like internal data client in the LMX layer. It may be a better reverse-engineering target than `LMXProxyServer`, but it is not registered as a 64-bit component on this machine. It still goes through the x86 native stack. ## NMX service surface `NmxSvc.exe` is registered as an out-of-process COM local server: ```text CLSID {AE24BD51-2E80-44CC-905B-E5446C942BEB} LocalServer32 = "C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe" ProgID = NmxSvc.NmxService.1 ``` Key `INmxService2` methods: ```csharp void RegisterEngine2(int localEngineId, string engineName, int version, INmxSvcCallback callback); void UnRegisterEngine(int localEngineId); void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId); void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte msgBody); void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId); void RemoveSubscriberEngine(...); void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks); void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version); ``` `NmxAdptr.dll` exposes `WonderWare.Nmx.CNmxAdapter` as an in-process COM adapter: ```text CLSID {42DB0511-28BE-11D3-80C0-00104B5F96A7} InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll ProgID = WonderWare.Nmx.CNmxAdapter.1 ThreadingModel = Apartment ``` Key `INmx4` methods: ```csharp void Initialize2(int platformId, int engineId, int version, int queueExtent); void InitializeAnonymous2(out int platformId, out int engineId, int version); void PutRequest2(int galaxyId, int platformId, int engineId, byte priority, byte type, int size, ref byte data, out int requestHandle); void GetResponse2(byte type, out int responseCode, out int requestHandle, out int size, IntPtr data); void PutMessageNoReply(...); void GetPartnerVersion(...); ``` Static strings identify NMX message categories: ```text NMXMTYPE_CONNECT_INFO NMXMTYPE_CONNECT_INFO_REQ NMXMTYPE_ENGINE_DATA NMXMTYPE_GET_HEARTBEAT_RATE NMXMTYPE_PLATFORM_HEARTBEAT NMXMTYPE_SET_HEARTBEAT_RATE NMXMTYPE_VERSION_ERROR NMXMTYPE_HEARTBEAT_DISCONNECT_REQ ``` On this machine, the running `NmxSvc.exe` process is listening on: ```text TCP 10.100.0.48:5026 UDP 10.100.0.48:5026 ``` That gives a network tracing target for the direct-native path. ## Why direct x64 COM is blocked There are two different issues: 1. `ArchestrA.MXAccess.dll` is a .NET Framework-era interop assembly. Modern .NET can consume many COM interfaces, but the generated event provider and TLB-imported types are built for the old COM interop shape. 2. The actual COM class is an x86 in-process DLL. A 64-bit process cannot load `LmxProxy.dll`, regardless of whether the caller is .NET Framework, .NET 10, or Rust. An out-of-process COM local server can cross bitness, but `LMXProxyServer` is not registered as a local server. It is an in-proc DLL. ## Implementation options ### Option A: Keep and formalize the 32-bit sidecar This is the shortest reliable path. Build or keep a small x86 process that owns: - COM apartment initialization. - `LMXProxy.LMXProxyServer` lifetime. - register/add/advise/write/unadvise cleanup. - reconnect and subscription replay. Expose a stable IPC API to the modern service: - named pipes for same-machine .NET/Rust services, - gRPC over named pipes or loopback TCP if cross-language tooling matters, - protobuf messages for `Read`, `Write`, `Subscribe`, `Unsubscribe`, and status events. This is effectively what the existing `OtOpcUaGalaxyHost` / proxy work has moved toward. It removes the OPC UA server's net48/x86 constraint without attempting to clone AVEVA's internal protocol. ### Option B: Use lower LMX COM interfaces from an x86 sidecar Instead of the high-level `LMXProxyServer`, use `Lmx.dll`'s `IDataClient` / `IDataConsumer` APIs inside the sidecar. Potential benefits: - direct `Read2` API rather than transient subscribe for reads, - batch register/read/write/monitor operations, - explicit `IDataVariant` payloads, - closer alignment with OPC UA-like status codes. Risks: - less documented than MXAccess, - still x86 COM, - endpoint URI, namespace, token setup, and licensing behavior need live probing. ### Option C: Direct NMX protocol client in Rust or .NET This is the true native replacement path. Required work: 1. Discover galaxy/platform/engine ids for target attributes. 2. Reconstruct NMX message envelope layout used by `PutRequest2` and `NmxSvc.TransferData`. 3. Reconstruct LMX item registration, subscription, write, and completion message bodies. 4. Reproduce heartbeat/version negotiation. 5. Reproduce security/licensing behavior or find a supported token flow. 6. Validate behavior across platform stopped/off-scan, engine restart, deploy, security-denied write, and repository-busy cases. Static type-library analysis gives method names and high-level message routes, but not the binary message schemas. Those schemas are assembled in native code. The next step for this option is live tracing. ## Proposed tracing plan 1. Build a minimal x86 MXAccess harness that: - registers as a known client name, - adds one known good tag and one bad tag, - subscribes with `AdviseSupervisory`, - writes a toggled value, - unregisters cleanly. 2. Capture process activity: - Process Monitor filters for `LmxProxy.dll`, `Lmx.dll`, `NmxAdptr.dll`, `NmxSvc.exe`. - ETW or API Monitor for COM method calls if available. - Wireshark or `netsh trace` on TCP/UDP `5026`. 3. Correlate high-level actions to low-level packets: - `Register` - `AddItem` - `AdviseSupervisory` - first data change - `Write` - `OnWriteComplete` - `UnAdvise` / `RemoveItem` 4. Repeat against: - local object attribute, - remote platform attribute, - stopped engine, - security-denied write, - secured/verified write. 5. Use `NmxAdptr.INmx4` as a stepping stone: - call `InitializeAnonymous2`, - issue controlled `PutRequest2` from an x86 harness, - compare buffers and responses with MXAccess traffic. ## Current recommendation For production, do not block the OPC UA server on a full native NMX clone. The low-risk path is: 1. keep the x86 MXAccess host as the vendor-bound adapter, 2. make the main OPC UA interface .NET 10 or Rust, 3. communicate through a narrow, well-tested IPC contract, 4. continue reverse-engineering `IDataClient` and NMX in parallel as an R&D path. The direct native path may still be worth pursuing, but it should be treated as a protocol-reconstruction project with packet captures and failure-mode testing, not as a decompile-only port. ## Artifacts produced Generated files in this project folder: ```text analysis/decompiled-mxaccess/ analysis/interop/ analysis/decompiled-interop/ ``` Installed tools used: ```text dotnet tool install --global ilspycmd python pefile Windows SDK TlbImp.exe ```