9

Parhaps a silly question... I'm new to C# and .Net.

In the example for the SafeHandle class (C#) on MSDN, the code made me scratch my head a bit.

[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
internal class MySafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private MySafeFileHandle()
      : base(true)
    {}
    // other code here
}

[SuppressUnmanagedCodeSecurity()]
internal static class NativeMethods
{
    // other code...

    // Allocate a file object in the kernel, then return a handle to it.
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    internal extern static MySafeFileHandle CreateFile(String fileName,
       int dwDesiredAccess, System.IO.FileShare dwShareMode,
       IntPtr securityAttrs_MustBeZero, System.IO.FileMode    
       dwCreationDisposition, int dwFlagsAndAttributes, 
       IntPtr hTemplateFile_MustBeZero);

    // other code...
}

// Later in the code the handle is created like this:
MySafeFileHandle tmpHandle;
tmpHandle = NativeMethods.CreateFile(fileName, NativeMethods.GENERIC_READ,
            FileShare.Read, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

My question is: How does the Win32 HANDLE from the C function CreateFile get into the MySafeFileHandle objects protected IntPtr "handle" variable? The constructor of MySafeFileHandle is private and doesn't even take an IntPtr as an argument!

The comment just over the CreateFile statement says something about

… the CLR's platform marshalling layer will store the handle into the SafeHandle object in an atomic fashion.

I'm not sure I know exactly what this means, can anyone explain please?

Karl Hansson
  • 227
  • 1
  • 2
  • 9
  • 5
    Basically, it's magic. The runtime "knows" about `SafeHandle` and how to stuff `IntPtr`s into it. The runtime is not, of course, bound to play by the rules of constructors. – Jeroen Mostert Sep 04 '18 at 18:15
  • 2
    Similarly, the C# compiler can create code that isn't legal C#. Just because *you* have to play by the rules doesn't mean other people have to. – Damien_The_Unbeliever Sep 04 '18 at 18:23
  • 6
    CreateFile() does not return a safe handle. It is a purely unmanaged function that doesn't know beans about .NET objects. But yet the [DllImport] declaration says it does. It is now the job of the pinvoke marshaller, the chunk of code inside the CLR that makes native function calls, to convert the IntPtr to the MySafeFileHandle object. It knows lots of conversion tricks, this is just one of them. – Hans Passant Sep 04 '18 at 18:27
  • 3
    Not a dumb question at all, by the way. The interop wrapping around certain unmanaged objects is some of the most complex stuff in the base runtime, imo. Some of the finalizer tricks to make known handle types behave in an intuitive fashion get...nasty. Fortunately this only becomes a problem for the typical user when dealing with handle types that are *not* known to the CLR. (Such as a handle to a truly limited resource within custom hardware) – zzxyz Sep 04 '18 at 18:43

1 Answers1

4

Short answer: it's magic. The runtime knows how to properly convert unmanaged handles (which are just pointer-sized values) to SafeHandle and back.

Long answer: it's sufficiently advanced technology. Specifically, ILSafeHandleMarshaler is the (unmanaged!) class that takes care of marshaling SafeHandles back and forth. The source code helpfully summarizes the process:

// 1) create local for new safehandle
// 2) prealloc a safehandle
// 3) create local to hold returned handle
// 4) [byref] add byref IntPtr to native sig
// 5) [byref] pass address of local as last arg
// 6) store return value in safehandle

The code it emits to load the unmanaged handle into the safe handle is actually managed code, albeit managed code that happily ignores accessibility. It gets and invokes the default constructor to create a new instance:

MethodDesc* pMDCtor = pMT->GetDefaultConstructor();
pslIL->EmitNEWOBJ(pslIL->GetToken(pMDCtor), 0);
pslIL->EmitSTLOC(dwReturnHandleLocal);   

And then it directly sets the SafeHandle.handle field:

mdToken tkNativeHandleField = 
    pslPostIL->GetToken(MscorlibBinder::GetField(FIELD__SAFE_HANDLE__HANDLE));
...

// 6) store return value in safehandle
pslCleanupIL->EmitLDLOC(dwReturnHandleLocal);
pslCleanupIL->EmitLDLOC(dwReturnNativeHandleLocal);
pslCleanupIL->EmitSTFLD(tkNativeHandleField);

Neither the constructor nor the handle field are actually accessible, but this code isn't subject to visibility checks.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
  • 1
    I deleted my answer because I thought it might be misleading, but I do think it's worth noting that it's relatively easy for any C# code to ignore accessibility. This can happen through `unsafe` and pointer arithmetic (like C++), or through `reflection`. – zzxyz Sep 05 '18 at 17:30
  • 2
    @zzxyz: of course -- unsafe managed code can do whatever it likes, including scribbling over arbitrary memory. However, it's also important to note that most of the hairy bits of marshaling are not written in managed code (not even unsafe) because it needs to interact with the garbage collector and the state of the managed runtime in an interesting way. Some things cannot be written in pure managed code (even unsafe) and still be correct. This bit of marshaling emits some managed instructions, but there's a bunch of interesting unmanaged stuff surrounding it I've omitted. – Jeroen Mostert Sep 05 '18 at 18:03
  • 1
    Excellent. Thanks for the comment. That clarifies things for me a bit. Hopefully it helps the OP too. And you got to use `omit` and `emit` in the same sentence! – zzxyz Sep 05 '18 at 18:47
  • Thanks for your answers! I kept looking around to see if there were some fancy implicit assignment/conversion thingy that I didn't know about. This explains things a bit though. – Karl Hansson Sep 05 '18 at 20:25