Ayende @ Rahien

Unnatural acts on source code

Talk about nasty bugs

Let us see how many of you can figure this one out.

Here is the code:

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName,
   IntPtr lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel,
   CopyFileFlags dwCopyFlags);

public void CopyFileWithProgressReports()
{
    var lpProgressRoutineIntPtr = Marshal.GetFunctionPointerForDelegate(new CopyProgressRoutine(LpProgressRoutine));
    int pbCancel = 0;
    CopyFileEx(hugeFile, hugeFile + ".new", lpProgressRoutineIntPtr, IntPtr.Zero, ref pbCancel,
           CopyFileFlags.COPY_FILE_RESTARTABLE);        
}

private static CopyProgressResult LpProgressRoutine(long totalFileSize, long totalBytesTransferred, long streamSize, 
    long streamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason, IntPtr hSourceFile, 
    IntPtr hDestinationFile, IntPtr lpData)
{
    Console.WriteLine("{0:#,#} / {1:#,#}", totalBytesTransferred, totalFileSize);
    return CopyProgressResult.PROGRESS_CONTINUE;
}

This code will run perfectly if you execute it in a single threaded fashion.

But, if you run it on a background thread and continue to do additional operations (just running it on a background thread work) that has nothing to do with the file system, it will crash, sometimes with a null reference exception, sometimes with attempt to write to protected memory, etc.

There is a very subtle bug here, can you figure out what it is?

Comments

Abc Dee
05/04/2010 08:41 PM by
Abc Dee

static & threading = trouble

OmariO
05/04/2010 08:43 PM by
OmariO

I don't know how it related to threading but one of the reasons can be that you don't keep reference to the delegate.

dave-ilsw
05/04/2010 08:52 PM by
dave-ilsw

If you were updating a graphical progress bar, the call to update it would have to be invoked to avoid cross-threading issues. Does that also apply when writing to the console?

Michael Davis
05/04/2010 08:52 PM by
Michael Davis

Seconding that not keeping a reference to the delegate to prevent it from being garbage collected before CopyFileEx() is done with it could be a potential problem. I've been burned by that before myself.

By the way, is there really a need to manually call Marshal.GetFunctionPointerForDelegate()? I'm pretty sure I've been able to specify an actual delegate type in a P/Invoke signature and the framework handled the conversion on its own.

John
05/04/2010 08:58 PM by
John

Could it be that you've got SetLastError set to true, are not calling GetLastWin32Error() anywhere, and when more than one thread is running there are errors trying to be set?

Andrew
05/04/2010 09:18 PM by
Andrew

You're not using a SafeHandle?

Ayende Rahien
05/04/2010 09:28 PM by
Ayende Rahien

OmariO & Michael,

Wow, that was fast.

Yep that is the problem.

Phil
05/04/2010 09:44 PM by
Phil

Dude! You blog so much that I wonder how you ever get work done.

tobi
05/04/2010 10:23 PM by
tobi

The reason why it only crashes if there is additional work done is that the garbage collector only runs if an allocation occurs.

dave-ilsw
05/04/2010 10:49 PM by
dave-ilsw
  1. I've never used Marshall methods. I take it that the GetFunctionPointerForDelegate method causes the callback method to be invoked in a thread-safe manner?

  2. What is the solution? Make var lpProgressRoutineIntPtr a class variable instead of a local?

Martin
05/04/2010 11:49 PM by
Martin

I suspect that if you were to run this on Mono, you WOULDN'T see this problem because it employs a conservative GC.

Laurion Burchall
05/05/2010 04:04 AM by
Laurion Burchall

Hmmm, looks familiar ;-)

Is this actually broken with a static method as well as member methods? What does reflector say the actual code looks like?

Ayende Rahien
05/05/2010 06:08 AM by
Ayende Rahien

Yes, it does.

The only important thing is that it isn't rooted

Comments have been closed on this topic.