-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Implement basic event loop for win32 #9957
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,30 @@ | ||
| require "c/iocp" | ||
| require "crystal/system/print_error" | ||
|
|
||
| module Crystal::EventLoop | ||
| @@queue = Deque(Fiber).new | ||
| @@queue = Deque(Crystal::Event).new | ||
|
|
||
| # Runs the event loop. | ||
| def self.run_once : Nil | ||
| next_fiber = @@queue.pop? | ||
| unless @@queue.empty? | ||
| next_event = @@queue.min_by { |e| e.wake_in } | ||
| time_elapsed = (Time.monotonic - next_event.slept_at) | ||
|
|
||
| if next_fiber | ||
| Crystal::Scheduler.enqueue next_fiber | ||
| unless time_elapsed > next_event.wake_in | ||
| sleepy_time = (next_event.wake_in - time_elapsed).total_milliseconds.to_i | ||
| io_entry = Slice.new(1, LibC::OVERLAPPED_ENTRY.new) | ||
|
|
||
| if LibC.GetQueuedCompletionStatusEx(Thread.current.iocp, io_entry, 1, out removed, sleepy_time, false) | ||
| if removed == 1 && io_entry.first.lpOverlapped | ||
| next_event = io_entry.first.lpOverlapped.value.cEvent.unsafe_as(Crystal::Event) | ||
| end | ||
|
Comment on lines
+17
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If removed is 0, this means that the sleepy_time passed without hitting any completed events, right? If so, if I read the code further down the min_value above is enqueued in the scheduler at line 27. But does anything actually cancel the completion of the event or keep track that it has timed out? Will it continue and eventually be completed? Because if it will then you will have a strange situation potentially enqueuing a fiber in unknown status unless I'm totally confused.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not exactly. If we make the GetQueuedCompletionStatusEx call, it times out and returns true, but removed is 0; then that means a sleep event completed. The idea was to treat sleep events the same as real I/O and use GetQueuedCompletionStatusEx's timeout as the mechanism for completing sleep events.
Yes, we then signal completion by removing the event from the event loop queue and telling the scheduler to switch to the event's associated fiber.
As another example, let's say we're in the middle of a 5-second sleep event blocking on the GetQueuedCompletionStatusEx call, and some real I/O (like a socket or file read) comes in. In this case, GetQueuedCompletionStatusEx will return as soon as possible (likely before our 5-second sleep is up) with the information about the real I/O. The removed The sleep event that was put aside will get handled on the next iteration after we calculate
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I see. I guess I got confused by having spent quite a lot of time staring at a different completion mechanism on linux (io_uring) that do support arbitrary wait events directly. Ok, I don't know enough about windows to say if that or using How would event timeouts work? I suppose there is some way of putting that information in the entry on submission, but how would the result be communicated and would there need to be some handling in here for that?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I missed this. After GetQueuedCompletionStatusEx is called and times out, it could be a sleep event completing or an I/O timeout, but I'm not handling that second case. There's an IO::Evented instance passed in to create read/write events with a read/write_timed_out property on it. I will add some logic in the event loop to check if next_event was a read/write event. If so, set that boolean accordingly and thereby communicate the result. |
||
| else | ||
| raise RuntimeError.from_winerror("Error getting i/o completion status") | ||
neatorobito marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @incognitorobito I think you need to check the error code here since a timeout will trow a RuntimeError in this case. See GetQueuedCompletionStatusEx docs on the timeout parameter
We should call |
||
| end | ||
| end | ||
|
|
||
| dequeue next_event | ||
| Crystal::Scheduler.enqueue next_event.fiber | ||
| else | ||
| Crystal::System.print_error "Warning: No runnables in scheduler. Exiting program.\n" | ||
| ::exit | ||
|
|
@@ -19,44 +35,56 @@ module Crystal::EventLoop | |
| def self.after_fork : Nil | ||
| end | ||
|
|
||
| def self.enqueue(fiber : Fiber) | ||
| unless @@queue.includes?(fiber) | ||
| @@queue << fiber | ||
| def self.enqueue(event : Crystal::Event) | ||
| unless @@queue.includes?(event) | ||
| @@queue << event | ||
| end | ||
| end | ||
|
|
||
| def self.dequeue(fiber : Fiber) | ||
| @@queue.delete(fiber) | ||
| def self.dequeue(event : Crystal::Event) | ||
| @@queue.delete(event) | ||
| end | ||
|
|
||
| # Create a new resume event for a fiber. | ||
| def self.create_resume_event(fiber : Fiber) : Crystal::Event | ||
| enqueue(fiber) | ||
|
|
||
| Crystal::Event.new(fiber) | ||
| end | ||
|
|
||
| # Creates a write event for a file descriptor. | ||
| def self.create_fd_write_event(io : IO::Evented, edge_triggered : Bool = false) : Crystal::Event | ||
| # TODO Set event's wake_in to write timeout. | ||
| Crystal::Event.new(Fiber.current) | ||
| end | ||
|
|
||
| # Creates a read event for a file descriptor. | ||
| def self.create_fd_read_event(io : IO::Evented, edge_triggered : Bool = false) : Crystal::Event | ||
| # TODO Set event's wake_in to read timeout. | ||
| Crystal::Event.new(Fiber.current) | ||
| end | ||
| end | ||
|
|
||
| struct Crystal::Event | ||
| property slept_at : Time::Span | ||
| property wake_in : Time::Span | ||
| property fiber : Fiber | ||
|
|
||
| def initialize(@fiber : Fiber) | ||
| @wake_in = Time::Span::ZERO | ||
| @slept_at = Time::Span::ZERO | ||
| end | ||
|
|
||
| # Frees the event | ||
| def free : Nil | ||
| Crystal::EventLoop.dequeue(@fiber) | ||
| Crystal::EventLoop.dequeue(self) | ||
| end | ||
|
|
||
| def add(time_span : Time::Span) : Nil | ||
| @slept_at = Time.monotonic | ||
| @wake_in = time_span | ||
| Crystal::EventLoop.enqueue(self) | ||
| end | ||
|
|
||
| def add(time_span : Time::Span?) : Nil | ||
| Crystal::EventLoop.enqueue(@fiber) | ||
| def to_unsafe | ||
| WSAOVERLAPPED.new(self) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| require "c/winnt" | ||
|
|
||
| # WSAOVERLAPPED is the primary communication structure for async I/O on Windows. | ||
| # See https://docs.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaoverlapped | ||
| @[Extern] | ||
| struct WSAOVERLAPPED | ||
| internal : LibC::ULONG_PTR | ||
| internalHigh : LibC::ULONG_PTR | ||
| offset : LibC::DWORD | ||
| offsetHigh : LibC::DWORD | ||
| hEvent : LibC::HANDLE | ||
| property cEvent : Void* | ||
|
|
||
| def initialize(crystal_event : Crystal::Event) | ||
| @cEvent = crystal_event.unsafe_as(Pointer(Void)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will work only when the size of
next_event = io_entry.first.lpOverlapped.value.cEvent.unsafe_as(Crystal::Event)I suspected that it reverted only the first 8 bytes. So I wrote the following code and ran it. struct DummyEvent
property buf : UInt8[32] = StaticArray(UInt8, 32).new { |i| i.to_u8 }
end
crystal_event = DummyEvent.new
puts(crystal_event)
cEvent = crystal_event.unsafe_as(Pointer(Void))
puts(cEvent.unsafe_as(DummyEvent))This printed: Only the first 8 bytes were reverted. The rest bytes seem uninitialized ones.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's been a long time since I worked with pointers in Crystal, but it seems like this gives the correct result: Store the pointer to def initialize(crystal_event : Crystal::Event)
@cEvent = pointerof(crystal_event)
...Get the the pointed-to value back: next_event = io_entry.first.lpOverlapped.value.cEvent.value
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you do
|
||
| end | ||
| end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this struct should be moved to
I don't think so. The primary communication structure is In addition, require "c/fileapi"
@[Extern]
struct Overlapped
overlapped : LibC::OVERRLAPPED
property cEvent : Void*
def initialize(crystal_event : Crystal::Event)
@cEvent = crystal_event.unsafe_as(Pointer(Void))
end
end |
||
|
|
||
| @[Link("advapi32")] | ||
| lib LibC | ||
neatorobito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| struct OVERLAPPED_ENTRY | ||
| lpCompletionKey : ULONG_PTR | ||
| lpOverlapped : WSAOVERLAPPED* | ||
| internal : ULONG_PTR | ||
| dwNumberOfBytesTransferred : DWORD | ||
| end | ||
|
|
||
| fun WSAGetLastError : Int | ||
|
|
||
| fun CreateIoCompletionPort( | ||
| fileHandle : HANDLE, | ||
| existingCompletionPort : HANDLE, | ||
| completionKey : ULONG_PTR, | ||
| numberOfConcurrentThreads : DWORD | ||
| ) : HANDLE | ||
|
|
||
| fun GetQueuedCompletionStatus( | ||
| completionPort : HANDLE, | ||
| lpNumberOfBytesTransferred : DWORD*, | ||
| lpCompletionKey : ULONG_PTR*, | ||
| lpOverlapped : WSAOVERLAPPED*, | ||
| dwMilliseconds : DWORD | ||
| ) : BOOL | ||
|
|
||
| fun GetQueuedCompletionStatusEx( | ||
| completionPort : HANDLE, | ||
| lpCompletionPortEntries : OVERLAPPED_ENTRY*, | ||
| ulCount : ULong, | ||
| ulNumEntriesRemoved : ULong*, | ||
| dwMilliseconds : DWORD, | ||
| fAlertable : BOOL | ||
| ) : BOOL | ||
| end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the definitions in this I think that it is better to move them as follows: |
||
Uh oh!
There was an error while loading. Please reload this page.