C#_appendix

C/C++ → C# Windows Native API / ABI Cheat Sheet

A practical reference for translating Windows native API definitions, structures, pointers, unions, and function prototypes from C/C++ to C# P/Invoke/delegates.


1. Core Rule

When translating a native API, don’t ask:

“What C# type looks like this C type?”

Ask:

“What is the size, representation, and indirection of this native type?”

For every parameter, determine:

  1. Is it a value or pointer?
  2. What is its size?
  3. Is it pointer-sized?
  4. Is it a structure?
  5. Is it a union?
  6. Is the native function expected to read, write, or both?
  7. Is the type architecture-dependent?

2. Basic Integer Types

Native C/C++ Size C#
BYTE 1 byte
CHAR 1 sbyte / byte
UCHAR 1 byte
WORD 2 ushort
SHORT 2 short
USHORT 2 ushort
DWORD 4 uint
LONG 4 int
ULONG 4 uint
INT 4 int
UINT 4 uint
LONG64 8 long
ULONG64 8 ulong
LONGLONG 8 long
ULONGLONG 8 ulong

Important

Don’t confuse:

1
LONG

with:

1
LONG_PTR

LONG is always 32-bit.

LONG_PTR is pointer-sized.


3. Pointer-Sized Types

These are especially important on x64.

Native Meaning C#
ULONG_PTR unsigned pointer-sized integer UIntPtr / nuint
LONG_PTR signed pointer-sized integer IntPtr / nint
SIZE_T unsigned pointer-sized size UIntPtr / nuint
SSIZE_T signed pointer-sized size IntPtr / nint
UINT_PTR unsigned pointer-sized integer UIntPtr / nuint
PVOID generic pointer IntPtr
HANDLE opaque pointer-sized handle IntPtr

Architecture

1
2
3
4
5
32-bit process:
    pointer-sized = 4 bytes

64-bit process:
    pointer-sized = 8 bytes

Check your process:

1
2
Console.WriteLine(Environment.Is64BitProcess);
Console.WriteLine(IntPtr.Size);

Expected on x64:

1
2
True
8

4. Pointers

This is one of the most important concepts.

Native value

1
DWORD Value;

C#:

1
uint Value;

Pointer to a value

1
DWORD *Value;

Possible C# representation:

1
ref uint Value

or:

1
IntPtr Value

depending on how the pointer is being used.


Pointer to a structure

1
CLIENT_ID *ClientId;

C#:

1
ref CLIENT_ID ClientId

Pointer to a pointer

1
PVOID *BaseAddress;

Break it down:

1
2
3
4
5
6
7
PVOID
 ↓
pointer
 ↓
PVOID *
 ↓
pointer to a pointer

C#:

1
ref IntPtr BaseAddress

5. ref vs out

out

Use when the native function produces the value.

Native:

1
PHANDLE ProcessHandle;

C#:

1
out IntPtr ProcessHandle

Example:

1
2
3
4
5
6
IntPtr processHandle;

NtOpenProcess(
    out processHandle,
    ...
);

ref

Use when the caller provides an existing value that the native function can read and/or modify.

Native:

1
PCLIENT_ID ClientId;

C#:

1
ref CLIENT_ID ClientId

Example:

1
2
3
4
5
6
CLIENT_ID clientId = new CLIENT_ID();

NtOpenProcess(
    ...,
    ref clientId
);

Mental model

1
2
3
4
5
out
    "Here, native function — give me a value."

ref
    "Here is a value. Native function may read/change it."

6. Common Windows Types

Windows type C#
HANDLE IntPtr
HMODULE IntPtr
HINSTANCE IntPtr
HWND IntPtr
LPVOID IntPtr
PVOID IntPtr
LPBYTE IntPtr / byte*
ACCESS_MASK uint
NTSTATUS int
BOOLEAN byte
BOOL bool / int depending on API
SIZE_T UIntPtr / nuint
ULONG_PTR UIntPtr / nuint

7. NTSTATUS

Native:

1
NTSTATUS

Usually map to:

1
int

Example:

1
2
3
private delegate int NtCloseDelegate(
    IntPtr Handle
);

Inspect the status

Don’t only print decimal:

1
Console.WriteLine(status);

Also print hexadecimal:

1
2
3
Console.WriteLine(
    $"NTSTATUS: 0x{unchecked((uint)status):X8}"
);

Example:

1
2
-1073741819
0xC0000005

This makes Windows error values much easier to identify.


8. Structures

Native:

1
2
3
4
typedef struct _CLIENT_ID {
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID;

Translate the fields first:

1
2
HANDLE → IntPtr
HANDLE → IntPtr

Then create:

1
2
3
4
5
6
[StructLayout(LayoutKind.Sequential)]
public struct CLIENT_ID
{
    public IntPtr UniqueProcess;
    public IntPtr UniqueThread;
}

Why Sequential?

It tells .NET to lay the fields out in the same order as the native structure.


9. Structure Validation

Never assume your structure layout is correct.

Check its size:

1
2
3
Console.WriteLine(
    Marshal.SizeOf<CLIENT_ID>()
);

On x64:

1
CLIENT_ID = 16 bytes

You can also inspect offsets:

1
2
3
4
5
Console.WriteLine(
    Marshal.OffsetOf<CLIENT_ID>(
        nameof(CLIENT_ID.UniqueProcess)
    )
);

10. OBJECT_ATTRIBUTES Example

Native concept:

1
2
3
4
5
6
7
8
typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    HANDLE RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG Attributes;
    PVOID SecurityDescriptor;
    PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

C#:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[StructLayout(LayoutKind.Sequential)]
public struct OBJECT_ATTRIBUTES
{
    public uint Length;
    public IntPtr RootDirectory;
    public IntPtr ObjectName;
    public uint Attributes;
    public IntPtr SecurityDescriptor;
    public IntPtr SecurityQualityOfService;
}

On x64, this structure is expected to be:

1
48 bytes

Approximate offsets:

Offset Size Field
0 4 Length
4 4 Padding
8 8 RootDirectory
16 8 ObjectName
24 4 Attributes
28 4 Padding
32 8 SecurityDescriptor
40 8 SecurityQualityOfService

11. Unions

Native:

1
2
3
4
5
6
7
union {
    ULONG64 ULong64;
    PVOID Pointer;
    SIZE_T Size;
    HANDLE Handle;
    ULONG ULong;
};

A union means:

“Multiple fields occupy the same memory location.”

Don’t use normal sequential layout for the union.

Use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[StructLayout(LayoutKind.Explicit)]
public struct MyUnion
{
    [FieldOffset(0)]
    public ulong ULong64;

    [FieldOffset(0)]
    public IntPtr Pointer;

    [FieldOffset(0)]
    public UIntPtr Size;

    [FieldOffset(0)]
    public IntPtr Handle;

    [FieldOffset(0)]
    public uint ULong;
}

Visual representation:

1
2
3
4
5
6
7
8
9
        Same memory
             ↓
┌─────────────────────────┐
│ ULong64                 │
│ Pointer                 │
│ Size                    │
│ Handle                  │
│ ULong                   │
└─────────────────────────┘

12. Bitfields

Native:

1
2
ULONG64 Type : 8;
ULONG64 Reserved : 56;

C# doesn’t have native C-style bitfields.

Usually represent the underlying storage:

1
ulong TypeAndReserved;

Then manipulate the bits manually.

For example:

1
2
3
4
5
64-bit value
┌────────┬────────────────────────────────────────────┐
│ Type   │                 Reserved                   │
│ 8 bits │                  56 bits                   │
└────────┴────────────────────────────────────────────┘

13. Arrays

Native embedded array:

1
BYTE Buffer[32];

Possible C# representation:

1
2
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] Buffer;

But distinguish this from:

1
BYTE *Buffer;

The first:

1
32 bytes embedded inside structure

The second:

1
pointer to memory somewhere else

They are not equivalent.


14. Strings

ANSI

Native:

1
char *Name;

Potential C# representation:

1
2
[MarshalAs(UnmanagedType.LPStr)]
string Name

Unicode

Native:

1
wchar_t *Name;

Potential C# representation:

1
2
[MarshalAs(UnmanagedType.LPWStr)]
string Name

Windows APIs commonly use UTF-16 Unicode strings.


15. Function Pointers

Native:

1
2
3
4
typedef NTSTATUS (*FUNCTION)(
    HANDLE Handle,
    ULONG Value
);

C#:

1
2
3
4
5
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate int FunctionDelegate(
    IntPtr Handle,
    uint Value
);

Then an exported function address can be converted into the delegate.

The important point:

“The delegate must exactly match the native function’s ABI.”

That means matching:

  • Return type
  • Parameter count
  • Parameter order
  • Parameter sizes
  • Pointer/value semantics
  • Calling convention
  • Structure layout

16. Calling Convention

For Windows APIs, you’ll commonly encounter:

1
[UnmanagedFunctionPointer(CallingConvention.Winapi)]

For modern x64 Windows, the underlying x64 ABI has a standardized calling convention, so the old x86 distinction between stdcall and cdecl does not work the same way.

For x86 targets, calling convention differences matter much more.


17. Example: NtOpenProcess

Native:

1
2
3
4
5
6
NTSTATUS NtOpenProcess(
    PHANDLE ProcessHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PCLIENT_ID ClientId
);

Translate one parameter at a time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
NTSTATUS
    ↓
int

PHANDLE
    ↓
out IntPtr

ACCESS_MASK
    ↓
uint

POBJECT_ATTRIBUTES
    ↓
ref OBJECT_ATTRIBUTES

PCLIENT_ID
    ↓
ref CLIENT_ID

Final delegate:

1
2
3
4
5
6
7
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate int NtOpenProcessDelegate(
    out IntPtr ProcessHandle,
    uint DesiredAccess,
    ref OBJECT_ATTRIBUTES ObjectAttributes,
    ref CLIENT_ID ClientId
);

18. Example: NtAllocateVirtualMemory

Native concept:

1
2
3
4
5
6
7
8
NTSTATUS NtAllocateVirtualMemory(
    HANDLE ProcessHandle,
    PVOID *BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
);

Translate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HANDLE
    ↓
IntPtr

PVOID *
    ↓
ref IntPtr

ULONG_PTR
    ↓
UIntPtr

PSIZE_T
    ↓
ref UIntPtr

ULONG
    ↓
uint

ULONG
    ↓
uint

Result:

1
2
3
4
5
6
7
8
private delegate int NtAllocateVirtualMemoryDelegate(
    IntPtr ProcessHandle,
    ref IntPtr BaseAddress,
    UIntPtr ZeroBits,
    ref UIntPtr RegionSize,
    uint AllocationType,
    uint Protect
);

Notice that ULONG_PTR is not:

1
uint

because it is pointer-sized.


19. Export Name Must Match Delegate

This is an especially important lesson from debugging native APIs.

Bad:

1
2
3
4
5
Export:
    NtAllocateVirtualMemory

Delegate:
    NtAllocateVirtualMemoryEx

or:

1
2
3
4
5
Export:
    FunctionA

Delegate:
    FunctionB's signature

The address may resolve successfully, but the call can still fail catastrophically.

Think:

1
2
3
4
5
6
7
8
9
Function name
      +
Function prototype
      +
Calling convention
      +
Parameter layout
      ↓
      ABI

All of them must agree.


20. Don’t Trust the Function Address Alone

This:

1
IntPtr address = GetProcAddress(...);

only proves that you found an address.

It does not prove:

  • ✓ Correct prototype
  • ✓ Correct parameter count
  • ✓ Correct parameter sizes
  • ✓ Correct calling convention
  • ✓ Correct structure layout

So:

1
2
3
GetProcAddress succeeds
        ≠
ABI is correct

21. A Systematic ABI Translation Workflow

When you find a new Windows native prototype, use this process.

Step 1 — Get the native declaration

Example:

1
2
3
4
5
6
NTSTATUS SomeFunction(
    HANDLE A,
    PVOID *B,
    ULONG C,
    PSIZE_T D
);

Step 2 — Identify every base type

1
2
3
4
5
NTSTATUS
HANDLE
PVOID
ULONG
SIZE_T

Step 3 — Resolve typedefs

1
2
3
4
5
NTSTATUS → signed 32-bit
HANDLE   → pointer-sized handle
PVOID    → pointer
ULONG    → unsigned 32-bit
SIZE_T   → unsigned pointer-sized

Step 4 — Resolve indirection

1
2
3
4
HANDLE       → IntPtr
PVOID *      → ref IntPtr
SIZE_T       → UIntPtr
PSIZE_T      → ref UIntPtr

Step 5 — Check structure definitions

If a parameter is:

1
POBJECT_ATTRIBUTES

find the actual definition of:

1
OBJECT_ATTRIBUTES

Don’t guess its fields.

Step 6 — Check architecture

1
IntPtr.Size

Step 7 — Check structure sizes

1
Marshal.SizeOf<OBJECT_ATTRIBUTES>()

Step 8 — Check the function prototype against the actual export

The function name and delegate signature must correspond.


22. The Most Useful Mental Model

Think in terms of levels of indirection.

Given:

1
DWORD

you have:

1
2
3
DWORD
  ↓
uint

Given:

1
DWORD *

you have:

1
2
3
DWORD *
  ↓
ref uint

Given:

1
DWORD **

you have:

1
2
3
DWORD **
   ↓
pointer to pointer

Often represented using:

1
ref IntPtr

or another explicitly marshalled representation depending on what the pointer actually points to.

For:

1
PVOID

think:

1
2
3
4
5
PVOID
 ↓
pointer
 ↓
IntPtr

For:

1
PVOID *

think:

1
2
3
4
5
PVOID *
   ↓
pointer to pointer
   ↓
ref IntPtr

23. Quick Decision Tree

When you see a native parameter, ask:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
                 Native type
                      │
                      ▼
              Is it a pointer?
                 /          \
               No            Yes
               │              │
               ▼              ▼
         What is size?    What does it point to?
               │              │
               ▼              ▼
       map exact width    struct? value? void?
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
              struct       value         void
                 │            │            │
                 ▼            ▼            ▼
               ref T       ref T       IntPtr/ref IntPtr

Then separately ask:

  • Is it pointer-sized?
  • Is it an array?
  • Is it a union?
  • Is it a bitfield?
  • Is it architecture-dependent?

24. Validation Checklist

Before calling an unfamiliar native function:

  • Native prototype obtained
  • Return type mapped
  • Every parameter mapped
  • Parameter order preserved
  • Parameter count preserved
  • Pointer indirection understood
  • ref/out semantics understood
  • Structures defined
  • Struct packing/layout checked
  • Unions represented with Explicit layout
  • Bitfields accounted for
  • Pointer-sized types identified
  • x86/x64 considered
  • Calling convention considered
  • Export name matches function
  • Function address is non-zero
  • Struct sizes validated with Marshal.SizeOf

25. The Golden Rules

If you remember only a few things, remember these:

Rule 1

Don’t map by name alone.

1
ULONG_PTR ≠ ULONG

They may look similar but have different widths.

Rule 2

Pointers matter more than the base type.

1
2
3
DWORD
DWORD *
DWORD **

are three different things.

Rule 3

Structure layout is part of the ABI.

The fields, order, alignment, and size all matter.

Rule 4

Unions are not normal structures.

Use:

1
LayoutKind.Explicit

with:

1
FieldOffset

when appropriate.

Rule 5

An exported address doesn’t validate your delegate.

The function’s ABI and your delegate must match.

Rule 6

Validate assumptions.

Use:

1
2
3
IntPtr.Size
Marshal.SizeOf<T>()
Marshal.OffsetOf<T>()

rather than guessing.


26. One-Line Mental Shortcut

When translating Windows C → C#, think:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
C type
  ↓
What is its actual width?
  ↓
Is it a pointer?
  ↓
What does the pointer point to?
  ↓
Does native read it, write it, or both?
  ↓
Is the data sequential, unioned, or bit-packed?
  ↓
C# type

That process will take you much further than memorizing a giant P/Invoke table.

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy