-
Notifications
You must be signed in to change notification settings - Fork 0
Use two loops: GUI event loop and IO event loop.
GUI event loop is kind of special, e.g. has to be on the main thread. Which makes it hard to integrate with the IO event loop.
Also not like the IO event loop, it does not necessary to run at maximum speed. e.g. If your monitor's refresh rate is 30 FPS, the GUI event loop runs at the same rate is enough. Use higher rate just wastes CPU resource.
The IO event loop can be easily plugged into the GUI event loop. We need to modify a bit of the code in view.
Here is the prototype of the IO event loop:
IO: context [
do-events: func [
time [integer!] ;-- time in ms, -1: wait until receive a event
][
forever [
iocp/wait iocp-port time
if time > -1 [exit]
]
]
]
Plug the IO event loop into the GUI loop.
;; pseudo-code
;; in view/backend/windows/events.reds
do-events: func [
no-wait? [logic!]
return: [logic!]
/local
msg [tagMSG value]
state [integer!]
msg? [logic!]
][
msg?: no
while [true][
if zero? PeekMessage msg null 0 0 1 [
break ;-- window is closed, exit the loop
]
unless msg? [msg?: yes]
state: process msg
if state >= EVT_DISPATCH [
current-msg: msg
TranslateMessage msg
DispatchMessage msg
]
if no-wait? [return msg?]
;-- check if there is any IO event, if it is, do some IO work
;-- otherwise wait some time
IO/do-events screen-refresh-rate
]
msg?
]
The core part of the IO event loop is the IOCP.
;; iocp.reds
iocp-event-handler!: alias function! [
data [int-ptr!]
]
iocp!: alias struct! [
port [int-ptr!] ;-- OS IOCP handle
events [int-ptr!]
evt-cnt [integer!]
]
iocp-data!: alias struct! [
device [handle!] ;-- device handle
event-handler [iocp-event-handler!]
event [integer!]
transferred [integer!] ;-- number of bytes transferred
]
iocp: context [
create: func [
return: [iocp!]
][]
close: func [
p [iocp!]
][]
bind: func [
"bind a device handle (TCP, thread, pipe, etc) to the I/O completion port"
p [iocp!]
handle [int-ptr!]
/local
port [int-ptr!]
][]
post: func [
"post an event into the IOCP"
p [iocp!]
user-data [int-ptr!]
][]
wait: func [
"wait I/O completion events and dispatch them"
p [iocp!]
timeout [integer!] ;-- time in ms, -1: infinite
/local
res [integer!]
cnt [integer!]
err [integer!]
i [integer!]
e [OVERLAPPED_ENTRY!]
data [iocp-data!]
][
cnt: 0
res: GetQueuedCompletionStatusEx p/port p/events p/evt-cnt :cnt timeout no
i: 0
while [i < cnt][
e: p/events + i
data: as iocp-data! e/lpOverlapped
data/event-handler as int-ptr! data ;-- calling the device event handler
i: i + 1
]
]
]
We implement this IOCP in each platforms by using the OS APIs (Windows (IOCP), macOS (Kqueue), Linux (epoll), FreeBSD (Kqueue)), for devices (TCP, pipe, thread, etc) which can be used directly with the IOCP, they will just work fine.
For devices which no direct support (GPIO, file, etc), we need to make them asynchronous. each such devices may use different ways base on their need. For example, to make file IO asynchronous, we need to create a thread to do the file operations. Once the operation has been finished, we post a message to the IOCP.
;; file.reds
;; pseudo-code
file-scheme: context [
file-data!: alias struct! [
iocp [iocp-data! value] ;-- should be a iocp-data struct
;-- below is scheme specific data
port [red-object! value] ;-- red port! cell
buffer [byte-ptr!]
]
event-handler: func [ ;-- will be called in iocp/wait
data [iocp-data!]
/local
p [red-object!]
msg [red-object!]
fdata [file-data!]
type [integer!]
bin [red-binary!]
][
fdata: as file-data! data
p: as red-object! :fdata/port
msg: p
type: data/event
switch type [
IO_EVT_READ [
bin: binary/load fdata/buffer data/transferred
copy-cell as cell! bin (object/get-values p) + port/field-data
]
IO_EVT_WROTE [0]
default [probe ["wrong file event: " type]]
]
io/call-awake p msg type ;-- call the awake function in the red port
]
create-file-data: func [
port [red-object!]
return: [iocp-data!]
/local
data [file-data!]
][
data: as file-data! allocate size? file-data!
data/iocp/event-handler: as iocp-event-handler! :event-handler
copy-cell as cell! port as cell! :data/port
as iocp-data! data
]
asyc-read: func [
port [red-object!]
/local
data [file-data!]
][
;-- this function runs in another thread
data: create-file-data port
;-- get file handle from the red port
handle: xxx
;-- read data
simple-io/read-data handle data/buffer size
;-- post a completion event
data/iocp/event: IO_EVT_READ
iocp/post g-iocp as int-ptr! data
;-- The g-iocp above is a global IOCP handle created by using iocp/create.
;-- Each Red instance only has one IOCP handle.
]
copy: func [ ;-- copy action
port [red-object!]
new [red-value!]
part [red-value!]
deep? [logic!]
types [red-value!]
return: [red-value!]
][
thread/create :asyc-read as int-ptr! port
]
]