IntPtr
SafeHandle
HandleRef
.NET
memory management

IntPtr, SafeHandle and HandleRef - Explained

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

IntPtr, SafeHandle, and HandleRef all show up in .NET interop code, but they are not interchangeable. Each one answers a different question: how do I carry a native value, how do I own and release it safely, and how do I keep a managed wrapper alive while native code runs.

Once you separate those concerns, the choice becomes much clearer. Most bugs come from using a raw IntPtr where ownership or lifetime should have been modeled explicitly.

What IntPtr Actually Represents

IntPtr is the low-level building block. It is a value type large enough to store a pointer or handle on the current platform, so it works on both 32-bit and 64-bit processes.

In practice, IntPtr is just a number-sized container. It does not know whether the value points to memory, a window handle, a file handle, or an invalid resource. It also does not know how to free anything.

csharp
1using System;
2using System.Runtime.InteropServices;
3
4internal static class NativeMethods
5{
6    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
7    public static extern IntPtr FindWindow(string? className, string windowName);
8}
9
10IntPtr handle = NativeMethods.FindWindow(null, "Untitled - Notepad");
11
12if (handle == IntPtr.Zero)
13{
14    Console.WriteLine("Window not found");
15}
16else
17{
18    Console.WriteLine($"Handle: {handle}");
19}

This is fine when you only need to pass a native value around briefly. The problem starts when the handle owns a resource that must be released.

Why SafeHandle Is Usually the Right Choice

SafeHandle wraps a native handle in a managed type that knows how to clean itself up. That makes it the preferred option for most P/Invoke signatures that return an owned handle.

The main benefit is reliability. A SafeHandle can release the native resource even when exceptions occur, and the runtime treats it specially during finalization. That is much safer than storing a raw IntPtr and hoping every code path calls the matching cleanup function.

csharp
1using Microsoft.Win32.SafeHandles;
2using System;
3using System.ComponentModel;
4using System.Runtime.InteropServices;
5
6internal static class Kernel32
7{
8    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
9    public static extern SafeFileHandle CreateFile(
10        string fileName,
11        uint desiredAccess,
12        uint shareMode,
13        IntPtr securityAttributes,
14        uint creationDisposition,
15        uint flagsAndAttributes,
16        IntPtr templateFile);
17}
18
19SafeFileHandle fileHandle = Kernel32.CreateFile(
20    "example.txt",
21    0x80000000,
22    1,
23    IntPtr.Zero,
24    3,
25    0,
26    IntPtr.Zero);
27
28if (fileHandle.IsInvalid)
29{
30    throw new Win32Exception(Marshal.GetLastWin32Error());
31}
32
33using (fileHandle)
34{
35    Console.WriteLine("Handle opened safely");
36}

If you own the handle, use SafeHandle whenever possible. It pushes the cleanup rule into the type itself instead of leaving it scattered across callers.

What HandleRef Solves

HandleRef is more specialized. It wraps an IntPtr together with a managed owner object so that the owner is kept alive for the duration of the native call.

That matters when a managed object stores a handle internally and exposes it to P/Invoke. Without HandleRef, the runtime could collect the wrapper object too early if nothing else references it strongly during the call.

csharp
1using System;
2using System.Runtime.InteropServices;
3
4internal static class NativePainter
5{
6    [DllImport("gdi32.dll")]
7    public static extern int DeleteObject(HandleRef handle);
8}
9
10public sealed class NativeBitmap
11{
12    private readonly IntPtr handle;
13
14    public NativeBitmap(IntPtr handle)
15    {
16        this.handle = handle;
17    }
18
19    public void Release()
20    {
21        NativePainter.DeleteObject(new HandleRef(this, handle));
22    }
23}

HandleRef does not free the resource for you. It only keeps the wrapper object alive while the native call executes. That is why it is less common in modern code than SafeHandle.

Where GC.KeepAlive Fits

Modern interop code sometimes uses GC.KeepAlive(owner) instead of HandleRef when the signature itself does not need a HandleRef parameter. The idea is similar: make sure the wrapper object stays alive until after the native call returns.

That does not make GC.KeepAlive a replacement for SafeHandle. It only affects object lifetime in managed code; it does not add cleanup semantics or invalid-handle checks.

Which One Should You Use

A good rule is simple:

  • Use IntPtr for raw pointers, opaque values, or temporary interop boundaries.
  • Use SafeHandle when the native handle has an ownership and cleanup rule.
  • Use HandleRef when you already have an object-plus-handle design and only need to protect that object from premature collection during a P/Invoke call.

If you are writing new interop code today, start by asking whether the API can use SafeHandle. Many bugs disappear immediately when lifetime is modeled explicitly instead of being handled manually.

Common Pitfalls

  • Treating IntPtr as if it automatically manages memory or native handles. It does not.
  • Returning an owned native handle as IntPtr and forgetting to release it on one error path.
  • Using HandleRef or GC.KeepAlive as if either one were a cleanup mechanism. They only affect lifetime during the call.
  • Writing new interop layers with raw handles even when SafeHandle would express the ownership model more clearly.

Summary

  • 'IntPtr is a raw pointer-sized value with no built-in ownership semantics.'
  • 'SafeHandle is the preferred choice for owned native handles because it models cleanup safely.'
  • 'HandleRef keeps a managed wrapper alive during a native call that uses its internal handle.'
  • Most modern P/Invoke code should favor SafeHandle over a plain IntPtr.
  • Pick the type based on the real lifetime problem you need to solve, not only on what compiles.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.