From 0d7224a2b8826af30e0830c6d186f60ce182099b Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 11:50:22 +0300 Subject: [PATCH] =?UTF-8?q?fix(lua):=20WASM=20memory=20access=20=E2=80=94?= =?UTF-8?q?=20resume=20coroutine=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20lua.gl?= =?UTF-8?q?obal.get=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20doStringSync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При каждом tick'е tickScheduler делал lua.doStringSync(resume code), что вызывало re-entrant WASM-крах memory access out of bounds после нескольких task.wait() итераций. Фикс: кешируем ссылку на __rbxl_resume_co при init и зовём её напрямую. Это безопасный путь (не парсит код заново, не открывает вложенный Lua-state поверх существующего). --- src/editor/engine/lua/RobloxShim.js | 76 ++++++++++++++++------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 7f68d7d..3fd24df 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -734,10 +734,11 @@ export function registerRobloxShim(lua, opts) { }); }); - // Лагалейн Lua-код: определяем task.wait, wait, и обёртку для скрипта. - // Этот код выполняется ВНУТРИ Lua VM. + // Lua-prelude: task.wait через coroutine.yield + готовая resume-функция. + // Главное: __rbxl_resume_co определена в Lua и вызывается из JS через + // lua.global.get('__rbxl_resume_co') — это безопаснее чем doStringSync + // потому что не парсит код заново и не создаёт re-entrant проблем. lua.doStringSync(` - -- task.wait(sec) → coroutine.yield(sec); main-loop вернёт через delay sec local function rbx_wait(sec) sec = sec or 0 coroutine.yield(sec) @@ -749,7 +750,23 @@ export function registerRobloxShim(lua, opts) { task = { wait = rbx_wait } end wait = rbx_wait + + -- Вызывается из JS-tickScheduler: + -- возвращает next-delay (number) если co yield'нулся ещё раз, + -- или nil если co завершился / умер. + function __rbxl_resume_co(co) + if not co or coroutine.status(co) ~= 'suspended' then return nil end + local ok, ret = coroutine.resume(co) + if not ok then + return false, tostring(ret) + end + if coroutine.status(co) == 'dead' then return nil end + if type(ret) == 'number' then return ret end + return 0 + end `); + // Достаём ссылку на Lua-функцию один раз; вызовы безопасны (не doStringSync) + const luaResumeCo = lua.global.get('__rbxl_resume_co'); // === Setter Part-свойств (Position/Size/Color/...) === // Юзер пишет: part.Position = Vector3.new(0, 10, 0) @@ -805,39 +822,32 @@ export function registerRobloxShim(lua, opts) { } } // 2. Резюм coroutine'ов которые task.wait() - // Скрипт-coroutine при первом запуске yield'ит с delay (от task.wait). - // Мы регистрируем delay через __rbxl_schedule_resume? Нет — проще: - // отслеживаем последний yield-результат через coroutine.resume → возвращает - // delay. После init скрипта проверим всех coroutines: если status==suspended, - // и им пора — резюмируем. - for (const [coId, co] of coroutines) { - let entry = waitingCoros.find(w => w.coId === coId); - if (!entry) { - // Первый раз видим этот co — нужно вытащить delay из yield-результата. - // Это сделано в _onYield ниже. - continue; + // Используем lua.global.get-кешированную __rbxl_resume_co функцию — + // безопаснее чем doStringSync (не re-entrant в WASM). + const dueCoros = []; + for (let i = waitingCoros.length - 1; i >= 0; i--) { + if (waitingCoros[i].wakeAt <= now) { + dueCoros.push(waitingCoros[i]); + waitingCoros.splice(i, 1); } - if (entry.wakeAt > now) continue; - // Время резюмить - waitingCoros.splice(waitingCoros.indexOf(entry), 1); + } + for (const entry of dueCoros) { + const co = coroutines.get(entry.coId); + if (!co) continue; try { - const code = ` - local co = __rbxl_get_co(${JSON.stringify(coId)}) - if co and coroutine.status(co) == 'suspended' then - local ok, ret = coroutine.resume(co) - if not ok then - __rbxl_send_error(${JSON.stringify(coId)}, tostring(ret)) - __rbxl_unregister_coroutine(${JSON.stringify(coId)}) - elseif type(ret) == 'number' then - __rbxl_schedule_resume(${JSON.stringify(coId)}, ret) - elseif coroutine.status(co) == 'dead' then - __rbxl_unregister_coroutine(${JSON.stringify(coId)}) - end - end - `; - lua.doStringSync(code); + const result = luaResumeCo(co); + // result: number (next delay), nil (done), false+errStr (failed) + if (result === null || result === undefined) { + coroutines.delete(entry.coId); + } else if (typeof result === 'number') { + waitingCoros.push({ + coId: entry.coId, + wakeAt: SCHEDULER.now() + result * 1000, + }); + } } catch (e) { - send('log', { level: 'error', text: `[coroutine resume ${coId}] ${e?.message || e}` }); + send('log', { level: 'error', text: `[coroutine ${entry.coId}] ${e?.message || e}` }); + coroutines.delete(entry.coId); } } },