Implement IDList

This commit is contained in:
Yoshi Askharoun
2026-07-12 23:03:52 -05:00
parent 94bfb22fc3
commit 2e0a053bb7
4 changed files with 279 additions and 6 deletions
+26
View File
@@ -91,3 +91,29 @@ The original constructor calls `global::<Module>.ZuneLibraryExports.CreateProper
**File:** `ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs` (previous `IntPtr`-based draft)
The ILSpy decompilation shows `delegate* unmanaged[Cdecl, Cdecl]` for the vtable dispatch (the double `Cdecl` appears to be an ILSpy artifact of C++/CLI compilation). Standard COM uses `__stdcall` on x86 and the platform ABI on x64 (where Cdecl and Stdcall converge). The current implementation uses `[GeneratedComInterface]` which handles this, but if raw vtable dispatch is ever needed (e.g. for the unknown slots), the calling convention should be verified against the native binary.
---
## 2026-07-12 — IMultiSortAttributes vtable slot correction
**File:** `ZuneDBApi/MicrosoftZuneInterop/QueryPropertyBag.cs`
The original comment had `GetSortOrders` at slot 3 and `GetSortAttributes` at slot 4. This was wrong. The correct assignment was determined by reading the full ILSpy decompilation of `PackMultiSortAttributes`:
```csharp
// ptr ← call at *(vtable + 32) (slot 4, assuming IUnknown at slots 02)
ptr = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr);
// ptr2 ← call at *(vtable + 40) (slot 5)
ptr2 = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr2 + 40)))((nint)intPtr2);
```
Then in the fill loop:
- `*ptr4 = (int)eQuerySortType` where `ptr4` starts at `ptr2` and advances by 4 each iteration → `ptr2` receives **EQuerySortType** values → slot 5 = `GetSortOrders()`
- `*(int*)((byte*)ptr5 + (nuint)ptr4) = num5` where `ptr5 = ptr - ptr2`, which simplifies to writing `num5` (the schema index from `CSchemaMap.GetIndex`) into `ptr[i]``ptr` receives **schema indices** → slot 4 = `GetSortAttributes()`
Slot 3 (offset 24) is not called in `PackMultiSortAttributes` and remains unknown.
**Corrected layout:**
- `[3]` unknown
- `[4]` `GetSortAttributes()``int*` (parallel array of SchemaMap indices)
- `[5]` `GetSortOrders()``int*` (parallel array of EQuerySortType values)
@@ -6,8 +6,9 @@ using System.Runtime.InteropServices.Marshalling;
namespace MicrosoftZuneInterop;
// Native COM interface IMultiSortAttributes — vtable layout (x64, IUnknown = slots 02):
// [3] GetSortOrders() -> EQuerySortType* (parallel array of sort directions)
// [4] GetSortAttributes() -> int* (parallel array of SchemaMap indices)
// [3] unknown
// [4] GetSortAttributes() -> int* (parallel array of SchemaMap indices)
// [5] GetSortOrders() -> int* (parallel array of EQuerySortType values)
//
// Native struct IDList (16 bytes):
// int Count
@@ -71,11 +72,25 @@ public class QueryPropertyBag : IDisposable
return StrategyBasedComWrappers.Instance.GetOrCreateComInterfaceForObject(_bag, CreateComInterfaceFlags.None);
}
// Packs multiIds into a native IDList: { int Count; int* Ids }.
// The returned IntPtr is unmanaged heap memory; the native caller is responsible for freeing it.
public IntPtr PackIDList(IList multiIds)
// Packs multiIds into a native IDList: { int Count; (4 pad); int* Ids }.
// The returned IntPtr points to native heap memory; the native caller is responsible for freeing it.
public unsafe IntPtr PackIDList(IList multiIds)
{
throw new NotImplementedException();
int count = multiIds.Count;
// Allocate the 16-byte IDList struct and zero the Ids pointer slot before filling it,
// so the struct is never in a half-initialized state if AllocHGlobal below throws.
IntPtr listPtr = Marshal.AllocHGlobal(16);
*(int*)listPtr = count;
*(long*)(listPtr + 8) = 0L;
IntPtr idsPtr = Marshal.AllocHGlobal(count * sizeof(int));
*(IntPtr*)(listPtr + 8) = idsPtr;
for (int i = 0; i < count; i++)
*(int*)(idsPtr + i * sizeof(int)) = (int)multiIds[i]!;
return listPtr;
}
// Packs parallel sort-direction/attribute arrays into a native IMultiSortAttributes COM object.
@@ -0,0 +1,94 @@
# IQueryPropertyBag — decompilation log
Append-only. Do not edit previous entries.
---
## 2026-07-12 — Interface reconstruction
**Source:** ILSpy decompilation of `ZuneShell/lib/ZuneDBApi.dll` (mixed-mode C++/CLI assembly).
ILSpy cannot see the native side of a C++/CLI assembly, so `IQueryPropertyBag` is only
visible indirectly — through the managed `QueryPropertyBag` class that wraps it. The
vtable offsets were read from the raw `delegate* unmanaged[Cdecl, Cdecl]` call sites
that ILSpy emits for native virtual dispatch.
### Vtable layout (x64, pointer size = 8 bytes)
Offsets confirmed by reading the decompiled call sites in `QueryPropertyBag`:
| Slot | Byte offset | Signature | Source |
|------|------------|-----------|--------|
| 0 | 0 | `QueryInterface` (IUnknown) | assumed; standard COM |
| 1 | 8 | `AddRef` (IUnknown) | assumed; standard COM |
| 2 | 16 | `Release` (IUnknown) | confirmed — `!QueryPropertyBag()` destructor calls `*(vtable + 16)` |
| 3 | 24 | unknown | not observed in any managed call site |
| 4 | 32 | unknown | not observed in any managed call site |
| 5 | 40 | `SetString(EQueryPropertyBagProp prop, wchar_t* value) -> HRESULT` | `SetValue` with `string` branch: `*(vtable + 40)` |
| 6 | 48 | unknown | not observed in any managed call site |
| 7 | 56 | `SetInt(EQueryPropertyBagProp prop, int value) -> HRESULT` | `SetValue` with `int`/`bool` branch: `*(vtable + 56)` |
| 812 | 6496 | unknown | not observed in any managed call site |
| 13 | 104 | `IsSet(EQueryPropertyBagProp prop, int* out) -> HRESULT` | `IsSet`: `*(vtable + 104)` |
Unknown slots (3, 4, 6, 812) are declared as `void _ReservedN()` placeholders in
`IQueryPropertyBag.cs` to preserve vtable ordering without blocking compilation.
These placeholders must not be called, and their signatures must be verified against
the native binary before the CCW/RCW is used in production.
**Needs:** Ghidra analysis of the native `IQueryPropertyBag` vtable definition to
fill in slots 3, 4, 6, and 812.
---
## 2026-07-12 — GUID: initial search
**Source:** ILSpy search for `IQueryPropertyBag` as a type in `ZuneDBApi.dll`.
The `[GeneratedComInterface]` attribute requires a GUID. ILSpy finds no managed type
named `IQueryPropertyBag` (only the global-namespace native struct and the
`CComPtrNtv<IQueryPropertyBag>` wrapper). A placeholder GUID
`3D8A1F2B-6C4E-4A5D-9B7F-2E0C1A8D3F4E` was used.
---
## 2026-07-12 — GUID: follow-up search in managed metadata
**Task:** Check whether the real GUID is encoded anywhere accessible to ILSpy.
Specifically examined: the global-namespace `IQueryPropertyBag` struct, `<Module>`,
and all call sites in `QueryPropertyBag`.
**Findings:**
1. The global-namespace struct `IQueryPropertyBag` carries only `[NativeCppClass]`,
`[DebugInfoInPDB]`, and `[MiscellaneousBits(65)]` — no `[Guid]` attribute.
2. `<Module>` could not be decompiled by ILSpy (tool returned an error); its fields
are not inspectable through the managed surface.
3. Full decompilation of `MicrosoftZuneInterop.QueryPropertyBag` shows no IID
reference in managed code. `GetIQueryPropertyBag()` returns the raw pointer
directly; there is no `QueryInterface` call visible.
4. The destructor (`!QueryPropertyBag`) calls `*(vtable + 16)` with just `this`,
confirming `Release` is at slot 2 — consistent with IUnknown, but this reveals
no IID.
**Conclusion:** The GUID is not present in the managed metadata of `ZuneDBApi.dll`.
If one exists it is in the native segment only (`__declspec(uuid(...))` or
`DEFINE_GUID`). Recovery requires Ghidra. Placeholder GUID stands.
It is also possible the interface has no GUID at all: the user confirmed that
`IQueryPropertyBag` objects are not `CoCreateInstance`-registered, and no
`QueryInterface` call was observed, so a GUID may simply never be needed at runtime.
---
## 2026-07-12 — Calling convention
The ILSpy decompilation emits `delegate* unmanaged[Cdecl, Cdecl]` for the vtable
dispatch. The duplicate `Cdecl` is a known ILSpy artifact of C++/CLI compilation and
does not mean the calling convention is Cdecl. On x64 Windows, Stdcall and Cdecl
are identical (both use the Microsoft x64 ABI), so the distinction does not affect
correctness there.
The current implementation uses `[GeneratedComInterface]`, which selects the correct
calling convention automatically. If raw vtable dispatch is ever needed (e.g. to call
the placeholder-reserved slots), the convention should be verified against the native
binary before assuming Stdcall.
@@ -0,0 +1,138 @@
# QueryPropertyBag — decompilation log
Append-only. Do not edit previous entries.
---
## 2026-07-12 — Decompilation
**Source:** ILSpy decompilation of `MicrosoftZuneInterop.QueryPropertyBag` in
`ZuneShell/lib/ZuneDBApi.dll` (mixed-mode C++/CLI assembly).
`QueryPropertyBag` is a managed wrapper around a native `IQueryPropertyBag*` COM
pointer created by the native export `ZuneLibraryExports.CreatePropertyBag`. The
class is not itself a COM object — it simply holds the pointer and delegates to it.
### Constructor
Calls `global::<Module>.ZuneLibraryExports.CreatePropertyBag(&pPropertyBag)`.
This is a native export. The DLL name, exact symbol, and calling convention are
unknown and cannot be determined from the managed metadata alone.
**Unknown (Q5):** P/Invoke signature for `CreatePropertyBag`. Needs native binary
inspection or documentation. Constructor is stubbed with a `// TODO` comment.
### SetValue / IsSet
Dispatch to `IQueryPropertyBag` vtable slots 5, 7, 13 (see
`../MicrosoftZuneInterop/IQueryPropertyBag.md`). Logic is fully implementable once
`MapNameToProp` is implemented.
### MapNameToProp
Walks a static 37-entry native array `MicrosoftZuneInterop.?A0x52c37a46.kPropIdMap`
of `PropIdMapEntry` structs (each 16 bytes: `wchar_t*` name at [0..7],
`EQueryPropertyBagProp` id at [8..11], 4 bytes padding at [12..15]) using
`_wcsicmp` for case-insensitive comparison.
The name strings and enum values are in native data — not visible through ILSpy.
Without them, `MapNameToProp` (and by extension `SetValue` and `IsSet`) cannot be
implemented.
**Unknown (Q3):** The 37 name strings and their `EQueryPropertyBagProp` values.
Needs Ghidra analysis of the native data segment.
**Unknown (Q4):** `EQueryPropertyBagProp` enum definition. Lives in the global
(unnamed) namespace; ILSpy queries with a namespace prefix fail. Resolving Q3 and
Q4 together in Ghidra would be efficient since the map table embeds the enum values
directly.
### GetIQueryPropertyBag
Returns `m_pPropertyBag` directly. No `QueryInterface`, no IID involved.
### PackIDList — implemented 2026-07-12
**Source:** Full ILSpy decompilation of `PackIDList` in `ZuneDBApi.dll`.
The original allocates a 16-byte `IDList` struct on the native heap and fills it:
```csharp
// Original (ILSpy output, simplified):
IDList* ptr = (IDList*)global::<Module>.@new(16uL);
*(int*)ptr = count; // Count at offset 0
*(long*)((ulong)(nint)ptr + 8uL) = 0L; // zero Ids ptr at offset 8
*(long*)((ulong)(nint)ptr + 8uL) = (nint)global::<Module>.new[](count * 4uL); // heap int[]
for (int i = 0; i < count; i++)
*(int*)(i * 4 + *(long*)((ulong)(nint)ptr + 8uL)) = (int)multiIds[i];
```
**Translation decisions:**
- `global::<Module>.@new(16)``Marshal.AllocHGlobal(16)`.
`operator new` and `Marshal.AllocHGlobal` both allocate unmanaged heap memory.
The key difference: `operator new` can return null on failure; `Marshal.AllocHGlobal`
throws `OutOfMemoryException` instead. The original null-check / try-catch is
therefore not needed — if allocation fails the exception propagates naturally.
- `global::<Module>.new[](count * 4)``Marshal.AllocHGlobal(count * sizeof(int))`.
Same allocation family; same reasoning. `sizeof(int)` is 4 on all platforms.
- The original overflow guard `(count > 4611686018427387903L ? ulong.MaxValue : count * 4)`
is omitted. `IList.Count` returns a 32-bit `int` (max ~2 billion). Multiplying by
4 yields at most ~8 GB, which fits in a 64-bit `nint` and would result in an
`OutOfMemoryException` from the allocator long before arithmetic overflow.
- The Ids pointer slot at offset 8 is zeroed before the second `AllocHGlobal` call,
matching the original's initialization order. This ensures the struct is never in
a half-initialized state if the second allocation throws.
- Pointer arithmetic is done with `unsafe` pointer casts, directly mirroring the
original byte-level layout.
**Reference:** The `IDList` struct layout (16 bytes: `int Count` + 4 pad + `int* Ids`)
is confirmed by the ILSpy output for this method and consistent with the comment
at the top of `QueryPropertyBag.cs`.
---
## 2026-07-12 — IMultiSortAttributes vtable layout
**Source:** ILSpy decompilation of `PackMultiSortAttributes` in `ZuneDBApi.dll`.
The original `PackMultiSortAttributes` calls two vtable slots on the native
`IMultiSortAttributes*` returned by `CreateMultiSortAttributes`:
```csharp
// ptr ← vtable slot at byte offset 32
ptr = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr + 32)))((nint)intPtr);
// ptr2 ← vtable slot at byte offset 40
ptr2 = ((delegate* unmanaged[Cdecl, Cdecl]<IntPtr, int*>)(*(ulong*)(*(long*)intPtr2 + 40)))((nint)intPtr2);
```
In the fill loop:
- `ptr4` starts at `ptr2` and advances by 4; `*ptr4 = (int)eQuerySortType``ptr2`
receives `EQuerySortType` values → offset 40 (slot 5) = **GetSortOrders()**
- `ptr5 = ptr - ptr2`; `*(int*)((byte*)ptr5 + ptr4) = num5` simplifies to writing
the schema index (`CSchemaMap.GetIndex` result) into `ptr[i]` → offset 32 (slot 4)
= **GetSortAttributes()**
Assuming IUnknown at slots 02 (offsets 0, 8, 16), slot 3 (offset 24) is not called
in this method and remains unknown. The original comment had these two methods
transposed; the code comment and this log both reflect the corrected order.
| Slot | Byte offset | Signature |
|------|------------|-----------|
| 02 | 016 | IUnknown (assumed) |
| 3 | 24 | unknown |
| 4 | 32 | `GetSortAttributes() -> int*` (schema index array) |
| 5 | 40 | `GetSortOrders() -> int*` (EQuerySortType array) |
### PackMultiSortAttributes — still a stub
Requires two native P/Invokes that cannot yet be resolved from managed metadata:
- `ZuneLibraryExports.CreateMultiSortAttributes(int count, IMultiSortAttributes** out)`
- `CSchemaMap.GetIndex(wchar_t* name) -> int`
Both the DLL names and calling conventions are unknown. Stubbed with a `// TODO`
comment listing the full algorithm derived from the decompilation.