Skip to content
Qingtian edited this page Jul 16, 2019 · 6 revisions

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
    ]
]
Clone this wiki locally