mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
It's not actually all that hard once you realize: a) the ECMA-335 specification is incorrect, whether to sign- or zero-extend does not always depends on the source type b) the C# compilator generates completely weird code when casting between (U)IntPtr and integers. Map all IntPtr casts to a few semi-sane cases, and we can obtain the correct semantics without introducing any helper methods.pull/728/head
6 changed files with 161 additions and 104 deletions
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
C# casts to/from IntPtr and UIntPtr don't behave like one would expect from the normal C# primitive types. |
||||
For example, they don't fully respect the checked/unchecked context. |
||||
|
||||
First, let's consider what methods we have available for converting between (U)IntPtr and normal C# types. |
||||
|
||||
Primitives for constructing IntPtr/UIntPtr: |
||||
* new IntPtr(int) equivalent to: conv i4->i <sign extend> |
||||
* new IntPtr(long) equivalent to: conv.ovf i8->i |
||||
* new IntPtr(void*) equivalent to: nop |
||||
* new UIntPtr(uint) equivalent to: conv u4->u <zero extend> |
||||
* new UIntPtr(ulong) equivalent to: conv.ovf u8->u |
||||
* new UIntPtr(void*) equivalent to: nop |
||||
|
||||
Primitives for getting the value back out: |
||||
* IntPtr.ToInt32() equivalent to: conv.ovf i->i4 |
||||
* IntPtr.ToInt64() equivalent to: conv i->i8 <sign extend> |
||||
* IntPtr.ToPointer() equivalent to: nop |
||||
* UIntPtr.ToUInt32() equivalent to: conv.ovf u->u4 |
||||
* UIntPtr.ToUInt64() equivalent to: conv u->u8 <zero extend> |
||||
* UIntPtr.ToPointer() equivalent to: nop |
||||
|
||||
The (U)IntPtr.op_Explicit implementations are equivalent to the corresponding primitives. |
||||
(void*) is a useful type because all (U)IntPtr<->void* conversions are no-ops. |
||||
C# pointer types act like a normal C# unsigned integer type (of 'native int' size), so we can |
||||
use `void*` whenever the target type is unsigned or the sign does not matter (overflow checking disabled). |
||||
|
||||
Next, we'll consider what the C# compiler does when casting between integer types and IntPtr. |
||||
I tried all these conversions in both checked and unchecked mode, and the C# compiler was |
||||
always generating the same code in both modes! |
||||
|
||||
OK = cast behavior is as if IntPtr was a built-in type and the context does not matter: |
||||
* sign/zero extension depending on source type |
||||
* never throws OverflowException and never is supposed to |
||||
CC = cast behavior is as if IntPtr was a built-in type and we are in a checked context: |
||||
* sign/zero extension depending on source type |
||||
* performs correct overflowing checking as in a direct cast to native (u)int |
||||
from -> to = C# cast |
||||
generated opcode sequence = what csc.exe produces for that cast |
||||
|
||||
from -> to : generated opcode sequence overall effect equivalent to |
||||
OK short -> IntPtr: call op_Explicit(int32) conv i2->i <sign extend> |
||||
OK ushort -> IntPtr: call op_Explicit(int32) conv u2->u <zero extend> |
||||
Sign extension in op_Explicit does not matter because sign bit is always 0 at that point. |
||||
OK int -> IntPtr: call op_Explicit(int32) conv i4->i <sign extend> |
||||
CC uint -> IntPtr: conv.u8 + call op_Explicit(int64) conv.ovf u4->i <zero extend> |
||||
CC long -> IntPtr: call op_Explicit(int64) conv.ovf i8->i |
||||
ulong -> IntPtr: call op_Explicit(int64) conv.ovf i8->i |
||||
short -> UIntPtr: conv.i8 + call op_Explicit(uint64) 32-bit: conv.ovf i2->u <sign extend>; 64-bit: conv i2->i <sign extend> |
||||
OverflowException for negative input values only on 32-bit! |
||||
OK ushort -> UIntPtr: call op_Explicit(uint32) conv u2->u <zero extend> |
||||
int -> UIntPtr: conv.i8 + call op_Explicit(uint64) 32-bit: conv.ovf i4->u <sign extend>; 64-bit: conv i4->i <sign extend> |
||||
OverflowException for negative input values only on 32-bit! |
||||
OK uint -> UIntPtr: call op_Explicit(uint32) conv u4->u <zero extend> |
||||
long -> UIntPtr: call op_Explicit(uint64) conv.ovf u8->u |
||||
CC ulong -> UIntPtr: call op_Explicit(uint64) conv.ovf u8->u |
||||
|
||||
If an unchecked conversion is desired and the desired entry is not marked 'OK', |
||||
we work around the problem by casting sourceType->void*->(U)IntPtr. |
||||
|
||||
If a checked conversion is desired and the desired entry is not marked 'OK' or 'CC', we have to find a replacement. |
||||
signed type -> UIntPtr: (UIntPtr)(void*)value |
||||
ulong -> IntPtr: (IntPtr)(long)value |
||||
|
||||
Continuing the conversion table for the other direction, (UIntPtr) to primitive types: |
||||
from -> to : generated opcode sequence overall effect equivalent to |
||||
IntPtr -> short: call int32 op_Explicit + conv.i2 conv.ovf i->i4; conv i4->i2 |
||||
IntPtr -> ushort: call int32 op_Explicit + conv.u2 conv.ovf i->i4; conv i4->u2 |
||||
CC IntPtr -> int: call int32 op_Explicit conv.ovf i->i4 |
||||
IntPtr -> uint: call int32 op_Explicit conv.ovf i->i4 |
||||
OK IntPtr -> long: call int64 op_Explicit conv i->i8 <sign extend> |
||||
IntPtr -> ulong: call int64 op_Explicit conv i->i8 <sign extend> |
||||
UIntPtr -> short: call uint32 op_Explicit + conv.i2 conv.ovf u->u4; conv u4->i2 |
||||
UIntPtr -> ushort: call uint32 op_Explicit + conv.u2 conv.ovf u->u4; conv u4->u2 |
||||
UIntPtr -> int: call uint32 op_Explicit conv.ovf u->u4 |
||||
CC UIntPtr -> uint: call uint32 op_Explicit conv.ovf u->u4 |
||||
UIntPtr -> long: call uint64 op_Explicit conv u->u8 <zero extend> |
||||
OK UIntPtr -> ulong: call uint64 op_Explicit conv u->u8 <zero extend> |
||||
|
||||
If an unchecked conversion is desired and the desired entry is not marked 'OK', |
||||
we work around the problem by casting (U)IntPtr->(u)long->targetType. |
||||
(`void*` would also work instead of `ulong`/`long`, but let's avoid unsafe code where possible) |
||||
|
||||
If a checked conversion is desired and the desired entry is not marked 'OK' or 'CC', |
||||
we also have to work around the problem, and this again works by casting via (u)long: (U)IntPtr->(u)long->targetType |
||||
(note that `void*` is not always a valid alternative in this case) |
||||
|
||||
Finally, conversions between IntPtr and (U)IntPtr, or IntPtr and `void*` need special consideration: |
||||
* C# does not allow directly casting IntPtr <-> UIntPtr |
||||
* Casting via `void*` works but is always unchecked. |
||||
* These should work for checked conversions: |
||||
IntPtr -> UIntPtr: cast IntPtr->long->ulong->UIntPtr |
||||
IntPtr -> void*: cast IntPtr->long->void* |
||||
UIntPtr -> IntPtr: cast UIntPtr->ulong->long->IntPtr |
||||
void* -> IntPtr: cast void*->long->IntPtr |
Loading…
Reference in new issue