New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Volatile and atomic operations on unmanaged memory (native pointer dereference, strong type checking) #409

Closed
zpodlovics opened this Issue May 4, 2015 · 2 comments

Comments

Projects
None yet
2 participants
@zpodlovics

zpodlovics commented May 4, 2015

I would like to be able to do volatile and atomic operations on unmanaged memory from .NET in F#. However it seems that there is no way to express the same operations in F# due missing pieces.

The original issue comes from:
dotnet/coreclr#916

Based on the provided example I created sample program in C# which looks working:
https://gist.github.com/zpodlovics/6f4195441f88f01cd0b3

I created a helper method in C# to figure out how the void* type is represented in F# which looks like nativeptr in F# and int32* type looks like nativeptr in F#.

unsafe public static void* ToVoidPtr(IntPtr value) {
  return (void*)value;
}
unsafe public static int* ToInt32Ptr(IntPtr value) {
  return (int*)value;
}
var ptr = Marshal.AllocHGlobal (sizeof(int));
Marshal.WriteInt32 (ptr, 0);
System.Console.WriteLine ("Ptr Old Value (Marshal): {0}", Marshal.ReadInt32(ptr));
var pOld = Interlocked.CompareExchange (ref *(Int32*)ptr, 1, 0);
System.Console.WriteLine ("Ptr Old Value (CompareExchange): {0}", pOld);
System.Console.WriteLine ("Ptr New Value (Marshal): {0}", Marshal.ReadInt32(ptr))

The CompareExchange call IL looks like this:

    IL_0024:  ldloc.0
    IL_0025:  call void* native int::op_Explicit(native int)
    IL_002a:  ldc.i4.1
    IL_002b:  ldc.i4.0
    IL_002c:  call int32 class [mscorlib]System.Threading.Interlocked::CompareExchange([out] int32&, int32, int32)
    IL_0031:  stloc.1

Typed ptr version:

var ptr = Marshal.AllocHGlobal (sizeof(int));
Marshal.WriteInt32 (ptr, 0);
var intPtr = ToInt32Ptr (ptr);
System.Console.WriteLine ("Ptr Old Value (Marshal): {0}", Marshal.ReadInt32(ptr));
var pOld = Interlocked.CompareExchange (ref *intPtr, 1, 0);
System.Console.WriteLine ("Ptr Old Value (CompareExchange): {0}", pOld);
System.Console.WriteLine ("Ptr New Value (Marshal): {0}", Marshal.ReadInt32(ptr));

The CompareExchange call IL looks like this:

    IL_0010:  call int32* class UnmanagedMemory.MainClass::ToInt32Ptr(native int)
    IL_0015:  stloc.1 
    IL_0016:  ldloc.1 
    IL_0017:  ldc.i4.1 
    IL_0018:  ldc.i4.0 
    IL_0019:  call int32 class [mscorlib]System.Threading.Interlocked::CompareExchange([out] int32&, int32, int32)
    IL_001e:  stloc.2 

F# implementation (without the pointer dereference)

    let ptr = Marshal.AllocHGlobal(sizeof<int32>) in   
    Marshal.WriteInt32(ptr, 0);
    System.Console.WriteLine("Old Value: {0}", Marshal.ReadInt32(ptr));
    //C# helper
    //let mutable intPtr = UnmanagedMemoryHelper.MyClass.ToVoidPtr(ptr) in
    let mutable intPtr = NativePtr.ofNativeInt<unit> ptr
    let pOld = Interlocked.CompareExchange(&intPtr, 1, 0);

I also tried with nativeptr<int32>

    let ptr = Marshal.AllocHGlobal(sizeof<int32>) in   
    Marshal.WriteInt32(ptr, 0);
    System.Console.WriteLine("Old Value: {0}", Marshal.ReadInt32(ptr));
    //C# helper
    //let mutable intPtr = UnmanagedMemoryHelper.MyClass.ToInt32Ptr(ptr) in
    let mutable intPtr = NativePtr.ofNativeInt<int32> ptr
    let pOld = Interlocked.CompareExchange(&intPtr, 1, 0);

The closest thing I found in the documentation for dereference is the NativePtr.read but this will copy the value from the ptr to a new variable.

https://msdn.microsoft.com/en-us/library/ee353827.aspx

    [<NoDynamicInvocation>]
    [<CompiledName("ReadPointerInlined")>]
    let inline read (p : nativeptr<'T>) = (# "ldobj !0" type ('T) p : 'T #) 
    let ptr = Marshal.AllocHGlobal(sizeof<int32>) in   
    Marshal.WriteInt32(ptr, 0);
    let mutable intPtr = UnmanagedMemoryHelper.MyClass.ToInt32Ptr(ptr);
    System.Console.WriteLine("Old Value: {0}", Marshal.ReadInt32(ptr));
    let mutable intVal = (NativePtr.read intPtr)
    let pOld = Interlocked.CompareExchange(&intVal, 1, 0);
    System.Console.WriteLine("New Value: {0}", Marshal.ReadInt32(ptr));

The output will be:

Old Value: 0
New Value: 0

Instead of:

Old Value: 0
New Value: 1

Maybe I miss something but it seems that I cannot express the same operations in F#. Is there any way to express a pointer dereference in F#? Is there any way to express the same operations in F# that is available in the C# sample?

@zpodlovics

This comment has been minimized.

zpodlovics commented May 4, 2015

Looks like I found a way to make an int32& from a nativeptr<int32>. Could you please confirm that a pointer dereference is nothing more than a cast from int32* to int32& ? It looks working, however I would like to have some feedback about this.

NativePtr helper:

module NativePtrExtension = begin
    [<NoDynamicInvocation>]
    let inline toByref (x: nativeptr<'T>) = (# "" x : 'T byref  #)
end
    let ptr = Marshal.AllocHGlobal(sizeof<int32>) in   
    Marshal.WriteInt32(ptr, 0);
    let mutable intPtr = NativePtr.ofNativeInt ptr
    System.Console.WriteLine("Old Value: {0}", Marshal.ReadInt32(ptr));
    let pOld = Interlocked.CompareExchange(NativePtrExtension.toByref intPtr, 1, 0);
    System.Console.WriteLine("New Value: {0}", Marshal.ReadInt32(ptr));

The generated IL looks like this:

    .method public static 
           default int32 main (string[] argv)  cil managed 
    {
        .custom instance void class [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::'.ctor'() =  (01 00 00 00 ) // ....

        // Method begins at RVA 0x2050
    .entrypoint
    // Code size 75 (0x4b)
    .maxstack 5
    .locals init (
        native int  V_0,
        native int  V_1,
        int32   V_2)
    IL_0000:  nop 
    IL_0001:  sizeof [mscorlib]System.Int32
    IL_0007:  call native int class [mscorlib]System.Runtime.InteropServices.Marshal::AllocHGlobal(int32)
    IL_000c:  stloc.0 
    IL_000d:  ldloc.0 
    IL_000e:  ldc.i4.0 
    IL_000f:  call void class [mscorlib]System.Runtime.InteropServices.Marshal::WriteInt32(native int, int32)
    IL_0014:  ldloc.0 
    IL_0015:  stloc.1 
    IL_0016:  ldstr "Old Value: {0}"
    IL_001b:  ldloc.0 
    IL_001c:  call int32 class [mscorlib]System.Runtime.InteropServices.Marshal::ReadInt32(native int)
    IL_0021:  box [mscorlib]System.Int32
    IL_0026:  call void class [mscorlib]System.Console::WriteLine(string, object)
    IL_002b:  ldloc.1 
    IL_002c:  ldc.i4.1 
    IL_002d:  ldc.i4.0 
    IL_002e:  call int32 class [mscorlib]System.Threading.Interlocked::CompareExchange([out] int32&, int32, int32)
    IL_0033:  stloc.2 
    IL_0034:  ldstr "New Value: {0}"
    IL_0039:  ldloc.0 
    IL_003a:  call int32 class [mscorlib]System.Runtime.InteropServices.Marshal::ReadInt32(native int)
    IL_003f:  box [mscorlib]System.Int32
    IL_0044:  call void class [mscorlib]System.Console::WriteLine(string, object)
    IL_0049:  ldc.i4.0 
    IL_004a:  ret 
    } // end of method Program::main

@dsyme

This comment has been minimized.

Contributor

dsyme commented May 7, 2015

Yes, this looks ok, I'll close this as a workaround unless we hear otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment