Skip to content

EventLoop: formalize driver contract and add public swapDriver API#12998

Open
kevinresol wants to merge 1 commit into
HaxeFoundation:developmentfrom
kevinresol:eventloop-driver
Open

EventLoop: formalize driver contract and add public swapDriver API#12998
kevinresol wants to merge 1 commit into
HaxeFoundation:developmentfrom
kevinresol:eventloop-driver

Conversation

@kevinresol

Copy link
Copy Markdown
Contributor

Goal

#12492 deliberately kept native loop integration private: EventLoop gained a structural hook (nativeLoop) but no supported way to install or swap a waiter from user or library code. That was the right tradeoff for inverting the HL dependency at the time.

More targets now ship or plan libuv bindings (Lua, eval stdlib, C++ via linc_uv and similar). This PR closes that gap by formalizing the driver contract and exposing a safe public API (EventLoopDriver, swapDriver, factory/ctor hooks) to plug in a loop driver without @:privateAccess or target-specific internals.

Summary

  • Replace the private nativeLoop sidecar with a public, mandatory EventLoopDriver that owns waiting (wait (previously run) / wake / close / hasExternalWork (previously isAlive())).
  • Give EventLoop a public swapDriver API (plus DEFAULT_DRIVER_FACTORY / ctor injection) so native waiters such as libuv can be installed explicitly and safely.
  • Move HL libuv integration onto that contract via UvEventLoopDriver and Loop.getFromEventLoopswapDriver, removing the nativeLoop private access.

Blocking/maxBlock/wake poller details for native loops are covered in #12993; this PR is about the abstraction and public swap surface.

Rationale

After #12492 inverted the dependency so EventLoop no longer imports hl.uv, native integration still looked like an internal bolt-on:

  • A private structural NativeEventLoop was assigned with @:privateAccess loop.nativeLoop = ….
  • Waiting stayed split: Haxe used Lock / Sys.sleep on the loop; the native side was optionally polled from loopOnce.
  • There was no supported way for user or library code to replace the waiter, observe what was installed, or swap once the loop was running.

That made UV (and any future native loop) a special case rather than a first-class waiting strategy. Concretely, this PR:

  1. One place owns waiting — the driver — while EventLoop keeps bookkeeping (queues, timers, promises, exit policy, Haxe callbacks).
  2. A stable public contract other targets/libraries can implement without reaching into private fields.
  3. Explicit, thread-safe swapping so main can start with the default Haxe driver (required by early __init__ / main construction) and later install UV when getFromEventLoop (or user code) asks for it.

EntryPoint remains the platform pump (who calls loop() / loopOnce()). Thread create APIs are unchanged; custom drivers are installed with swapDriver on that thread’s loop.

Design

EntryPoint (pump)
  -> EventLoop.loop() / loopOnce()
       -> EventLoop: events, timers, promises, exit policy, Haxe callbacks
       -> EventLoopDriver (always present): wait / wake / close / hasExternalWork
            - HaxeEventLoopDriver (default)
            - UvEventLoopDriver (HL, via getFromEventLoop + swapDriver)

Public API

API Role
haxe.EventLoopDriver Contract for a waiter
HaxeEventLoopDriver Default Lock / sleep / no-op implementation
EventLoop.DEFAULT_DRIVER_FACTORY Used when new EventLoop(?driver) omits a driver
new EventLoop(?driver) Optional construct-time driver
swapDriver(driver, ?onSwapped) Replace waiter; sync or deferred
getDriver() / getPendingDriver() Observe current / pending install
hl.uv.UvEventLoopDriver Libuv implementation
hl.uv.Loop.getFromEventLoop Sync return of Loop + swapDriver install

swapDriver rules

  • Sync apply when on the loop thread (or thread unset) and not inside wait / loopOnce.
  • Otherwise defer until applyPendingSwap (start of each loop() iteration / end of loopOnce), and wake() the current driver so a blocked wait can progress.
  • Last-wins: a new pending swap immediately close()s any superseded pending driver and drops its callback.
  • Publish order under the mutex: assign new driver, then close() old, so concurrent wakeup never targets a just-closed driver.
  • onSwapped always runs on the loop thread.
  • A pending swap counts as keep-alive work so loop() cannot idle-exit between e.g. socket creation and driver publish (important because getFromEventLoop is often called with inLoop == true, so install is deferred).

UV attach

getFromEventLoop still returns the concrete hl.uv.Loop synchronously (required by AsyncSocket / Tcp / Fs). It creates a UvEventLoopDriver (isDefault for main’s default_loop), calls swapDriver, and reuses an already current or pending UV driver (idempotent). Closing a default-loop driver does not uv_loop_close the process-global loop.

Implementation notes

  • Removed private NativeEventLoop / nativeLoop from EventLoop.
  • Waiting in loop() always goes through driver.wait(...); loopOnce runs Haxe callbacks only (no blocking wait — preserves JS/Flash EntryPoint).
  • dispose() closes pending + current drivers and leaves a closed non-null driver so later cross-thread wakeup cannot NPE.
  • Docs: Timer no longer references removed createWithEventLoop / runWithEventLoop.

For how native wait(maxBlock) / wake interact with Haxe deadlines and cross-thread enqueue, see #12993.

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.

1 participant