Skip to content
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

Winio #559

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Winio #559

wants to merge 4 commits into from

Conversation

kazu-yamamoto
Copy link
Collaborator

I would like to merge your WIP branch.
This PR is a rebased version.
However, even with unsafe call, recv cannot be interrupted.
I tested with my http2 and dnsext libraries.
What is the expected behavior?

@Mistuke
Copy link
Collaborator

Mistuke commented May 31, 2023

So this version doesn't do much yet. It was just me marking what needs updating. I have a bigger branch locally that I was working on where I started functional change or separating out the Fd typ which is the hard part. But didn't get around to finishing it.

I'll push it here tomorrow when I'm home for you to see.

@kazu-yamamoto
Copy link
Collaborator Author

OK. If I understand correctly, an async mechanism should be introduced to make use of WINIO.
I will try your new branch!

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 2, 2023

OK. If I understand correctly, an async mechanism should be introduced to make use of WINIO. I will try your new branch!

Short answer no, long answer:

While for efficiency network should eventually have an async interface, it's not required for the use of WinIO.

WINIO is async only, but provides mechanisms for you to implement synchronous APIs on top. primarely using IOPort https://hackage.haskell.org/package/base-4.15.0.0/docs/src/GHC-IOPort.html

IOPort will allow async I/O even on the non-threaded RTS. that is to say, the scheduler knows not to consider a program waiting on I/O to be deadlocked, and unlike, say an MVar won't try to break the lock.

That's how e.g. readFile works, we create a port, issue an async read, and block on the port. The I/O manager fills in the port once data has been filled into the buffer.

For this to work, the FFI call should never block itself. How does that work? I/O calls are wrapped into a helper function that will provide a LPWSAOVERLAPPED value for use in Windows API calls like https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecv.

You also provide a second function to tell it how to interpret results of the API call, e.g, is it done, did it error etc. https://github.com/ghc/ghc/blob/07f858eb1ff419b5190f6999f0d4dd5ba275b40c/libraries/base/GHC/Event/Windows/FFI.hsc#L398

The I/O manager then handles the rest. The issue is that some of the API calls in Network are in the C file. and so we make an FFI call to this C file. We don't want that and instead the call should be made from Haskell land. This removes one FFI call indirection, but also allows the scheduler to see what's running.

I think @coot said he will have time to help here.

@coot
Copy link
Contributor

coot commented Jun 2, 2023

@Mistuke we're discussing this internally in IOG, we might indeed find time to work on this.

@kazu-yamamoto
Copy link
Collaborator Author

If you guys kindly list up things to do, I could help.
I have Arm Windows 11 Pro in Parallels desktop on m1 mac.

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 6, 2023

@kazu-yamamoto will do, I'll write up a todo this weekend. Need to go over things again.

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 11, 2023

Ok, so the first thing to do is

After that we can hook things up to WinIO.

So here's a quick overview of how WinIO works (simplified to what is important for this work):

The easiest way to think about WinIO is that it's a message queue based system. Instead of you making an I/O call directly, you instead ask the I/O manager to make it for you.

As an example I'll use how readFile is implemented in base.

So typically, a blocking call to readFile is made as such:

ret <- c_ReadFile handle (castPtr outBuffer) (fromIntegral numBytes) nullPtr nullPtr

Where the program will block until the I/O finishes. Typically a block on an I/O function is handled specially by the RTS and there's a bit of book keeping that says that an I/O action is being performed. With regards to network base actually has a flag saying that the operation is a socket operation such that the RTS doesn't try to close it eagerly.

Now if we look at the documentation for ReadFile https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile the last parameter is lpOverlapped, every Windows API that supports asynchronous query supports this. This is a pointer to a structure that allows us to track an in flight request.

Luckily network already makes the right API calls and passes NULL to these fields to get synchronous responses, e.g. the message receive code does

WSARecvMsg (SOCKET s, LPWSAMSG lpMsg, LPDWORD lpdwNumberOfBytesRecvd,
and
c_recvmsg fd msgHdrPtr len_ptr nullPtr nullPtr

However for other functions, like recv, recvfrom etc are using the POSIX APIs, even on Windows. https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv

This is an issue as this API can't be made asynchronous. So for WinIO we need to switch to using the native APIs on Windows: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecv

This mapping should be reasonably easy to do, the POSIX functions are just wrappers around the native ones.

Now if you've looked at the docs for these functions, the overlapped arguments all have:

A pointer to a WSAOVERLAPPED structure (ignored for nonoverlapped sockets).

So how do we get an overlapped socket? (Haddock doesn't render Windows specific docs so I'll point to src instead). Well to get one after we create a socket, we need to set an overlapped flag, so

needs to switch from the POSIX socket https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket to the native WSASocket https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw where (only when WinIO is being used) do we need to pass WSA_FLAG_OVERLAPPED as a flag. This creates the socket in overlapped mode, but it needs to be registered with WinIO.

This can be done through the helper functions associateHandle' and associateHandle https://github.com/ghc/ghc/blob/182df90e4d1f652c3d078294921805b9b982671b/libraries/base/GHC/Event/Windows.hsc#LL477C1-L477C16

After this the I/O manager can track requests on this Handle.

So how do you get an lpOverlapped to pass to the other APIs? well again using a helper function from WinIO withOverlappedEx and withOverlapped https://github.com/ghc/ghc/blob/182df90e4d1f652c3d078294921805b9b982671b/libraries/base/GHC/Event/Windows.hsc#L527

The difference between withOverlappedEx and withOverlapped is that withOverlapped uses the default manager. (WinIO supports multiple parallel I/O managers).

The helper function looks like this

withOverlappedEx :: forall a.
                    Manager -- ^ The scheduling manager to use.
                 -> String -- ^ Handle name
                 -> HANDLE -- ^ Windows handle associated with the operation.
                 -> Bool   -- ^ whether we should force the application to be synchronous
                 -> Word64 -- ^ Value to use for the @OVERLAPPED@
                           --   structure's Offset/OffsetHigh members, determines the offset of which to start reading from
                 -> StartIOCallback Int -- ^ wrapper around the native I/O function you wish to invoke, this will queue a message.
                 -> CompletionCallback (IOResult a) -- ^ A utility to tell the I/O manager how to interpret the result of the I/O call.
                 -> IO (IOResult a)

Putting it all together, readFile for instance looks like:

hwndRead :: Io NativeHandle -> Ptr Word8 -> Word64 -> Int -> IO Int
hwndRead hwnd ptr offset bytes = do
  mngr <- Mgr.getSystemManager
  fmap fromIntegral $ Mgr.withException "hwndRead" $
     withOverlappedEx mngr "hwndRead" (toHANDLE hwnd) (isAsynchronous hwnd)
                      offset (startCB ptr) completionCB
  where
    startCB outBuf lpOverlapped = do
      debugIO ":: hwndRead"
      -- See Note [ReadFile/WriteFile].
      ret <- c_ReadFile (toHANDLE hwnd) (castPtr outBuf)
                        (fromIntegral bytes) nullPtr lpOverlapped
      return $ Mgr.CbNone ret

    completionCB err dwBytes
      | err == #{const ERROR_SUCCESS}       = Mgr.ioSuccess $ fromIntegral dwBytes
      | err == #{const ERROR_HANDLE_EOF}    = Mgr.ioSuccess 0
      | err == #{const STATUS_END_OF_FILE}  = Mgr.ioSuccess 0
      | err == #{const ERROR_BROKEN_PIPE}   = Mgr.ioSuccess 0
      | err == #{const STATUS_PIPE_BROKEN}  = Mgr.ioSuccess 0
      | err == #{const ERROR_NO_MORE_ITEMS} = Mgr.ioSuccess $ fromIntegral dwBytes
      | err == #{const ERROR_MORE_DATA}     = Mgr.ioSuccess $ fromIntegral dwBytes
      | otherwise                           = Mgr.ioFailed err

this shows you how everything is tied together, to make an I/O call you call withOverlappedEx passing it the needed arguments. At some point the I/O manager will make a call to your start callback, giving you an initialized value for lpOverlapped which is simply passed on to the native API. Now since all native Windows APIs have the same error codes to start an Async request, you can leave it up to the I/O manager to see what happened to the request by returning Mgr.CbNone ret, which means I did not handle the function result and the status of the call was this.

For a list of options see https://github.com/ghc/ghc/blob/182df90e4d1f652c3d078294921805b9b982671b/libraries/base/GHC/Event/Windows.hsc#L465

Once a result is ready or error has occurred your completionCB is asking you how to interpet the results. This wants either a success or failure. If success the number of bytes read (This is automatically read from the overlapped` structure for you, but we ask you to determine if the number is useful.) and if error we ask for the error code and we'll print a friendly message.

Note that the behaviour of withOverlapped(Ex) depend on the function being called. If you call a blocking API it will block, if not it won't.

Lastly we also need to support bot MIO and WinIO. to make this easier we expose helper functions https://github.com/ghc/ghc/blob/182df90e4d1f652c3d078294921805b9b982671b/libraries/base/GHC/IO/SubSystem.hs#L44 but of course these are only available in GHCs which support WinIO. To make it work with older GHCs we export a CPP define __IO_MANAGER_WINIO__ this is defined if the GHC has support for WinIO.

Does this all make sense to both of you @kazu-yamamoto @coot ? I'm happy to explain over a call if easier as well.

@kazu-yamamoto
Copy link
Collaborator Author

ebd1cc6 should fix #426 but SOCKET cannot be found on Windows at this moment.

@kazu-yamamoto
Copy link
Collaborator Author

@Mistuke We don't have to support old GHCs in v3.2.
Old GHCs users should use v3.1.

@kazu-yamamoto
Copy link
Collaborator Author

@Mistuke One confirmation: we keep Mio-based code as is and create new code with withOverlappedEx for WinIO, then chose proper one for the OS with conditional. Is this understanding correct?

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 12, 2023 via email

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 12, 2023

We can use conditional or the infix operator <|>

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 12, 2023

ebd1cc6 should fix #426 but SOCKET cannot be found on Windows at this moment.

The file is missing an #include <winsock2.h> although SOCKET and HANDLE are the same types, so we can just use a DWORD (unsigned 64-bit value). It was only different on 16-bit Windows which is long dead.

@kazu-yamamoto
Copy link
Collaborator Author

ebd1cc6 should fix #426 but SOCKET cannot be found on Windows at this moment.

The file is missing an #include <winsock2.h> although SOCKET and HANDLE are the same types, so we can just use a DWORD (unsigned 64-bit value). It was only different on 16-bit Windows which is long dead.

Due to complicated mechanism of hsc2hs and CPP, I can only use CULong here.
With 56bbb6d, this branch can be compiled on Windows (but tests are stuck).

@kazu-yamamoto
Copy link
Collaborator Author

@Mistuke Could you provide WinIO-version of recvMsg first?

@Mistuke
Copy link
Collaborator

Mistuke commented Jun 15, 2023 via email

@kazu-yamamoto kazu-yamamoto mentioned this pull request Jul 4, 2023
5 tasks
@kazu-yamamoto
Copy link
Collaborator Author

Gentle ping.
Any progression?

@Mistuke
Copy link
Collaborator

Mistuke commented Mar 7, 2024

Gentle ping. Any progression?

Sorry had been too busy. I have pushed a branch win-io to the network repo which contains what i started on. That shows the general idea implementing one function. (not sure I fixed all the type errors before but they should be trivial to fix).

Not that for it to work, the thing that creates the sockets needs to register the socket. I think the easiest way to support this is what wrapper helper functions like withAsyncSupport such that the user can still pick which one to use until the old one is removed.

I'm happy to answer any questions any time but don't know when I'll have time to code again on it. Is this enough to give you an idea of what's needed?

@kazu-yamamoto kazu-yamamoto deleted the win-io branch March 25, 2024 08:19
@Mistuke
Copy link
Collaborator

Mistuke commented Mar 25, 2024

@kazu-yamamoto hmm what happened?

@kazu-yamamoto kazu-yamamoto restored the win-io branch March 25, 2024 10:02
@kazu-yamamoto
Copy link
Collaborator Author

Sorry. My bad. I restored the branch.

@Mistuke
Copy link
Collaborator

Mistuke commented Mar 25, 2024

@kazu-yamamoto that branch is in your fork. The one I updated is https://github.com/haskell/network/tree/win-io

I thought you just wanted me to convert one API call so you'd know what to do. That one has a framework started to convert the calls.

@kazu-yamamoto
Copy link
Collaborator Author

I'm confused because network/win-io exists from my point of view.
Would you rebase win-io onto master and do push -f?
I will take care to not delete that branch.

@kazu-yamamoto kazu-yamamoto reopened this Mar 26, 2024
@kazu-yamamoto
Copy link
Collaborator Author

Never mind. I was really confused last night.
I just re-opened this PR.
@Mistuke If you like, please rebase this PR onto master and push -f. (This is not required)

@Mistuke
Copy link
Collaborator

Mistuke commented Mar 30, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants