Skip to content

Conversation

@Nicell
Copy link
Collaborator

@Nicell Nicell commented Dec 14, 2025

Summary

This PR fixes a regression introduced in 0.1.0-nightly.20251020 where vm.create() child VMs did not have native @lute/* modules registered (e.g. @lute/net). As a result, child VMs would fall back to definitions/*.luau and error with "not implemented" (repro: examples/parallel_serve.luau). Closes #691.

Additionally, once child VMs can load native libraries again, multiple runtimes running on different OS threads must not share and concurrently drive uv_default_loop(). This PR gives each spawned runtime its own libuv loop and wires the native libraries to use the runtime’s loop.

User-visible fixes

  • examples/parallel_serve.luau works again (no longer hits definitions/net.luau: not implemented when running @lute/net inside a vm.create() VM).
  • Spawned VMs that use libuv-backed native libraries (net/task/fs/process/io) no longer rely on the process-global default loop across multiple threads.

Key changes

  • lute/vm/src/spawn.cpp

    • Register native @lute/* modules in child VMs (matching the main CLI runtime behavior).
    • Spawned runtimes allocate a dedicated libuv loop and are registered for process-lifetime tracking.
  • lute/runtime/include/lute/runtime.h, lute/runtime/src/runtime.cpp

    • Runtime now owns an optional dedicated uv_loop_t and exposes getUvLoop().
    • Added a small spawned-runtime registry (registerSpawnedRuntime, waitForSpawnedRuntimes) so the CLI can stay alive while spawned runtimes still have work.
  • lute/cli/src/climain.cpp

    • After the main script finishes, wait for spawned runtimes to stop having work (since child VMs no longer contribute to uv_default_loop() activity).
  • libuv-backed libraries now use the calling runtime’s loop instead of uv_default_loop():

    • lute/net/src/net.cpp (also minimal locking around global server maps for multi-VM access)
    • lute/task/src/task.cpp
    • lute/fs/src/fs.cpp, lute/fs/src/fs_impl.cpp
    • lute/process/src/process.cpp
    • lute/io/src/io.cpp

Tests

  • Added regression coverage for vm.create() module loading:
    • tests/src/vmcreate.test.cpp
    • tests/src/vm/vm_requirer.luau
    • tests/src/vm/vm_helper.luau

Repro / Verification

  • Repro (previously failed on 20251020+):
    lute tools/luthier.luau run lute -- ./examples/parallel_serve.luau

  • Run tests:
    lute tools/luthier.luau run Lute.Test

Notes

This avoids undefined behavior from multiple runtime threads concurrently creating handles on and calling uv_run() on the single process-global uv_default_loop() (and the associated uWebSockets loop).

This was worked on by Codex for an hour, neat. I need to spend some time reviewing everything before opening this from draft

vrn-sn added a commit that referenced this pull request Dec 16, 2025
…uteVfs` (#696)

Resolves #691. As @Nicell points out in #692, we have some libuv memory
safety issues that have been revealed with this change, so they'll need
to be fixed outside of this PR.
@aatxe
Copy link
Member

aatxe commented Jan 6, 2026

cc @vrn-sn this isn't obsoleted by the changes you made, right?

@aatxe aatxe requested a review from vrn-sn January 6, 2026 21:14
@vrn-sn
Copy link
Member

vrn-sn commented Jan 6, 2026

cc @vrn-sn this isn't obsoleted by the changes you made, right?

Partially, this part is solved by my previous PR:

  • Register native @lute/* modules in child VMs (matching the main CLI runtime behavior).

The rest of the logic in this PR that relates to libuv is unrelated to what I merged in.

@Vighnesh-V
Copy link
Collaborator

Vighnesh-V commented Jan 7, 2026

Left some preliminary feedback - I think with Varuns PR we can make this one considerably smaller.

This avoids undefined behavior from multiple runtime threads concurrently creating handles on and calling uv_run() on the single process-global uv_default_loop() (and the associated uWebSockets loop).

I'm not sure there is undefined behaviour - the default loop is single threaded right? So each coroutine only runs on the main thread, and continuations are only processed in the main thread when uv_run is called.

#include <vector>

struct lua_State;
struct uv_loop_s;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an internal libuv type - you'll want to store uv_loop_t

~Runtime();

bool useDedicatedUvLoop();
uv_loop_s* getUvLoop() const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uv_loop_s* getUvLoop() const;
uv_loop_t* getUVLoop() const;


std::atomic<int> activeTokens;

bool ownsUvLoop = false;
Copy link
Collaborator

@Vighnesh-V Vighnesh-V Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do one runtime, one loop - see my comment below, since we might not actually need this?

Comment on lines +84 to +85
// Event loop for this runtime; defaults to `uv_default_loop()`, but can be dedicated via `useDedicatedUvLoop`.
uv_loop_s* uvLoop = nullptr;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a lot of complexity. Instead of defaulting, just initialize a uv_loop manually for this runtime.

if (ownsUvLoop)
return true;

uv_loop_t* loop = new uv_loop_t();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can move into the RuntimeConstructor, along with the uv_loop_init call.

Comment on lines +17 to +18
static std::mutex spawnedRuntimeMutex;
static std::vector<std::weak_ptr<Runtime>> spawnedRuntimes;
Copy link
Collaborator

@Vighnesh-V Vighnesh-V Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These shouldn't be static data( I don't think we need a mutex here either?) Each runtime is going to be on it's own event loop.

{
uv_fs_t unlink_req;
int err = uv_fs_unlink(uv_default_loop(), &unlink_req, luaL_checkstring(L, 1), nullptr);
uv_loop_t* loop = reinterpret_cast<uv_loop_t*>(getRuntime(L)->getUvLoop());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't want to re-interpret cast here - this should just be a uv_loop_t.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ditto for all the places below.

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.

Lute libraries are not available from loaded vms

4 participants