Language support for a native-sized signed and unsigned integer types.
The motivation is for interop scenarios and for low-level libraries.
The identifiers nint
and nuint
are new contextual keywords that represent native signed and unsigned integer types.
The identifiers are only treated as keywords when name lookup does not find a viable result at that program location.
nint x = 3;
string y = nameof(nuint);
_ = nint.Equals(x, 3);
The types nint
and nuint
are represented by the underlying types System.IntPtr
and System.UIntPtr
with compiler surfacing additional conversions and operations for those types as native ints.
Constant expressions may be of type nint
or nuint
.
There is no direct syntax for native int literals. Implicit or explicit casts of other integral constant values can be used instead: const nint i = (nint)42;
.
nint
constants are in the range [ int.MinValue
, int.MaxValue
].
nuint
constants are in the range [ uint.MinValue
, uint.MaxValue
].
There are no MinValue
or MaxValue
fields on nint
or nuint
because, other than nuint.MinValue
, those values cannot be emitted as constants.
Constant folding is supported for all unary operators { +
, -
, ~
} and binary operators { +
, -
, *
, /
, %
, ==
, !=
, <
, <=
, >
, >=
, &
, |
, ^
, <<
, >>
}.
Constant folding operations are evaluated with Int32
and UInt32
operands rather than native ints for consistent behavior regardless of compiler platform.
If the operation results in a constant value in 32-bits, constant folding is performed at compile-time.
Otherwise the operation is executed at runtime and not considered a constant.
There is an identity conversion between nint
and IntPtr
, and between nuint
and UIntPtr
.
There is an identity conversion between compound types that differ by native ints and underlying types only: arrays, Nullable<>
, constructed types, and tuples.
The tables below cover the conversions between special types.
(The IL for each conversion includes the variants for unchecked
and checked
contexts if different.)
Operand | Target | Conversion | IL |
---|---|---|---|
object |
nint |
Unboxing | unbox |
void* |
nint |
PointerToVoid | conv.i |
sbyte |
nint |
ImplicitNumeric | conv.i |
byte |
nint |
ImplicitNumeric | conv.u |
short |
nint |
ImplicitNumeric | conv.i |
ushort |
nint |
ImplicitNumeric | conv.u |
int |
nint |
ImplicitNumeric | conv.i |
uint |
nint |
ExplicitNumeric | conv.u / conv.ovf.u |
long |
nint |
ExplicitNumeric | conv.i / conv.ovf.i |
ulong |
nint |
ExplicitNumeric | conv.i / conv.ovf.i |
char |
nint |
ImplicitNumeric | conv.i |
float |
nint |
ExplicitNumeric | conv.i / conv.ovf.i |
double |
nint |
ExplicitNumeric | conv.i / conv.ovf.i |
decimal |
nint |
ExplicitNumeric | long decimal.op_Explicit(decimal) conv.i / ... conv.ovf.i |
IntPtr |
nint |
Identity | |
UIntPtr |
nint |
None | |
object |
nuint |
Unboxing | unbox |
void* |
nuint |
PointerToVoid | conv.u |
sbyte |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
byte |
nuint |
ImplicitNumeric | conv.u |
short |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
ushort |
nuint |
ImplicitNumeric | conv.u |
int |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
uint |
nuint |
ImplicitNumeric | conv.u |
long |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
ulong |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
char |
nuint |
ImplicitNumeric | conv.u |
float |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
double |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
decimal |
nuint |
ExplicitNumeric | ulong decimal.op_Explicit(decimal) conv.u / ... conv.ovf.u.un |
IntPtr |
nuint |
None | |
UIntPtr |
nuint |
Identity |
Operand | Target | Conversion | IL |
---|---|---|---|
nint |
object |
Boxing | box |
nint |
void* |
PointerToVoid | conv.i |
nint |
nuint |
ExplicitNumeric | conv.u / conv.ovf.u |
nint |
sbyte |
ExplicitNumeric | conv.i1 / conv.ovf.i1 |
nint |
byte |
ExplicitNumeric | conv.u1 / conv.ovf.u1 |
nint |
short |
ExplicitNumeric | conv.i2 / conv.ovf.i2 |
nint |
ushort |
ExplicitNumeric | conv.u2 / conv.ovf.u2 |
nint |
int |
ExplicitNumeric | conv.i4 / conv.ovf.i4 |
nint |
uint |
ExplicitNumeric | conv.u4 / conv.ovf.u4 |
nint |
long |
ImplicitNumeric | conv.i8 / conv.ovf.i8 |
nint |
ulong |
ExplicitNumeric | conv.i8 / conv.ovf.i8 |
nint |
char |
ExplicitNumeric | conv.u2 / conv.ovf.u2 |
nint |
float |
ImplicitNumeric | conv.r4 |
nint |
double |
ImplicitNumeric | conv.r8 |
nint |
decimal |
ImplicitNumeric | conv.i8 decimal decimal.op_Implicit(long) |
nint |
IntPtr |
Identity | |
nint |
UIntPtr |
None | |
nuint |
object |
Boxing | box |
nuint |
void* |
PointerToVoid | conv.u |
nuint |
nint |
ExplicitNumeric | conv.i / conv.ovf.i |
nuint |
sbyte |
ExplicitNumeric | conv.i1 / conv.ovf.i1 |
nuint |
byte |
ExplicitNumeric | conv.u1 / conv.ovf.u1 |
nuint |
short |
ExplicitNumeric | conv.i2 / conv.ovf.i2 |
nuint |
ushort |
ExplicitNumeric | conv.u2 / conv.ovf.u2 |
nuint |
int |
ExplicitNumeric | conv.i4 / conv.ovf.i4 |
nuint |
uint |
ExplicitNumeric | conv.u4 / conv.ovf.u4 |
nuint |
long |
ExplicitNumeric | conv.i8 / conv.ovf.i8 |
nuint |
ulong |
ImplicitNumeric | conv.u8 / conv.ovf.u8 |
nuint |
char |
ExplicitNumeric | conv.u2 / conv.ovf.u2.un |
nuint |
float |
ImplicitNumeric | conv.r.un conv.r4 |
nuint |
double |
ImplicitNumeric | conv.r.un conv.r8 |
nuint |
decimal |
ImplicitNumeric | conv.u8 decimal decimal.op_Implicit(ulong) |
nuint |
IntPtr |
None | |
nuint |
UIntPtr |
Identity |
Conversion from A
to Nullable<B>
is:
- an implicit nullable conversion if there is an identity conversion or implicit conversion from
A
toB
; - an explicit nullable conversion if there is an explicit conversion from
A
toB
; - otherwise invalid.
Conversion from Nullable<A>
to B
is:
- an explicit nullable conversion if there is an identity conversion or implicit or explicit numeric conversion from
A
toB
; - otherwise invalid.
Conversion from Nullable<A>
to Nullable<B>
is:
- an identity conversion if there is an identity conversion from
A
toB
; - an explicit nullable conversion if there is an implicit or explicit numeric conversion from
A
toB
; - otherwise invalid.
The predefined operators are as follows.
These operators are considered during overload resolution based on normal rules for implicit conversions if at least one of the operands is of type nint
or nuint
.
(The IL for each operator includes the variants for unchecked
and checked
contexts if different.)
Unary | Operator Signature | IL |
---|---|---|
+ |
nint operator +(nint value) |
nop |
+ |
nuint operator +(nuint value) |
nop |
- |
nint operator -(nint value) |
neg |
~ |
nint operator ~(nint value) |
not |
~ |
nuint operator ~(nuint value) |
not |
Binary | Operator Signature | IL |
---|---|---|
+ |
nint operator +(nint left, nint right) |
add / add.ovf |
+ |
nuint operator +(nuint left, nuint right) |
add / add.ovf.un |
- |
nint operator -(nint left, nint right) |
sub / sub.ovf |
- |
nuint operator -(nuint left, nuint right) |
sub / sub.ovf.un |
* |
nint operator *(nint left, nint right) |
mul / mul.ovf |
* |
nuint operator *(nuint left, nuint right) |
mul / mul.ovf.un |
/ |
nint operator /(nint left, nint right) |
div |
/ |
nuint operator /(nuint left, nuint right) |
div.un |
% |
nint operator %(nint left, nint right) |
rem |
% |
nuint operator %(nuint left, nuint right) |
rem.un |
== |
bool operator ==(nint left, nint right) |
beq / ceq |
== |
bool operator ==(nuint left, nuint right) |
beq / ceq |
!= |
bool operator !=(nint left, nint right) |
bne |
!= |
bool operator !=(nuint left, nuint right) |
bne |
< |
bool operator <(nint left, nint right) |
blt / clt |
< |
bool operator <(nuint left, nuint right) |
blt.un / clt.un |
<= |
bool operator <=(nint left, nint right) |
ble |
<= |
bool operator <=(nuint left, nuint right) |
ble.un |
> |
bool operator >(nint left, nint right) |
bgt / cgt |
> |
bool operator >(nuint left, nuint right) |
bgt.un / cgt.un |
>= |
bool operator >=(nint left, nint right) |
bge |
>= |
bool operator >=(nuint left, nuint right) |
bge.un |
& |
nint operator &(nint left, nint right) |
and |
& |
nuint operator &(nuint left, nuint right) |
and |
| |
nint operator |(nint left, nint right) |
or |
| |
nuint operator |(nuint left, nuint right) |
or |
^ |
nint operator ^(nint left, nint right) |
xor |
^ |
nuint operator ^(nuint left, nuint right) |
xor |
<< |
nint operator <<(nint left, int right) |
shl |
<< |
nuint operator <<(nuint left, int right) |
shl |
>> |
nint operator >>(nint left, int right) |
shr |
>> |
nuint operator >>(nuint left, int right) |
shr.un |
For some binary operators, the IL operators support additional operand types (see ECMA-335 III.1.5 Operand type table). But the set of operand types supported by C# is limited for simplicity and for consistency with existing operators in the language.
Lifted versions of the operators, where the arguments and return types are nint?
and nuint?
, are supported.
Compound assignment operations x op= y
where x
or y
are native ints follow the same rules as with other primitive types with pre-defined operators.
Specifically the expression is bound as x = (T)(x op y)
where T
is the type of x
and where x
is only evaluated once.
The shift operators should mask the number of bits to shift - to 5 bits if sizeof(nint)
is 4, and to 6 bits if sizeof(nint)
is 8.
(see shift operators in C# spec).
The conversions and operators are synthesized by the compiler and are not part of the underlying IntPtr
and UIntPtr
types.
As a result those conversions and operators are not available from the runtime binder for dynamic
.
nint x = 2;
nint y = x + x; // ok
dynamic d = x;
nint z = d + x; // RuntimeBinderException: '+' cannot be applied 'System.IntPtr' and 'System.IntPtr'
The only constructor for nint
or nuint
is the parameter-less constructor.
The following members of System.IntPtr
and System.UIntPtr
are explicitly excluded from nint
or nuint
:
// constructors
// arithmetic operators
// implicit and explicit conversions
public static readonly IntPtr Zero; // use 0 instead
public static int Size { get; } // use sizeof() instead
public static IntPtr Add(IntPtr pointer, int offset);
public static IntPtr Subtract(IntPtr pointer, int offset);
public int ToInt32();
public long ToInt64();
public void* ToPointer();
The remaining members of System.IntPtr
and System.UIntPtr
are implicitly included in nint
and nuint
. For .NET Framework 4.7.2:
public override bool Equals(object obj);
public override int GetHashCode();
public override string ToString();
public string ToString(string format);
Interfaces implemented by System.IntPtr
and System.UIntPtr
are implicitly included in nint
and nuint
,
with occurrences of the underlying types replaced by the corresponding native integer types.
For instance if IntPtr
implements ISerializable, IEquatable<IntPtr>, IComparable<IntPtr>
,
then nint
implements ISerializable, IEquatable<nint>, IComparable<nint>
.
nint
and System.IntPtr
, and nuint
and System.UIntPtr
, are considered equivalent for overriding, hiding, and implementing.
Overloads cannot differ by nint
and System.IntPtr
, and nuint
and System.UIntPtr
, alone.
Overrides and implementations may differ by nint
and System.IntPtr
, or nuint
and System.UIntPtr
, alone.
Methods hide other methods that differ by nint
and System.IntPtr
, or nuint
and System.UIntPtr
, alone.
nint
and nuint
expressions used as array indices are emitted without conversion.
static object GetItem(object[] array, nint index)
{
return array[index]; // ok
}
nint
and nuint
can be used as an enum
base type.
enum E : nint // ok
{
}
Reads and writes are atomic for types nint
, nuint
, and enum
with base type nint
or nuint
.
Fields may be marked volatile
for types nint
and nuint
.
ECMA-334 15.5.4 does not include enum
with base type System.IntPtr
or System.UIntPtr
however.
default(nint)
and new nint()
are equivalent to (nint)0
.
typeof(nint)
is typeof(IntPtr)
.
sizeof(nint)
is supported but requires compiling in an unsafe context (as does sizeof(IntPtr)
).
The value is not a compile-time constant.
sizeof(nint)
is implemented as sizeof(IntPtr)
rather than IntPtr.Size
.
Compiler diagnostics for type references involving nint
or nuint
report nint
or nuint
rather than IntPtr
or UIntPtr
.
nint
and nuint
are represented in metadata as System.IntPtr
and System.UIntPtr
.
Type references that include nint
or nuint
are emitted with a System.Runtime.CompilerServices.NativeIntegerAttribute
to indicate which parts of the type reference are native ints.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.GenericParameter |
AttributeTargets.Parameter |
AttributeTargets.Property |
AttributeTargets.ReturnValue,
AllowMultiple = false,
Inherited = false)]
public sealed class NativeIntegerAttribute : Attribute
{
public NativeIntegerAttribute()
{
TransformFlags = new[] { true };
}
public NativeIntegerAttribute(bool[] flags)
{
TransformFlags = flags;
}
public IList<bool> TransformFlags { get; }
}
}
The encoding uses the approach as used to encode DynamicAttribute
, although obviously DynamicAttribute
is encoding which types within the type reference are dynamic
rather than which types are native ints.
If the encoding results in an array of false
values, no NativeIntegerAttribute
is needed.
The parameterless NativeIntegerAttribute
constructor generates an encoding with a single true
value.
nuint A; // [NativeInteger] UIntPtr A
(Stream, nint) B; // [NativeInteger(new[] { false, false, true })] ValueType<Stream, IntPtr> B
An alternative to the "type erasure" approach above is to introduce new types: System.NativeInt
and System.NativeUInt
.
public readonly struct NativeInt
{
public IntPtr Value;
}
Distinct types would allow overloading distinct from IntPtr
and would allow distinct parsing and ToString()
.
But there would be more work for the CLR to handle these types efficiently which defeats the primary purpose of the feature - efficiency.
And interop with existing native int code that uses IntPtr
would be more difficult.
Another alternative is to add more native int support for IntPtr
in the framework but without any specific compiler support.
Any new conversions and arithmetic operations would be supported by the compiler automatically.
But the language would not provide keywords, constants, or checked
operations.
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-05-26.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-06-13.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-07-05.md#native-int-and-intptr-operators https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-10-23.md https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-03-25.md