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.
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.
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.
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.
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
IntPtrfor raw pointers, opaque values, or temporary interop boundaries. - Use
SafeHandlewhen the native handle has an ownership and cleanup rule. - Use
HandleRefwhen 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
IntPtras if it automatically manages memory or native handles. It does not. - Returning an owned native handle as
IntPtrand forgetting to release it on one error path. - Using
HandleReforGC.KeepAliveas if either one were a cleanup mechanism. They only affect lifetime during the call. - Writing new interop layers with raw handles even when
SafeHandlewould express the ownership model more clearly.
Summary
- '
IntPtris a raw pointer-sized value with no built-in ownership semantics.' - '
SafeHandleis the preferred choice for owned native handles because it models cleanup safely.' - '
HandleRefkeeps a managed wrapper alive during a native call that uses its internal handle.' - Most modern P/Invoke code should favor
SafeHandleover a plainIntPtr. - Pick the type based on the real lifetime problem you need to solve, not only on what compiles.
Related reading
- Intuitive explanation for why QuickSort is n log n?
- IOPS vs Throughput. Which one to use while choosing AWS EBS
- iOS 5 Best Practice Release/retain?
- iOS 6 apps - how to deal with iPhone 5 screen size?
- Invalid cast from ''System.Int32'' to ''System.Nullable1System.Int32, mscorlib
- Invalid postback or callback argument. Event validation is enabled using 'pages enableEventValidationtrue/
- ios app maximum memory budget
- ios crash EXC_BAD_ACCESS KERN_INVALID_ADDRESS

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 courseTrack 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.