# NMX COM contracts and managed-client implications This note captures the COM/type-library layer that sits below MXAccess and above the local NMX transport. ## Native binaries inspected Primary installed files: ```text C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvcps.dll C:\Program Files (x86)\ArchestrA\Framework\Bin\WWProxyStub.dll ``` No x64 equivalents were found under `C:\Program Files`; only x86 installed copies and Galaxy file-repository copies were present. Generated inspection reports: ```text analysis\native\LmxProxy.dll.md analysis\native\NmxAdptr.dll.md analysis\native\NmxSvcps.dll.md analysis\native\WWProxyStub.dll.md ``` `NmxSvcps.dll` is a MIDL COM proxy/stub DLL. It exports: ```text DllCanUnloadNow DllGetClassObject DllRegisterServer DllUnregisterServer ``` It imports the expected RPC proxy/stub helpers from `RPCRT4.dll`: ```text NdrDllGetClassObject NdrDllRegisterProxy NdrDllUnregisterProxy CStdStubBuffer_* IUnknown_*_Proxy ``` ## Key NMX service contracts The already decompiled interop tree contains the type-library contracts: ```text analysis\decompiled-interop\Interop.NmxSvc analysis\decompiled-interop\Interop.NmxAdptr ``` Important interfaces: | Interface | GUID | Important methods | | --- | --- | --- | | `INmxService` | `575008DB-845D-46C6-A906-F6F8CA86F315` | `RegisterEngine`, `UnRegisterEngine`, `Connect`, `TransferData`, subscriber and heartbeat methods | | `INmxService2` | `2630A513-A974-4B1A-8025-457A9A7C56B8` | `RegisterEngine2`, `GetPartnerVersion` | | `INmxSvcCallback` | `B49F92F7-C748-4169-8ECA-A0670B012746` | `DataReceived`, `StatusReceived` | | `INmxNotify` | `73849AEA-472A-4715-B8C6-1C806AF12DFC` | `ConnectionEstablished`, `ConnectionClosed` | | `INmx4` | `84168012-B544-4217-A145-32819C607435` | `PutRequest2`, `GetResponse2`, `Initialize2`, `InitializeAnonymous2` | Core transport methods: ```csharp void INmxService.TransferData( int lRemoteGalaxyID, int lRemotePlatformID, int lRemoteEngineID, int lSize, ref byte pMsgBody); void INmxSvcCallback.DataReceived( int dwBufferSize, ref sbyte lpDataBuffer); void INmxSvcCallback.StatusReceived( int dwBufferSize, ref sbyte lpStatusBuffer); void INmx4.PutRequest2( int dwClusterId, int dwPlatformId, int dwEngineId, byte byPriority, byte byType, int dwSize, ref byte pData, out int pdwRequestHandle); void INmx4.GetResponse2( byte byType, out int pdwResponseCode, out int pdwRequestHandle, out int pdwSize, IntPtr pData); ``` ## Local mixed stream The localhost `127.0.0.1:57415 <-> 127.0.0.1:57433` stream is not plain DCE/RPC. It is a compact mixed protocol: - 12-byte control records: `int32 code_or_status`, `int32 token_low`, `int32 token_high`. - Data records: `uint32 body_length`, followed by `body_length` bytes. - A positive control `code_or_status` often announces the total byte count of one or more following data records. - `-1` appears as a normal acknowledgement/status control. - `-2` appears as a bidirectional status/control marker around write windows. The parser for this stream is: ```text analysis\scripts\decode_mixed_local_stream.py ``` Generated mixed-stream decodes: ```text captures\016-loopback-write-test-int-advised\mixed-stream-57415-to-57433.tsv captures\016-loopback-write-test-int-advised\mixed-stream-57433-to-57415.tsv captures\017-loopback-write-test-int-100\mixed-stream-57415-to-57433.tsv captures\017-loopback-write-test-int-100\mixed-stream-57433-to-57415.tsv captures\020-loopback-write-test-int-102\mixed-stream-57415-to-57433.tsv captures\020-loopback-write-test-int-102\mixed-stream-57433-to-57415.tsv ``` Write-window extraction and diff helpers: ```text analysis\scripts\analyze_write_window.py analysis\scripts\diff_write_window_records.py ``` ## Controlled write captures Usable value-change captures: | Folder | Write path | | --- | --- | | `captures\017-loopback-write-test-int-100` | `TestInt` changed from `99` to `100` | | `captures\020-loopback-write-test-int-102` | `TestInt` changed from `101` to `102` | | `captures\021-loopback-write-test-int-sequence-103-105` | `TestInt` changed from `102` to `103`, `104`, then `105` in one session | `captures\018-loopback-write-test-int-101` has a successful harness log but a header-only pcap, so it should not be used for packet analysis. The rerun `captures\019-loopback-write-test-int-101-rerun` captured correctly, but it was a same-value write because the attribute was already `101`. The same-session sequence shows that the decoded write-window records do not carry the requested `int32` values as plain little-endian scalar payloads. The visible moving fields are mostly local sequence tokens and opaque body fields. For a managed implementation, this raises the priority of tracing the in-process native API boundary before the payload enters the localhost transport. That boundary has now been traced with headless Ghidra-derived Frida hooks. See: ```text docs\Ghidra-Headless-Analysis.md captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv ``` The key result is that `CLMXProxyServer::Write` receives the raw `int32` scalar directly, and `CNmxAdapter::PutRequest` receives a 40-byte body with the scalar at offset `18`. `CNmxAdapter::TransferData` wraps that body in an 86-byte message, placing the scalar at offset `64`. The corresponding `ProcessDataReceived` update body carries the scalar at offset `84`. Additional Frida captures generalized the write body across common scalar types: | Type | `PutRequest` body | `TransferData` body | Callback/update body | Encoding | | --- | --- | --- | --- | --- | | bool | size `37`, value offset `18` | size `83`, value offset `64` | size `85`, value offset `84` | `VT_BOOL`; true `ff ff ff 00` in write body and `ff` in data-change body; false `00 ff ff 00` and `00` | | int | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `int32` | | float | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `float32` | | double | size `44`, value offset `18` | size `90`, value offset `64` | size `92`, value offset `84` | little-endian `float64` | | string | size `58` or `60`, value offset `26` | size `104` or `106`, value offset `72` | size `106` or `108`, value offset `92` | UTF-16LE | | datetime | size `86`, value offset `26` | size `132`, value offset `72` | size `98`, value offset `88` | outbound UTF-16LE display string; callback/update FILETIME | The matrix is saved at: ```text analysis\frida\write-body-matrix.tsv ``` Array writes are also captured: ```text analysis\frida\write-array-body-matrix.tsv ``` Write-mode captures are saved at: ```text analysis\frida\write-mode-matrix.tsv ``` The first combined Frida plus loopback correlation is: ```text captures\043-frida-loopback-write-test-int-115 captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv captures\044-frida-loopback-write-test-int-123456789 captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv ``` Numeric arrays use an array descriptor at body offset `17`, then packed values at offset `28`. The descriptor is: ```text kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32 ``` Observed kind bytes are `0x41` bool, `0x42` int, `0x43` float, `0x44` double, and `0x45` variable-width string/date. String and datetime arrays use per-element variable records; outbound datetime array writes encode display strings, while callback/update bodies encode FILETIME values. The bool-array capture succeeded but did not preserve the requested alternating pattern, so that path is documented as unresolved pending a targeted follow-up. Secured and verified attributes did not require the COM `WriteSecured` methods. Those methods returned before value-bearing NMX requests in the tested cases. The supported public path was normal `Write` with the fourth argument set to the Galaxy security classification (`2` for `SecuredWrite`, `3` for `VerifiedWrite`). Timestamped `Write2` keeps the scalar value slot and embeds a FILETIME after the value. The adapter body is still not the wire format. Capture `043` proves that exact `PutRequest`, `TransferData`, and callback bodies are absent from the reassembled TCP streams. Capture `044` uses the distinctive value `123456789` and shows the raw scalar is absent from the full pcap payload scan, parsed DCE/RPC stubs, and mixed local stream. A native managed client therefore needs a structural DCE/RPC/NDR decoder in addition to the adapter-body codec. The initial managed codec is: ```text src\MxNativeCodec\MxNativeCodec.csproj src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj ``` It does not yet synthesize every unknown header field. It uses captured `PutRequest` bodies as templates and proves that scalar typed value slots, timestamped scalar bodies, packed numeric arrays, string-array records, and the write index can be decoded and re-encoded in .NET 10 x64 managed code. ## Implication for .NET 10 x64 The public COM contracts are useful as a schema, but the installed runtime path is x86. A full managed .NET 10 x64 implementation cannot simply load these in-proc COM components. The service boundary was traced directly in capture `046`: ```text captures\046-service-boundary-write-test-int-123456791 ``` The same 86-byte write body appears at: ```text CNmxAdapter.TransferData CNmxService.TransferData CNmxControler.TransferData CNmxControler.DataReceived CNmxControler.ProcessDataReceivedForEngine ``` The distinctive scalar `123456791` is at body offset `64` in every one of those 86-byte bodies. The `TransferData` service body is a 46-byte NMX envelope followed by the adapter `PutRequest` body. The envelope stores the inner body length at offset `2`. For the observed `TestInt=123456791` write: Do not send a bare 46-byte envelope as a normal probe payload. If the adapter receives a header-only or length-mismatched `TransferData` body, System Platform can log `NMX Header ... buffer size pktHeader.dwDataSize ... doesn't match received message size ...`. The local COM and managed DCE/RPC harnesses now validate this envelope length before sending. | Body | Size | Value offset | | --- | ---: | ---: | | `CNmxAdapter.PutRequest` | 40 | 18 | | `INmxService2.TransferData` / `CNmxAdapter.TransferData` | 86 | 64 | The managed codec now includes `NmxTransferEnvelopeTemplate` to decode and re-encode this observed envelope form. The .NET 10 x64 probe can activate `NmxSvc.NmxService`, but the first `INmxService2` method call fails with `0x8007000B` because COM tries to load the 32-bit `NmxSvcps.dll` proxy/stub into the x64 process. The probe is: ```text src\MxNativeClient.Probe ``` The same probe can marshal the activated object as remote `IUnknown` and dump a standard OBJREF: ```text analysis\proxy\nmxservice-objref-context2.txt ``` The OBJREF exposes a stable OXID plus per-activation OID/IPID values and binding towers. This proves a managed implementation can obtain the object identity without loading `NmxSvcps.dll`; the remaining step is to implement the ORPC `QueryInterface` and method call flow manually. The .NET 10 client scaffold now has the first managed protocol primitives: ```text src\MxNativeClient\DceRpcPdu.cs src\MxNativeClient\DceRpcTcpClient.cs src\MxNativeClient\OrpcStructures.cs src\MxNativeClient\RemUnknownMessages.cs src\MxNativeClient\ObjectExporterMessages.cs src\MxNativeClient\ObjectExporterClient.cs src\MxNativeClient.Tests\MxNativeClient.Tests.csproj ``` Those tests parse and re-encode real bind/alter-context style traffic from capture `046` and parse a captured request PDU. The capture parser now records presentation-context UUIDs in `dcerpc-stream-pdus.tsv`, which makes it easier to separate useful DCE/RPC structure from traffic that is unrelated to the NMX service body. The visible DCE/RPC stream does not begin request stubs with `ORPCTHIS`, so it is not the direct `INmxService2` ORPC method channel. The managed ORPC work is therefore being built from the DCOM specification plus the OBJREF returned by local COM activation. `IRemUnknown::RemQueryInterface` composition is now represented in code; a live RPC binding and response validation remain pending. The first live managed RPC probe binds to `IObjectExporter` on RPCSS and sends `ResolveOxid` for the OBJREF OXID. The request is accepted, but the unauthenticated call returns `0x00000005` (`ERROR_ACCESS_DENIED`). That matches the Python and Impacket reference behavior, so the next blocker is RPC authentication rather than the OXID NDR layout. The DCOM activation and scalar call path has now been proven with an Impacket reference probe: ```text analysis\scripts\probe_dcom_inmxservice2.py analysis\proxy\dcom-inmxservice2-getpartner-probe.txt ``` That probe uses packet privacy, activates CLSID `{AE24BD51-2E80-44CC-905B-E5446C942BEB}`, requests `INmxService2` `{2630A513-A974-4B1A-8025-457A9A7C56B8}`, binds to the returned OXID endpoint, and calls `GetPartnerVersion` opnum `11`. The service returns `partner_version=6` and `ErrorCode=0x00000000`. This changes the remaining work from "prove DCOM can reach NmxSvc without the x86 proxy" to "port packet-private DCOM authentication and the decoded NDR method stubs into the .NET 10 client." The public method schema, target CLSID, service endpoint, object identity flow, and at least one working service method are now verified. The .NET 10 managed client now reproduces the critical scalar path without the AVEVA x86 proxy and without using SSPI `MakeSignature`: ```text analysis\proxy\managed-remqi-and-getpartner-probe.txt src\MxNativeClient\ManagedNtlmClientContext.cs src\MxNativeClient\DceRpcTcpClient.cs src\MxNativeClient\NmxService2Messages.cs ``` The managed probe obtains an `IUnknown` OBJREF, resolves the OXID with a managed NTLMv2 packet-integrity DCE/RPC call, calls `IRemUnknown::RemQueryInterface` for `INmxService2`, and then invokes `INmxService2::GetPartnerVersion`. The live result is `managed_getpartner_version=6` and `managed_getpartner_hresult=0x00000000`. The remaining client-side proxy work is now concentrated on the non-scalar COM methods: marshaling a managed callback object for `RegisterEngine2`, encoding the correlated `byte[size]` parameter for `TransferData`, and implementing the callback endpoint that receives `DataReceived` and `StatusReceived`. `TransferData` has since been encoded and live-probed: ```text analysis\proxy\managed-transferdata-control-probe.txt ``` The service returned `0x80041101`, which is an application-level HRESULT after the ORPC call reached `NmxSvc.exe`; it was not a DCE/RPC/NDR failure. That confirms the `byte[size]` request shape. The x64 callback marshal probe is: ```text analysis\proxy\callback-marshal-probe.txt ``` It fails with `0x80040154 REGDB_E_CLASSNOTREG` when trying to marshal `INmxSvcCallback`. This is the same architecture problem in reverse: the client-side x64 process has no registered `NmxSvcps.dll` proxy/stub for the callback IID. A full managed client therefore needs to export the callback object itself over DCE/RPC/ORPC. Therefore the viable managed path is to implement the observed local contracts directly: 1. Recreate enough of the NMX service/session behavior represented by `INmxService`, `INmxSvcCallback`, and `INmx4`. 2. Encode/decode the NMX adapter message bodies identified by the Frida trace. 3. Replace the 32-bit `NmxSvcps.dll` MIDL proxy with a managed NDR/DCOM proxy for `NmxSvc.exe`. 4. Use the Galaxy repository for tag/type/security metadata rather than depending on the x86 MXAccess wrapper. The next hard blocker is no longer the basic `int32` value location. It is the managed replacement for the MIDL proxy/stub plus synthesis of add-item, advise/unadvise, remove-item, and status/error request bodies. See also: ```text docs\DotNet10-Native-Library-Plan.md analysis\proxy\nmxsvcps-proxy-layout.tsv analysis\proxy\nmxsvcps-procedures.tsv ``` ## Decoded proxy/stub procedure table The proxy/stub MIDL procedure bytecode is extracted by: ```text analysis\scripts\extract_nmxsvcps_proc_formats.py ``` The output is: ```text analysis\proxy\nmxsvcps-procedures.tsv analysis\proxy\type-format-snippets\ ``` The core service opnums recovered from `NmxSvcps.dll` are: | Interface | Method | Opnum | Parameter shape | | --- | --- | ---: | --- | | `INmxService2` | `RegisterEngine` | 3 | `int`, `BSTR`, `INmxSvcCallback*`, `HRESULT` | | `INmxService2` | `UnRegisterEngine` | 4 | `int`, `HRESULT` | | `INmxService2` | `Connect` | 5 | `int`, `int`, `int`, `int`, `HRESULT` | | `INmxService2` | `TransferData` | 6 | `int`, `int`, `int`, `int size`, `byte[size]`, `HRESULT` | | `INmxService2` | `AddSubscriberEngine` | 7 | `int`, `int`, `int`, `int`, `HRESULT` | | `INmxService2` | `RemoveSubscriberEngine` | 8 | `int`, `int`, `int`, `int`, `HRESULT` | | `INmxService2` | `SetHeartbeatSendInterval` | 9 | `int`, `int`, `HRESULT` | | `INmxService2` | `RegisterEngine2` | 10 | `int`, `BSTR`, `int version`, `INmxSvcCallback*`, `HRESULT` | | `INmxService2` | `GetPartnerVersion` | 11 | `int`, `int`, `int`, `out int`, `HRESULT` | | `INmxSvcCallback` | `DataReceived` | 3 | `int size`, `sbyte[size]`, `HRESULT` | | `INmxSvcCallback` | `StatusReceived` | 4 | `int size`, `sbyte[size]`, `HRESULT` | Important NDR type-format offsets: | Offset | Usage | | --- | --- | | `0x0006` | callback byte array correlated to `dwBufferSize` | | `0x002c` | `BSTR_UserMarshal` string | | `0x0036` | `INmxSvcCallback` interface pointer | | `0x004c` | `TransferData` byte array correlated to `lSize` | | `0x005c` | `INmxNotify` interface pointer | This confirms that the missing x64/full-managed layer is not the public method schema. The remaining native dependency is the DCOM/ORPC transport and the NDR interpreter behavior normally provided by `NmxSvcps.dll`. ## RegisterEngine2 marshaling findings The direct x86 COM harness is: ```text src\NmxComHarness\NmxComHarness.csproj src\NmxComHarness\Program.cs ``` It bypasses `ArchestrA.MXAccess.dll` and invokes `NmxSvc.NmxService.RegisterEngine2` directly through the installed 32-bit `NmxSvcps.dll` proxy. The focused Frida hook is: ```text analysis\frida\nmx-com-proxy-trace.js ``` Captured runs: ```text captures\052-frida-direct-nmx-registerengine2-marshals-retry captures\053-frida-direct-nmx-registerengine2-null-stub captures\054-frida-direct-nmx-registerengine2-callback-stub analysis\proxy\x86-callback-objref-probe.txt analysis\proxy\x86-registerengine2-null-callback-probe.txt analysis\proxy\managed-registerengine2-null-callback-probe.txt ``` `BSTR_UserMarshal` writes the string as: ```text char_count:uint32 byte_length:uint32 char_count:uint32 utf16_payload_without_null ``` For `NmxComProxyWire5`, the exact captured bytes were: ```text 10 00 00 00 20 00 00 00 10 00 00 00 4e 00 6d 00 78 00 43 00 6f 00 6d 00 50 00 72 00 6f 00 78 00 79 00 57 00 69 00 72 00 65 00 35 00 ``` The generated proxy also writes a 4-byte user-marshal marker before that BSTR payload: ```text 55 73 65 72 ``` Interpreted little-endian, that value is `0x72657355` (`"User"`). The managed encoder in `src\MxNativeClient\NmxService2Messages.cs` now reproduces the null-callback `RegisterEngine2` request: ```text ORPCTHIS localEngineId:int32 0x72657355:uint32 BSTR_UserMarshal(engineName) padding to 4-byte boundary version:int32 callback:null-interface-pointer:uint32 = 0 ``` The live .NET 10 x64 probe reaches `NmxSvc.exe` and returns a non-failing COM success code: ```text managed_register2_null_hresult=0x00000001 managed_unregister_after_register_hresult=0x00000001 ``` This confirms that the managed DCOM/NDR path can perform the service registration lifecycle without the x86 proxy when no callback endpoint is required. For a non-null callback, the x86 proxy wraps the callback OBJREF as: ```text 0x00020000:uint32 objref_size:uint32 objref_size:uint32 objref_bytes ``` The same capture showed `objref_size=0x44` for a compact same-machine standard OBJREF. A separately marshaled x86 callback stream produced a 366-byte standard OBJREF with dual-string bindings. The managed callback exporter can therefore use the same MInterfacePointer wrapper around an OBJREF that advertises the managed callback endpoint. ## Callback OBJREF experiments Two managed callback OBJREF strategies have now been tested. ### Synthetic managed TCP OBJREF `ManagedCallbackExporter` can build a standard OBJREF that advertises a managed TCP listener: ```text src\MxNativeClient\ManagedCallbackExporter.cs analysis\proxy\managed-registerengine2-callback-probe.txt analysis\proxy\managed-registerengine2-callback-loopback-probe.txt analysis\proxy\managed-registerengine2-callback-fixed-port-probe.txt analysis\proxy\managed-callback-fixed-port-tcp-poll.txt analysis\proxy\managed-callback-nmxsvc-tcp-poll.txt ``` Without security bindings the service rejects the callback OBJREF with `0x8001011D`. Adding the default security binding sequence seen in x86 `CoMarshalInterface` changes the failure to `0x800706BA` (`RPC server unavailable`), but the managed listener sees no inbound connection. TCP polling also shows no SYN to the advertised port. Inference: for standard OBJREFs, COM is not treating the embedded string binding as a direct object endpoint. It is resolving the OXID through the local COM/OXID resolver machinery. A purely synthetic OXID that is not registered with RPCSS is not enough. ### COM-registered IUnknown OBJREF patched to callback IID The probe can also ask the local x64 COM runtime to marshal a managed `IUnknown` OBJREF, then replace only the OBJREF IID with `INmxSvcCallback`: ```text analysis\proxy\managed-registerengine2-callback-com-iunknown-objref-probe.txt analysis\proxy\managed-registerengine2-callback-com-iunknown-self-transfer-probe.txt ``` That OBJREF is backed by a real RPCSS-registered OXID/OID/IPID. With this form, `NmxSvc.exe` accepts a non-null callback pointer: ```text managed_register2_callback_hresult=0x00000000 managed_unregister_after_callback_register_hresult=0x00000000 ``` A self-directed `TransferData` probe also returns `0x00000000`: ```text managed_callback_self_transfer_hresult=0x00000000 ``` No managed callback event was delivered during that self-transfer probe. This means the COM-registered patched OBJREF is currently a registration proof, not the final callback solution. The final managed implementation still needs an endpoint whose OXID/IPID can be resolved by COM and whose request dispatch is handled by managed code rather than the missing x64 `NmxSvcps.dll` proxy/stub. ### x64 type-library marshaling for INmxSvcCallback The x64 callback marshal failure can be removed without AVEVA's 32-bit `NmxSvcps.dll` by registering type-library metadata for the callback interface and using the Windows standard automation proxy/stub: ```text analysis\scripts\register_x64_callback_typelib.ps1 analysis\proxy\typelib\NmxComHarness.tlb analysis\proxy\callback-marshal-after-typelib-probe.txt ``` The script exports `src\NmxComHarness\bin\Release\net481\NmxComHarness.exe` to a TLB, registers it with `LoadTypeLibEx(REGKIND_REGISTER)`, and sets: ```text HKLM\SOFTWARE\Classes\Interface\{B49F92F7-C748-4169-8ECA-A0670B012746} ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046} TypeLib = {4DBF23F3-069E-3D29-B67F-4C7850F588B3}, Version 1.0 NumMethods = 5 ``` After that registration, the x64 managed process can call `CoMarshalInterface` for `INmxSvcCallback` directly. The probe now emits a standard 366-byte OBJREF for the callback IID instead of `REGDB_E_CLASSNOTREG`. Using the real marshaled callback OBJREF, `NmxSvc.exe` accepts non-null `RegisterEngine2`, `Connect`, `AddSubscriberEngine`, `TransferData`, and `UnRegisterEngine` calls: ```text analysis\proxy\managed-registerengine2-callback-com-real-probe.txt analysis\proxy\x86-registerengine2-self-transfer-callback-probe.txt ``` The same synthetic self-transfer route does not produce a callback in the x86 harness either. Therefore the absence of a callback event in the current probe is caused by the synthetic NMX message/session body, not by inability to marshal the callback interface after the type-library registration.