Sorry for such a long question. It's just I've spent several days trying to solve my problem, and I'm exhausted.
I'm trying to use WinINet in an asynchronous mode. And I must say... this is simply insane. I really can't understand this. It does so many things, but unfortunately its asynchronous API is so much poorly designed that it just can't be used in a serious application with high stability demands.
My problem is the following: I need to do a lot of HTTP/HTTPS transactions serially, whereas I also need to be able to abort them immediately at request.
I was going to use WinINet in the followig way:
- Initialize WInINet usage via
InternetOpen
function withINTERNET_FLAG_ASYNC
flag. - Install a global callback function (via
InternetSetStatusCallback
).
Now, in order to perform a transaction that's what I thought to do:
- Allocate a per-transaction structure with various members describing the transaction state.
- Call
InternetOpenUrl
to initiate the transaction. In the asynchronous mode it usually immediately returns with an error, which isERROR_IO_PENDING
. One of its parameters is the 'context', the value which will be passed to the callback function. We set it to the pointer to the per-transaction state structure. - Very shortly after this the global callback function is called (from another thread) with status
INTERNET_STATUS_HANDLE_CREATED
. At this moment we save the WinINet session handle. - Eventually callback function is invoked with
INTERNET_STATUS_REQUEST_COMPLETE
when the transaction is complete. This allows us to use some notification mechanism (such as setting an event) to notify the originating thread that the transaction is complete. - The thread that issued the transaction realizes that it's complete. Then it does the cleanup: closes the WinINet session handle (by
InternetCloseHandle
), and deletes the state structure.
So far there seems to be no problem.
How to abort a transaction which is in the middle of execution? One way is to close the appropriate WinINet handle. And since WinINet doesn't have functions such as InternetAbortXXXX
- closing the handle seems to be the only way to abort.
Indeed this worked. Such a transaction completes immediately with ERROR_INTERNET_OPERATION_CANCELLED
error code.
But here all the problems begin...
The first unpleasant surprise that I've encountered is that WinINet tends to invoke sometimes the callback function for the transaction even after it has already been aborted.
According to the MSDN the INTERNET_STATUS_HANDLE_CLOSING
is the last invocation of the callback function. But it's a lie. What I see is that sometimes there's a consequent INTERNET_STATUS_REQUEST_COMPLETE
notification for the same handle.
I also tried to disable the callback function for the transaction handle right before closing it, but this didn't help. It seems that the callback invocation mechanism of the WinINet is asynchronous. Hence - it may call the callback function even after the transaction handle has been closed.
This imposes a problem: as long as WinINet may call the callback function - obviously I can't free the transaction state structure. But how the hell do I know whether or not WinINet will be so kind to call it? From what I saw - there's no consistency.
Nevertheless I've worked this around. Instead I now keep a global map (protected by the critical section of course) of allocated transaction structures. Then, inside the callback function I ensure that the transaction indeed exists and lock it for the duration of the callback invocation.
But then I've discovered another problem, which I couldn't solve so far. It arises when I abort a transaction very shortly after it's started.
What happens is that I call InternetOpenUrl
, which returns the ERROR_IO_PENDING
error code. Then I just wait (very short usually) until the callback function will be called with the INTERNET_STATUS_HANDLE_CREATED
notification. Then - the transaction handle is saved, so that now we have an opportunity to abort without handle/resource leaks, and we may go ahead.
I tried to do the abort exactly after this moment. That is, close this handle immediately after I receive it. Guess what happens? WinINet crashes, invalid memory access! And this is not related to whatever I do in the callback function. The callback function isn't even called, the crash is somewhere deep inside the WinINet.
On the other hand if I wait for the next notification (such as 'resolving name') - usually it works. But sometimes crashes as well!
The problem seems to disappear if I put some minimal Sleep
between obtaining the handle and closing it. But obviously this can't be accepted as a serious solution.
All this makes me conclude: The WinINet is poorly designed.
- There's no strict definition about the scope of the callback function invocation for the specific session (transaction).
- There's no strict definition about the moment from which I'm allowed to close WinINet handle.
- Who knows what else?
Am I wrong? Is that something that I don't understand? Or WinINet just can't be safely used?
EDIT:
This is the minimal code block that demonstrates the 2nd issue: crash. I've removed all the error handling and etc.
HINTERNET g_hINetGlobal;
struct Context
{
HINTERNET m_hSession;
HANDLE m_hEvent;
};
void CALLBACK INetCallback(HINTERNET hInternet, DWORD_PTR dwCtx, DWORD dwStatus, PVOID pInfo, DWORD dwInfo)
{
if (INTERNET_STATUS_HANDLE_CREATED == dwStatus)
{
Context* pCtx = (Context*) dwCtx;
ASSERT(pCtx && !pCtx->m_hSession);
INTERNET_ASYNC_RESULT* pRes = (INTERNET_ASYNC_RESULT*) pInfo;
ASSERT(pRes);
pCtx->m_hSession = (HINTERNET) pRes->dwResult;
VERIFY(SetEvent(pCtx->m_hEvent));
}
}
void FlirtWInet()
{
g_hINetGlobal = InternetOpen(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC);
ASSERT(g_hINetGlobal);
InternetSetStatusCallback(g_hINetGlobal, INetCallback);
for (int i = 0; i < 100; i++)
{
Context ctx;
ctx.m_hSession = NULL;
VERIFY(ctx.m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL));
HINTERNET hSession = InternetOpenUrl(
g_hINetGlobal,
_T("http://ww.google.com"),
NULL, 0,
INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD,
DWORD_PTR(&ctx));
if (hSession)
ctx.m_hSession = hSession;
else
{
ASSERT(ERROR_IO_PENDING == GetLastError());
WaitForSingleObject(ctx.m_hEvent, INFINITE);
ASSERT(ctx.m_hSession);
}
VERIFY(InternetCloseHandle(ctx.m_hSession));
VERIFY(CloseHandle(ctx.m_hEvent));
}
VERIFY(InternetCloseHandle(g_hINetGlobal));
}
Usually on first/second iteration the application crashes. One of the thread created by the WinINet generates an access violation:
Access violation reading location 0xfeeefeee.
Worth to note that the above address has special meaning to the code written in C++ (at least MSVC).
AFAIK when you delete an object that has a vtable
(i.e. - has virtual functions) - it's set to the above address.
So that it's an attempt to call a virtual function of an already-deleted object.