Native Integers in C#: Understanding nint and nuint

This article provides a comprehensive overview of native-sized integers in C#, specifically the nint and nuint types introduced in C# 9. You will learn what these types represent, how the runtime adapts their size and behavior across 32-bit and 64-bit hardware architectures, how they differ from traditional integer types and System.IntPtr, and the primary scenarios where they should be used.

What Are Native Integers?

Native integers, represented by the contextual keywords nint (signed) and nuint (unsigned), are integer types whose storage size matches the natural word size of the host machine's processor.

Under the hood, the C# compiler maps nint and nuint directly to the underlying runtime types System.IntPtr and System.UIntPtr. However, unlike traditional pointers, the compiler treats native integers as standard numeric types, complete with full support for arithmetic operators, comparisons, bitwise operations, and overflow checking.

How Native Integers Work Across Architectures

The primary characteristic of nint and nuint is that their size is determined at execution time based on the process architecture:

  • 32-Bit Environments (x86, ARM32): On 32-bit architectures, nint behaves like a standard 32-bit signed integer (int / System.Int32) with a range of -2,147,483,648 to 2,147,483,647. Similarly, nuint acts as a 32-bit unsigned integer (uint / System.UInt32) ranging from 0 to 4,294,967,295.
  • 64-Bit Environments (x64, ARM64): On 64-bit architectures, nint expands to an 8-byte, 64-bit signed integer equivalent to long (System.Int64), spanning from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Likewise, nuint functions as an 8-byte, 64-bit unsigned integer equivalent to ulong (System.UInt64), ranging from 0 to 18,446,744,073,709,551,615.

Because the underlying representation matches the pointer size of the executing system, you do not need to write architecture-specific code or conditional compilation directives (#if) to adjust integer widths between 32-bit and 64-bit builds.

Language Support vs. IntPtr

Prior to C# 9, developers working with pointer-sized integers relied on System.IntPtr and System.UIntPtr. However, these types lacked standard mathematical operators, requiring verbose casting to and from long or unsafe pointer manipulation to perform basic addition or comparison.

With nint and nuint, native types receive first-class language support:

  • Standard Arithmetic: You can add, subtract, multiply, and divide native integers directly using +, -, *, and /.
  • Bitwise Logic: Bitwise operations such as &, |, ^, <<, and >> work natively without casting.
  • Overflow Checking: Operations honor checked and unchecked contexts. If an arithmetic overflow occurs inside a checked block, an OverflowException is thrown based on the executing architecture's limits.
  • Literal Values: You can assign standard integer literals directly to nint and nuint without explicit casting, provided the value fits within the target's platform constraints.

When to Use nint and nuint

Native integers are primarily designed for performance-critical scenarios and systems-level programming:

  1. Native Interoperability (P/Invoke): When interfacing with C or C++ APIs that use platform-dependent types such as size_t, intptr_t, or uintptr_t, nint and nuint provide a direct, safe mapping.
  2. Pointer Arithmetic and Span Indexing: When performing high-performance memory operations, native integers allow calculating memory offsets and buffer indices without the overhead of converting between 32-bit and 64-bit values.
  3. Low-Level Libraries: Frameworks, graphics engines, and runtime utilities benefit from native integers by maximizing CPU register utilization across different platforms without code duplication.