Unsafe Code and Native Interoperability
C# can use raw pointers inside an unsafe context.
Concept
C# can use raw pointers inside an unsafe context. It requires the compiler option AllowUnsafeBlocks (<AllowUnsafeBlocks>true</AllowUnsafeBlocks> in a .csproj, or #:property AllowUnsafeBlocks=true in a file-based app); without it you get CS0227.
int* p = &value; Address of a local p + 1, p++ Pointer arithmetic (in units of the element size) fixed (byte* p = array) { }
Pin a managed array so the GC cannot move it while you hold the pointer stackalloc into a pointer Stack memory without a span Size of an unmanaged type
The CLR no longer checks bounds or types through pointers: bugs corrupt memory silently. Use unsafe code only for interop, hardware buffers and measured hot paths — Span<T> (Module 41) covers most former use cases safely.
[StructLayout(LayoutKind.Sequential)] keeps field order (the default for structs); fields are aligned to their size, and the struct is padded to a multiple of its largest alignment. Pack = 1 removes padding (as in packed C structs for binary protocols). LayoutKind.Explicit + [FieldOffset] gives C-union-like overlays. Blittable types (primitives, and structs containing only blittable fields) have identical managed and native layouts and are passed without copying.
Platform Invoke calls functions in native DLLs (Windows APIs, vendor SDKs for CAN interfaces, measurement hardware):
[DllImport("kernel32.dll")] static extern uint GetCurrentThreadId(); — classic, marshalling code generated at run time.
[LibraryImport("kernel32.dll")] static partial uint GetCurrentProcessId(); — .NET 7+, source-generated marshalling (AOT-friendly). It requires AllowUnsafeBlocks because the generated code uses pointers (SYSLIB1062 otherwise) and explicit string marshalling (StringMarshalling.Utf16/Utf8) for strings and chars.
Marshalling: strings (UTF-16 for W Windows APIs, UTF-8/ANSI for most C libraries), out/ref structs, arrays with [In]/[Out], bool via [MarshalAs(UnmanagedType.Bool)] (Win32 BOOL is 4 bytes).
Errors: SetLastError = true + Marshal.GetLastPInvokeError(); Win32Exception turns the code into a message.
Native libraries: NativeLibrary.Load/TryGetExport for dynamic loading; SafeHandle for handles.
COM interop: [ComImport]/COM wrappers (or dynamic) for COM automation servers — still common with test equipment and Office.
In engineering tools the typical stack is a C# application → a vendor's native SDK (CAN interface DLL, measurement hardware driver) via P/Invoke → the device (Module 61).
Why It Matters
Many automotive and industrial devices ship only a C API. Knowing layout, marshalling and pinning is what makes the difference between a working CAN interface wrapper and random memory corruption.
Syntax
#:property AllowUnsafeBlocks=true
unsafe { int x = 1; int* p = &x; *p = 2; }
fixed (byte* p = buffer) { ... }
[StructLayout(LayoutKind.Sequential, Pack = 1)] struct Header { public byte Type; public uint Length; }
[LibraryImport("vendor.dll", StringMarshalling = StringMarshalling.Utf8)] static partial int Open(string channel);Example
42.1 Pointers, fixed, struct layout and reinterpreting bytes
42.2 Calling the Windows API with LibraryImport and DllImport
#:property AllowUnsafeBlocks=true
using System.Runtime.InteropServices;
// Pointers require an 'unsafe' context AND the AllowUnsafeBlocks compiler option
// (the #:property line above; in a .csproj: <AllowUnsafeBlocks>true</AllowUnsafeBlocks>)
unsafe
{
int value = 10;
int* pointer = &value;
*pointer += 5;
Console.WriteLine($"value via pointer: {*pointer}, value: {value}, sizeof(int*) = {sizeof(int*)}");
// Pointer arithmetic over a pinned array
byte[] frame = [0x02, 0x10, 0x03, 0x00];
fixed (byte* p = frame) // pin the array so the GC cannot move it
{
byte* cursor = p;
cursor += 1;
Console.WriteLine($"frame[1] via pointer arithmetic: 0x{*cursor:X2}");
*(p + 3) = 0xAA;
}
Console.WriteLine($"frame after pointer write: {Convert.ToHexString(frame)}");
// stackalloc with a pointer
int* squares = stackalloc int[4];
for (int i = 0; i < 4; i++) squares[i] = i * i;
Console.WriteLine($"stackalloc pointer: {squares[0]} {squares[1]} {squares[2]} {squares[3]}");
}
// Struct layout matters for interop and binary protocols
Console.WriteLine($"Marshal.SizeOf<CanFrameNative>() = {Marshal.SizeOf<
…#:property AllowUnsafeBlocks=true
using System.Runtime.InteropServices;
// P/Invoke into native Windows DLLs (kernel32, user32).
// LibraryImport is the source-generated, AOT-friendly successor to DllImport.
Console.WriteLine($"GetCurrentProcessId() == Environment.ProcessId: {Native.GetCurrentProcessId() == (uint)Environment.ProcessId}");
Console.WriteLine($"GetTickCount64() > 0: {Native.GetTickCount64() > 0}");
// Strings across the boundary: UTF-16 for W-suffixed Windows APIs
char[] buffer = new char[260];
uint len = Native.GetSystemDirectoryW(buffer, (uint)buffer.Length);
string systemDir = new(buffer, 0, (int)len);
Console.WriteLine($"GetSystemDirectoryW ends with System32: {systemDir.EndsWith("System32", StringComparison.OrdinalIgnoreCase)}");
// Structs are marshalled by layout
Native.GetSystemInfo(out SYSTEM_INFO info);
Console.WriteLine($"GetSystemInfo: processors {info.dwNumberOfProcessors}, page size {info.dwPageSize} bytes (matches Environment.ProcessorCount: {info.dwNumberOfProcessors == Environment.ProcessorCount})");
// Errors: SetLastError + Marshal.GetLastPInvokeError
bool ok = Native.SetCurrentDirectoryW(@"Z:\does\not\exist");
Console.WriteLine($"SetCurrentDire
…Expected Result
CanFrameNative has 13 bytes of fields but a size of 16: the struct is padded to a multiple of 4 (the alignment of its uint). With Pack = 1, PackedHeader is exactly 1 + 4 + 2 = 7 bytes. MemoryMarshal.Read reinterpreted the little-endian bytes 00 F1 FE 18 as 0x18FEF100.
Error 3 is ERROR_PATH_NOT_FOUND. (The processor count and page size are those of the machine that produced this output.)
value via pointer: 15, value: 15, sizeof(int*) = 8
frame[1] via pointer arithmetic: 0x10
frame after pointer write: 021003AA
stackalloc pointer: 0 1 4 9
Marshal.SizeOf<CanFrameNative>() = 16, Unsafe.SizeOf = 16
Marshal.SizeOf<PackedHeader>() = 7 (Pack = 1)
decoded id 0x18FEF100, dlc 8, data0 1, data7 8GetCurrentProcessId() == Environment.ProcessId: True
GetTickCount64() > 0: True
GetSystemDirectoryW ends with System32: True
GetSystemInfo: processors 8, page size 4096 bytes (matches Environment.ProcessorCount: True)
SetCurrentDirectoryW ok: False, last error: 3 (The system cannot find the path specified.)
DllImport GetCurrentThreadId() > 0: True
NativeLibrary: kernel32 exports GetTickCount64: TrueCommon Mistakes
Holding a pointer to a managed array outside fixed — the GC may move the array.
Wrong struct layout or packing compared to the C header → garbage values.
Marshalling Win32 BOOL as a 1-byte bool.
Mixing up UTF-16 and UTF-8/ANSI strings.
Ignoring SetLastError and then calling other APIs before reading the error.
Leaking native handles — wrap them in SafeHandle and dispose.
Enabling AllowUnsafeBlocks for convenience across a whole solution.
Best Practices
Prefer Span<T>/MemoryMarshal to pointers; isolate unsafe code in small, tested classes.
Prefer LibraryImport over DllImport in new code.
Mirror native structs exactly and verify sizes with Marshal.SizeOf/sizeof in unit tests.
Wrap vendor SDKs behind a managed interface (ICanInterface) so the rest of the code is testable without hardware.
Interview Relevance
Medium (high for embedded/tools roles): "what does fixed do?", "what are blittable types?", "DllImport vs LibraryImport?", "how do you debug a crash in native interop?", "struct padding and Pack?".
Practice Questions
Define [StructLayout(LayoutKind.Explicit)] so that a uint and four bytes overlay each other.
Call GetComputerNameW with LibraryImport and print the result.
Write a SafeHandle subclass for a fictional vendor_open/vendor_close pair.
Compute a checksum over a byte array with pointer arithmetic, then rewrite it with Span<byte>.
Quick Quiz
Q1. (Multiple choice) What does fixed do? A) Makes a variable constant B) Pins an object so the GC won't move it C) Allocates on the stack D) Disables bounds checks globally Answer: B.
Q2. (Predict the output) Marshal.SizeOf of a sequential struct { byte; uint; ushort; } with Pack = 1.
Answer: 7 (verified).
Q3. (True/False) An unsafe block compiles in any project without extra settings.
Answer: False. It requires AllowUnsafeBlocks; otherwise CS0227 (verified).
Q4. (Find the bug) [LibraryImport("kernel32.dll")] static partial bool SetCurrentDirectoryW(string path);
Answer: The string marshalling isn't specified (use StringMarshalling = StringMarshalling.Utf16), and the Win32 BOOL return needs [return: MarshalAs(UnmanagedType.Bool)].
Coding Challenge
Write an unsafe method that computes the XOR checksum of a byte array using a pinned pointer, and verify it against a safe foreach version.
#:property AllowUnsafeBlocks=true
byte[] frame = [0x02, 0x10, 0x03, 0x7E, 0x55];
Console.WriteLine($"unsafe 0x{XorUnsafe(frame):X2}, safe 0x{XorSafe(frame):X2}");
static unsafe byte XorUnsafe(byte[] data)
{
byte x = 0;
fixed (byte* start = data)
{
for (byte* p = start, end = start + data.Length; p < end; p++)
x ^= *p;
}
return x;
}
static byte XorSafe(byte[] data)
{
byte x = 0;
foreach (byte b in data) x ^= b;
return x;
}