diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index b8020fd17..3236c6148 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -163,7 +163,11 @@ bool runBytecode( lua_pop(GL, 1); - return runtime.runToCompletion(); + bool success = runtime.runToCompletion(); + if (success) + waitForSpawnedRuntimes(); + + return success; } static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv, LuteReporter& reporter) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index e3d31b11c..c820734a1 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -233,7 +233,8 @@ int open(lua_State* L) int fs_remove(lua_State* L) { 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(getRuntime(L)->getUvLoop()); + int err = uv_fs_unlink(loop, &unlink_req, luaL_checkstring(L, 1), nullptr); uv_fs_req_cleanup(&unlink_req); if (err) @@ -248,7 +249,8 @@ int fs_mkdir(lua_State* L) int mode = luaL_optinteger(L, 2, 0777); uv_fs_t req; - int err = uv_fs_mkdir(uv_default_loop(), &req, path, mode, nullptr); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_mkdir(loop, &req, path, mode, nullptr); uv_fs_req_cleanup(&req); if (err) @@ -262,7 +264,8 @@ int fs_rmdir(lua_State* L) const char* path = luaL_checkstring(L, 1); uv_fs_t rmdir_req; - int err = uv_fs_rmdir(uv_default_loop(), &rmdir_req, path, nullptr); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_rmdir(loop, &rmdir_req, path, nullptr); uv_fs_req_cleanup(&rmdir_req); if (err) @@ -276,7 +279,8 @@ int fs_stat(lua_State* L) const char* path = luaL_checkstring(L, 1); uv_fs_t stat_req; - int err = uv_fs_stat(uv_default_loop(), &stat_req, path, nullptr); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_stat(loop, &stat_req, path, nullptr); if (err) { @@ -353,7 +357,8 @@ int fs_copy(lua_State* L) auto* req = new uv_fs_t(); req->data = new ResumeToken(getResumeToken(L)); - int err = uv_fs_copyfile(uv_default_loop(), req, path, dest, 0, defaultCallback); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_copyfile(loop, req, path, dest, 0, defaultCallback); if (err) { @@ -373,7 +378,8 @@ int fs_link(lua_State* L) auto* req = new uv_fs_t(); req->data = new ResumeToken(getResumeToken(L)); - int err = uv_fs_link(uv_default_loop(), req, path, dest, defaultCallback); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_link(loop, req, path, dest, defaultCallback); if (err) { @@ -402,7 +408,8 @@ int fs_symlink(lua_State* L) req->flags = 0; } - int err = uv_fs_symlink(uv_default_loop(), req, path, dest, req->flags, defaultCallback); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int err = uv_fs_symlink(loop, req, path, dest, req->flags, defaultCallback); if (err) { @@ -474,7 +481,8 @@ int fs_watch(lua_State* L) event->callbackReference = std::make_shared(L, 2); event->handle.data = event; - int init_err = uv_fs_event_init(uv_default_loop(), &event->handle); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); + int init_err = uv_fs_event_init(loop, &event->handle); if (init_err) { @@ -539,7 +547,7 @@ int fs_exists(lua_State* L) req->data = new ResumeToken(getResumeToken(L)); int err = uv_fs_access( - uv_default_loop(), + reinterpret_cast(getRuntime(L)->getUvLoop()), req, path, F_OK, @@ -578,7 +586,7 @@ int type(lua_State* L) uv_fs_t req; - int err = uv_fs_stat(uv_default_loop(), &req, path, nullptr); + int err = uv_fs_stat(reinterpret_cast(getRuntime(L)->getUvLoop()), &req, path, nullptr); if (err) { @@ -601,7 +609,7 @@ int listdir(lua_State* L) req->data = new ResumeToken(getResumeToken(L)); int err = uv_fs_scandir( - uv_default_loop(), + reinterpret_cast(getRuntime(L)->getUvLoop()), req, path, 0, diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 0d84722bf..e5c5724b1 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -20,6 +20,7 @@ struct FSRead : FSRequest : FSRequest(L) , file(file) { + loop = reinterpret_cast(getRuntime(L)->getUvLoop()); chunk.resize(kChunkIOSize); iov = uv_buf_init(chunk.data(), chunk.size()); buffer.reserve(kChunkIOSize); @@ -28,6 +29,7 @@ struct FSRead : FSRequest static void readCallback(uv_fs_t* req); UVFile* file = nullptr; + uv_loop_t* loop = nullptr; std::vector buffer; std::vector chunk; uv_buf_t iov; @@ -41,12 +43,14 @@ struct FSWrite : FSRequest , toWrite(buf, buf + len) , offset(0) { + loop = reinterpret_cast(getRuntime(L)->getUvLoop()); chunk.resize(kChunkIOSize); } static void writeCallback(uv_fs_t* req); UVFile* file = nullptr; + uv_loop_t* loop = nullptr; std::vector chunk; uv_buf_t iov; std::vector toWrite; @@ -59,6 +63,7 @@ struct FSClose : FSRequest : FSRequest(L) , file(file) { + loop = reinterpret_cast(getRuntime(L)->getUvLoop()); } ~FSClose() @@ -67,13 +72,15 @@ struct FSClose : FSRequest } UVFile* file = nullptr; + uv_loop_t* loop = nullptr; }; int open_impl(lua_State* L, const char* path, int flags, int mode) { uvutils::ScopedUVRequest req(L); + uv_loop_t* loop = reinterpret_cast(getRuntime(L)->getUvLoop()); uv_fs_open( - uv_default_loop(), + loop, &req->req, path, flags, @@ -134,7 +141,7 @@ void FSRead::readCallback(uv_fs_t* req) std::fill(r->chunk.begin(), r->chunk.end(), 0); uvutils::ScopedUVRequest scopedReq{std::move(r)}; - uv_fs_read(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); + uv_fs_read(scopedReq->loop, &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); } void FSWrite::writeCallback(uv_fs_t* req) @@ -167,7 +174,7 @@ void FSWrite::writeCallback(uv_fs_t* req) w->iov = uv_buf_init(w->chunk.data(), chunkSize); uvutils::ScopedUVRequest scopedReq{std::move(w)}; - uv_fs_write(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); + uv_fs_write(scopedReq->loop, &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); } int read_impl(lua_State* L, UVFile* handle) @@ -178,7 +185,7 @@ int read_impl(lua_State* L, UVFile* handle) } uvutils::ScopedUVRequest req{L, handle}; - uv_fs_read(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); + uv_fs_read(req->loop, &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); // Automatically releases when req goes out of scope return lua_yield(L, 0); } @@ -197,7 +204,7 @@ int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numByte std::copy(req->toWrite.begin(), req->toWrite.begin() + chunkSize, req->chunk.begin()); req->iov = uv_buf_init(req->chunk.data(), chunkSize); - uv_fs_write(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); + uv_fs_write(req->loop, &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); return lua_yield(L, 0); } @@ -211,7 +218,7 @@ int close_impl(lua_State* L, UVFile* handle) uvutils::ScopedUVRequest req{L, handle}; uv_fs_close( - uv_default_loop(), + req->loop, &req->req, handle->fd.value(), [](uv_fs_t* req) diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 926302da1..75d1f518f 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -82,7 +82,7 @@ static void onTtyRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) int read(lua_State* L) { auto handle = std::make_shared(); - handle->loop = uv_default_loop(); + handle->loop = reinterpret_cast(getRuntime(L)->getUvLoop()); handle->resumeToken = getResumeToken(L); handle->self = handle; diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 6314674b6..d78cb76db 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -12,6 +12,7 @@ #include "uv.h" #include +#include #include #include #include @@ -227,6 +228,7 @@ static const int kEmptyServerKey = 0; static Luau::DenseHashMap serverInstances(kEmptyServerKey); static Luau::DenseHashMap> serverStates(kEmptyServerKey); static int nextServerId = 1; +static std::mutex serverMutex; struct ServerLoopState { @@ -500,6 +502,8 @@ void setupAppAndListen(auto* app, std::shared_ptr state, bool& bool closeServer(int serverId) { + std::scoped_lock lock(serverMutex); + if (!serverInstances.contains(serverId) || !serverStates.contains(serverId)) { return false; @@ -530,8 +534,6 @@ bool closeServer(int serverId) int lua_serve(lua_State* L) { - uWS::Loop::get(uv_default_loop()); - std::string hostname = "127.0.0.1"; int port = 3000; bool reusePort = false; @@ -618,8 +620,13 @@ int lua_serve(lua_State* L) } Runtime* runtime = getRuntime(L); + uWS::Loop::get(runtime->getUvLoop()); - int serverId = nextServerId++; + int serverId = 0; + { + std::scoped_lock lock(serverMutex); + serverId = nextServerId++; + } auto state = std::make_shared(); state->runtime = runtime; @@ -655,8 +662,11 @@ int lua_serve(lua_State* L) return 0; } - serverInstances[serverId] = std::move(app); - serverStates[serverId] = state; + { + std::scoped_lock lock(serverMutex); + serverInstances[serverId] = std::move(app); + serverStates[serverId] = state; + } lua_createtable(L, 0, 3); diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 701513fca..94fa318a9 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -225,7 +225,7 @@ const std::string kStdioKindNone = "none"; int executionHelper(lua_State* L, std::vector args, ProcessOptions opts) { auto handle = std::make_shared(); - handle->loop = uv_default_loop(); + handle->loop = reinterpret_cast(getRuntime(L)->getUvLoop()); handle->self = handle; uv_process_options_t options = {}; diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 1defc5ec8..e0d4be67c 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -14,6 +14,7 @@ #include struct lua_State; +struct uv_loop_s; struct ThreadToContinue { @@ -44,6 +45,9 @@ struct Runtime Runtime(); ~Runtime(); + bool useDedicatedUvLoop(); + uv_loop_s* getUvLoop() const; + bool runToCompletion(); RuntimeStep runOnce(); @@ -77,6 +81,9 @@ struct Runtime // Shorthand for global state lua_State* GL = nullptr; + // Event loop for this runtime; defaults to `uv_default_loop()`, but can be dedicated via `useDedicatedUvLoop`. + uv_loop_s* uvLoop = nullptr; + std::mutex dataCopyMutex; std::unique_ptr dataCopy; @@ -92,6 +99,8 @@ struct Runtime std::thread runLoopThread; std::atomic activeTokens; + + bool ownsUvLoop = false; }; Runtime* getRuntime(lua_State* L); @@ -114,3 +123,7 @@ struct ResumeTokenData ResumeToken getResumeToken(lua_State* L); lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox); + +// Track child runtimes created via `@lute/vm` so the CLI can stay alive when they have work (e.g. servers). +void registerSpawnedRuntime(const std::shared_ptr& runtime); +void waitForSpawnedRuntimes(); diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index d36a7606c..4f6f3257d 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -7,8 +7,55 @@ #include "uv.h" +#include #include +#include +#include #include +#include + +static std::mutex spawnedRuntimeMutex; +static std::vector> spawnedRuntimes; + +void registerSpawnedRuntime(const std::shared_ptr& runtime) +{ + std::scoped_lock lock(spawnedRuntimeMutex); + spawnedRuntimes.push_back(runtime); +} + +static bool spawnedHasWork() +{ + std::scoped_lock lock(spawnedRuntimeMutex); + + bool hasWork = false; + + spawnedRuntimes.erase( + std::remove_if( + spawnedRuntimes.begin(), + spawnedRuntimes.end(), + [&hasWork](const std::weak_ptr& weak) + { + std::shared_ptr rt = weak.lock(); + if (!rt) + return true; + + if (rt->hasWork()) + hasWork = true; + + return false; + } + ), + spawnedRuntimes.end() + ); + + return hasWork; +} + +void waitForSpawnedRuntimes() +{ + while (spawnedHasWork()) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} static void lua_close_checked(lua_State* L) { @@ -22,6 +69,7 @@ Runtime::Runtime() { stop.store(false); activeTokens.store(0); + uvLoop = uv_default_loop(); } Runtime::~Runtime() @@ -36,6 +84,54 @@ Runtime::~Runtime() if (runLoopThread.joinable()) runLoopThread.join(); + + if (ownsUvLoop && uvLoop) + { + // Best-effort cleanup of any remaining handles so we can close the loop. + uv_stop(reinterpret_cast(uvLoop)); + + uv_walk( + reinterpret_cast(uvLoop), + [](uv_handle_t* handle, void* /*arg*/) + { + if (!uv_is_closing(handle)) + uv_close(handle, nullptr); + }, + nullptr + ); + + // Run the loop to execute close callbacks. + while (uv_run(reinterpret_cast(uvLoop), UV_RUN_NOWAIT)) + { + } + + uv_loop_close(reinterpret_cast(uvLoop)); + delete reinterpret_cast(uvLoop); + uvLoop = nullptr; + } +} + +bool Runtime::useDedicatedUvLoop() +{ + if (ownsUvLoop) + return true; + + uv_loop_t* loop = new uv_loop_t(); + int rc = uv_loop_init(loop); + if (rc != 0) + { + delete loop; + return false; + } + + uvLoop = loop; + ownsUvLoop = true; + return true; +} + +uv_loop_s* Runtime::getUvLoop() const +{ + return uvLoop; } bool Runtime::hasWork() @@ -44,12 +140,12 @@ bool Runtime::hasWork() // Unfortunately, we do currently have some places where we add/release // tokens that don't correspond to libuv activity, so for now we keep both. // uv_ref/unref could be used to patch tokens into the libuv loop itself. - return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(uv_default_loop()); + return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(reinterpret_cast(uvLoop)); } RuntimeStep Runtime::runOnce() { - uv_run(uv_default_loop(), UV_RUN_NOWAIT); + uv_run(reinterpret_cast(uvLoop), UV_RUN_NOWAIT); // Complete all C++ continuations std::vector> copy; @@ -233,7 +329,7 @@ void Runtime::scheduleLuauResume(std::shared_ptr ref, std::function f) { - auto loop = uv_default_loop(); + auto loop = reinterpret_cast(uvLoop); uv_work_t* work = new uv_work_t(); work->data = new decltype(f)(std::move(f)); diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index 23e1c8658..4c73e69aa 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -23,6 +23,7 @@ struct WaitData ResumeToken resumptionToken; + uv_loop_t* loop = nullptr; uint64_t startedAtMs; bool closed = false; @@ -46,11 +47,15 @@ struct WaitData static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack, int nargs) { + Runtime* runtime = getRuntime(L); + uv_loop_t* loop = reinterpret_cast(runtime->getUvLoop()); + WaitData* yield = new WaitData(); - uv_timer_init(uv_default_loop(), &yield->uvTimer); + uv_timer_init(loop, &yield->uvTimer); yield->resumptionToken = getResumeToken(L); - yield->startedAtMs = uv_now(uv_default_loop()); + yield->loop = loop; + yield->startedAtMs = uv_now(loop); yield->uvTimer.data = yield; yield->putDeltaTimeOnStack = putDeltaTimeOnStack; yield->nargs = nargs; @@ -67,7 +72,7 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT int stackReturnAmount = yield->putDeltaTimeOnStack ? yield->nargs + 1 : yield->nargs; if (yield->putDeltaTimeOnStack) - lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); + lua_pushnumber(L, static_cast(uv_now(yield->loop) - yield->startedAtMs) / 1000.0); uv_close(reinterpret_cast(&yield->uvTimer), [](uv_handle_t* handle) { WaitData* yield = static_cast(handle->data); diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 29bf4f659..314f3cb1b 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -4,6 +4,7 @@ #include "lute/require.h" #include "lute/requirevfs.h" #include "lute/runtime.h" +#include "lute/vm.h" #include "Luau/Require.h" @@ -11,6 +12,8 @@ #include "lualib.h" #include +#include +#include struct TargetFunction { @@ -20,6 +23,40 @@ struct TargetFunction constexpr int kTargetFunctionTag = 1; +int luteopen_crypto(lua_State* L); +int luteopen_fs(lua_State* L); +int luteopen_io(lua_State* L); +int luteopen_luau(lua_State* L); +int luteopen_net(lua_State* L); +int luteopen_process(lua_State* L); +int luteopen_system(lua_State* L); +int luteopen_task(lua_State* L); +int luteopen_time(lua_State* L); + +static void luteopen_libs(lua_State* L) +{ + std::vector> libs = {{ + {"@lute/crypto", luteopen_crypto}, + {"@lute/fs", luteopen_fs}, + {"@lute/luau", luteopen_luau}, + {"@lute/net", luteopen_net}, + {"@lute/process", luteopen_process}, + {"@lute/task", luteopen_task}, + {"@lute/vm", luteopen_vm}, + {"@lute/system", luteopen_system}, + {"@lute/time", luteopen_time}, + {"@lute/io", luteopen_io}, + }}; + + for (const auto& [name, func] : libs) + { + lua_pushcfunction(L, luarequire_registermodule, nullptr); + lua_pushstring(L, name); + func(L); + lua_call(L, 2, 0); + } +} + static bool copyLuauObject(lua_State* from, lua_State* to, int fromIdx) { switch (lua_type(from, fromIdx)) @@ -202,11 +239,16 @@ int lua_spawn(lua_State* L) const char* file = luaL_checkstring(L, 1); auto child = std::make_shared(); + if (!child->useDedicatedUvLoop()) + luaL_error(L, "Failed to spawn, unable to initialize dedicated uv loop"); + + registerSpawnedRuntime(child); setupState( *child, [](lua_State* L) { + luteopen_libs(L); luaopen_require(L, requireConfigInit, createChildVmRequireContext(L)); } ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b353f8384..0c044f1c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -24,7 +24,8 @@ target_sources(Lute.Test PRIVATE src/packagerequire.test.cpp src/require.test.cpp src/staticrequires.test.cpp - src/stdsystem.test.cpp) + src/stdsystem.test.cpp + src/vmcreate.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) diff --git a/tests/src/vm/vm_helper.luau b/tests/src/vm/vm_helper.luau new file mode 100644 index 000000000..db401c6c3 --- /dev/null +++ b/tests/src/vm/vm_helper.luau @@ -0,0 +1,14 @@ +local net = require("@lute/net") + +return { + check = function() + local ok, err = pcall(net.serve, nil) + if ok then + error("expected net.serve to error on invalid arguments") + end + + if type(err) == "string" and string.find(err, "not implemented", 1, true) then + error("expected native @lute/net implementation in child VM, got definitions stub") + end + end, +} diff --git a/tests/src/vm/vm_requirer.luau b/tests/src/vm/vm_requirer.luau new file mode 100644 index 000000000..1315bfcf2 --- /dev/null +++ b/tests/src/vm/vm_requirer.luau @@ -0,0 +1,3 @@ +local vm = require("@lute/vm") + +vm.create("./vm_helper").check() diff --git a/tests/src/vmcreate.test.cpp b/tests/src/vmcreate.test.cpp new file mode 100644 index 000000000..32e2270c2 --- /dev/null +++ b/tests/src/vmcreate.test.cpp @@ -0,0 +1,23 @@ +#include "lute/climain.h" + +#include "Luau/FileUtils.h" + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +#include +#include + +TEST_CASE_FIXTURE(LuteFixture, "vm_create_child_vm_loads_lute_modules") +{ + char executablePlaceholder[] = "lute"; + + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/vm/vm_requirer.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } +}