Calling WinInet Asynchronously from managed code

Hi, I’m Reymarx Gereda, SDET at the Networking Development Platform in the Windows team. Recently I saw a question in one of the MSDN forums about calling WinInet API from managed functions; this is something we did for testing the component, and here I want to share something we found when calling some of the WinInet APIs in an asynchronous fashion. I’ll talk about specifically of the use of InternetReadFile in this mode. I assume you understand the asynchronous behavior of WinInet and a basic knowledge of interacting with native components using PInvoke.

When calling functions in WinInet from managed code using PInvoke, you can map the buffer arguments to managed byte arrays (byte[]) and pass them to the functions without having to pin the memory used by those buffers. For example, for InternetReadFile, the native signature is as follows:

 BOOL InternetReadFile(
  HINTERNET hFile,
  LPVOID lpBuffer,
  DWORD dwNumberOfBytesToRead,
  LPDWORD lpdwNumberOfBytesRead
);
 

And the PInvoke declaration could look like this:

 [DllImport("WinInet", SetLastError = true, ExactSpelling = true)]
internal extern static bool InternetReadFile(
    IntPtr hFile, 
    byte[] buffer, 
    Int32 numberOfBytesToRead, 
    ref Int32 numberOfBytesRead
);
 

This signature works really well in the synchronous case, since the marshaler takes care of pinning the memory used by the buffer for the duration of the function call.

For the asynchronous case, when the function returns ERROR_IO_PENDING, WinInet requires access, at the completion of the call, to the buffer passed in the function call; and it could happen that the memory location for buffer used in the function call, has changed due to the work of the garbage collector. In this case, you’re responsible for pinning the memory used by the buffer until the asynchronous call completes. In the case of InternetReadFile, WinInet needs to update the information inside the buffer and numberOfBytesRead parameters, hence the PInvoke declarations needs to be changed to:

 [DllImport("WinInet", SetLastError = true, ExactSpelling = true)]
internal extern static bool InternetReadFile(
    IntPtr hFile, 
    IntPtr buffer, 
    Int32 numberOfBytesToRead, 
    IntPtr numberOfBytesRead
);
 

The change on the parameters type to IntPtr requires an additional overhead of pinning the memory used by this two variables and keeping them until the asynchronous call to the native API completes. To pin the memory, use the methods provided by the Marshal class; it also provides methods for dumping the content of the memory in a byte array, string or instance of a class/structure.

To use the Marshall class with the buffer passed to InternetReadFile, you can use something similar to this:

Remarks: To help illustrate the point, this code omits exception handling

 ...
//At this point a request has been sent to the server 
//and we’re going to read the response’s entity body
Int32 buffsize = 1024;
Int32 bytes2Read = 1024;

//Pointers to use with the WinInet call
IntPtr bytesRead = Marshal.AllocHGlobal(sizeof(Int32));
IntPtr buff = Marshal.AllocHGlobal(buffsize);

//Variables to copy the data returned from WinInet
Int32 totalBytesRead = 0;
Int32 bytesNum = 0;
byte[] tempByteArray = new byte[buffsize];

//We’ll allocate the response in a MemoryStream object
MemoryStream memStream = new MemoryStream();

InternetReadFile(this.Handle, buff, bytes2Read, bytesRead);
//Verify if the call completed asynchronously 
// Marshal.GetLastWin32Error()==ERROR_IO_PENDING (997)
//And wait for the completion of the call
... //Perform some other operations while the call completes

//At the completion of the call 
//Extract the information from the buffer
bytesNum = Marshal.ReadInt32(bytesRead);
totalBytesRead += bytesNum;
Marshal.Copy(buff, tempByteArray, 0, bytesNum);
memStream.Write(tempByteArray, 0, bytesNum);

//Repeat the call to InternetReadFile until bytesNum==0

//At the end of the process, release the memory allocated
Marshal.FreeHGlobal(bytesRead);
Marshal.FreeHGlobal(buff);
 

Using this approach you’ll ensure the appropriate behavior of the code, otherwise you could end with access violations or incomplete data inside the buffer.

For more information about the PInvoke wrappers, look at https://www.PInvoke.net.

  -- Reymarx Gereda