Workers#

nupp.workers runs a named module in a fresh LuaJIT state on a native thread. The states share no Lua heap, globals, loaded modules, closures, userdata, or cdata. They communicate by copying serialized values through bounded queues.

Workers are currently available only in a binary target whose stub is "nupp". The compiler-owned host supplies the pinned LuaJIT, the stamped payload from which worker entries load, and the early machine-code address-space reservation needed by later LuaJIT states. Builds refuse workers in module and bundle targets or with a third-party binary stub.

Start and call a worker#

List every independently loaded worker entry in the target so it is carried in the binary:

return {
   include = { "src" },
   build = {
      default = "app",
      targets = {
         app = {
            kind = "binary",
            stub = "nupp",
            entries = { "main", "jobs.hash" },
         },
      },
   },
}

spawn returns an owned worker. Its drop operation closes the inbox, joins the thread, and releases the queues on every structured exit:

The entry obtains its own endpoints with current and can serve request/reply calls until its inbox closes:

A handler error becomes a failed reply for that call; the serve loop continues. An uncaught entry error is instead recorded by join:

Calling join does not close a running worker. Use stop for explicit cleanup, or let ownership call it at the end of the worker's scope.

Messages#

send and Self:send carry ordinary one-way messages. receive() waits for one, receive(timeoutMs) waits up to a nonnegative number of milliseconds, and tryReceive() only polls. Nil means the channel closed after its queued messages were drained.

Transferable values are booleans, numbers, strings, and tables recursively made from those values with scalar keys. A top-level nil, function, thread, userdata, cdata value, metatable, table deeper than 32 levels, cycle, or repeated table alias is rejected before encoding. The error identifies the first rejected path. Each receiver decodes an independent copy.

Each direction holds at most 1024 messages and 256 MiB. Sending never waits for capacity: it raises if either bound is full or the channel is closed. This keeps producer backpressure from introducing a second, potentially deadlocking wait.

Waiting and stopping#

Ready operations return immediately. Without a suspension handler, an empty receive sleeps on the native channel condition variable and join blocks on the thread. With a handler installed, those same calls register readiness sources and park cooperatively; their ordinary call syntax does not change.

Closing is cooperative. It is nonblocking and wakes an entry waiting in Self:receive or Self:serve, but it cannot interrupt arbitrary worker code. A worker that ignores its closed inbox can therefore keep join, stop, and automatic cleanup waiting forever. Work that must be forcibly terminated or isolated from native crashes belongs in an operating-system process instead.

See Suspension for blocking and handled waits, and Ownership for automatic cleanup.