`PROCESSENTRY32W` in C#?
Asked Answered
R

3

6

I declared the function Process32FirstW and the structure PROCESSENTRY32W like this:

[DllImport("KERNEL32.DLL", CallingConvention = CallingConvention.StdCall, EntryPoint = "Process32FirstW")]
private static extern bool Process32FirstW (IntPtr hSnapshot, ref ProcessEntry pProcessEntry);

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode, Size = 568)]
internal struct ProcessEntry {
    [FieldOffset(0)] public int Size;
    [FieldOffset(8)] public int ProcessId;
    [FieldOffset(32)] public int ParentProcessID;
    [FieldOffset(44), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string ExeFile;
}

When calling Process32FirstW (with a 64-bit process), I always get a TypeLoadException saying

The type ProcessEntry couldn't be loaded, because the object field at offset 44 is aligned wrong or is overlapped by another field, which isn't an object field.

I also tried using char[] instead of string for ProcessEntry.ExeFile and using Pack=4 and Pack=8 in the structure's StructLayoutAttribute. I always set ProcessEntry.Size to 568 and I copied the offset data from a C++ program (64-bit build):

typedef unsigned long long ulong;
PROCESSENTRY32W entry;

wcout << sizeof(PROCESSENTRY32W) << endl;                           // 568
wcout << (ulong)&entry.dwSize - (ulong)&entry << endl;              // 0
wcout << (ulong)&entry.th32ProcessID - (ulong)&entry << endl;       // 8
wcout << (ulong)&entry.th32ParentProcessID - (ulong)&entry << endl; // 32
wcout << (ulong)&entry.szExeFile - (ulong)&entry << endl;           // 44

I can't figure out, what is going wrong, so how to declare PROCESSENTRY32W in C# for a 64-bit application? Do I have to use C++/CLI or am I simply doing something wrong here?


EDIT: Running this code as a 64-bit program works perfectly fine for me

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

PROCESSENTRY32W entry;
entry.dwSize = sizeof(PROCESSENTRY32W);

if (Process32FirstW(hSnapshot, &entry)) {
    do {
        // Do stuff
    } while (Process32NextW(hSnapshot, &entry));
}

CloseHandle(hSnapshot);
Raffaello answered 10/11, 2015 at 14:27 Comment(3)
Your struct does not match the MSDN's PROCESSENTRY32 anywhere close.Macmacabre
+scott-chamberlain Why? dwSize is 4 bytes in size, like my Size field. Both are located at offset 0. The PID is also 4 bytes like mine and at offset 8. The parent PID also is 4 bytes in size and seems to be at offset 32 according to the C++ sample. I used 260 wide characters at offset 44. Where does my struct differ from the one in the MSDN?Raffaello
You are ignoring the fact that ULONG_PTR is differently sized on 32 and 64 bit systems, wrote up a full answer.Macmacabre
M
5

PROCESSENTRY32 is fully defined as

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  TCHAR     szExeFile[MAX_PATH];
} PROCESSENTRY32, *PPROCESSENTRY32;

You ignore ULONG_PTR th32DefaultHeapID;, that member is 4 bytes on 32 bit systems and 8 bytes on 64 bit systems, that means your FieldOffsetAttribute for ParentProcessID and ExeFile will have a different offset depending on if you are running 32 bits vs 64 bits. Looking at your math it appears you assumed it would always be 8 bytes.

The easiest workaround is don't explicitly define the offsets and use IntPtr to dynamicly figure the correct offset.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PROCESSENTRY32 
{ 
   public uint dwSize; 
   public uint cntUsage; 
   public uint th32ProcessID; 
   public IntPtr th32DefaultHeapID; 
   public uint th32ModuleID; 
   public uint cntThreads; 
   public uint th32ParentProcessID; 
   public int pcPriClassBase; 
   public uint dwFlags; 
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)] public string szExeFile; 
 }; 
Macmacabre answered 10/11, 2015 at 15:7 Comment(2)
Even though it works (thank you :), I still have to ask how I could ignore th32DefaultHeapID when actually using the offset data from a 64-bit program?Raffaello
See Hans' answer, you really can't due to the managed memory model.Macmacabre
R
4

Yes, this cannot work. When you use LayoutKind.Explicit then the structure layout of the managed struct will be the one you specify. As well as the unmanaged version of the struct. In this specific case however that violates the .NET memory model. Which dictates that object references, like ProcessEntry.ExeFile, are always atomic.

Atomicity can only be achieved if the variable is properly aligned. So it can be updated with a single memory bus cycle. In 64-bit mode, that requires an object reference to be aligned to 8 since object references are 8-byte pointers. Problem is, an offset of 44 is aligned only to 4, not to 8.

Not a problem in the unmanaged version of the struct at all, the ExeFile member is actually a WCHAR[] array. Which only requires an alignment to 2 so no need to pad to get the member at 48.

You must give up on LayoutKind.Explicit and use LayoutKind.Sequential instead. Easy peasy, also can make you feel good that your code still works properly in 32-bit mode.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct ProcessEntry {
    public int Size;
    public int Usage;
    public int ProcessId;
    public IntPtr DefaultHeapId;
    public int ModuleId;
    public int Threads;
    public int ParentProcessID;
    public int Priority;
    public int Flags;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string ExeFile;
}

And a check never hurts:

    System.Diagnostics.Debug.Assert(IntPtr.Size == 8 &&
        Marshal.OffsetOf(typeof(ProcessEntry), "ExeFile") == (IntPtr)44);
Ranged answered 10/11, 2015 at 15:19 Comment(2)
I read something about alignment, but I didn't know about atomicity. But when using sequential layout the ExeFile field also gets aligned to 4. Isn't this a violation too?Raffaello
Now the managed layout of the struct no longer matches the unmanaged layout. The managed struct has the member at 48, the unmanaged at 44. The struct is no longer blittable, but it wasn't in the first place anyway because of the string.Ranged
H
0

Try setting alignment Pack=8 and Charset.Unicode.

Start field szExeFile is 40, not 44.
See individual size of each member.

Hemipterous answered 10/11, 2015 at 14:59 Comment(2)
IMO, this answer should either be a comment or be fleshed out more.Viminal
I already tried Pack=8 and I already set CharSet=Unicode (all mentioned in my question above). I also set the Charset for the function import even though it hasn't got any string parameters.Raffaello

© 2022 - 2024 — McMap. All rights reserved.